NvimHandler._get_tk_attrs()   F
last analyzed

Complexity

Conditions 14

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
c 0
b 0
f 0
dl 0
loc 41
rs 2.7581

How to fix   Complexity   

Complexity

Complex classes like NvimHandler._get_tk_attrs() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
'''
2
Implements a UI for neovim  using tkinter.
3
4
* The widget has lines updated/deleted so that any
5
  given time it only contains what is being displayed.
6
7
* The widget is filled with spaces
8
'''
9
10
import sys
11
import math
12
import time
13
from neovim import attach
14
15
# from tkquick.gui.tools import rate_limited
16
17
from pytknvim.ui_bridge import UIBridge
18
from pytknvim.screen import Screen
19
from pytknvim.util import _stringify_key, _stringify_color
20
from pytknvim.util import _split_color, _invert_color
21
from pytknvim.util import debug_echo
22
from pytknvim.util import attach_headless, attach_child
23
from pytknvim import tk_util
24
25
try:
26
    import Tkinter as tk
27
    import tkFont as tkfont
28
    import ttk
29
except ImportError:
30
    import tkinter as tk
31
    import tkinter.font as tkfont
32
33
import attr
34
35
RESIZE_DELAY = 0.04
36
37
def parse_tk_state(state):
38
    if state & 0x4:
39
        return 'Ctrl'
40
    elif state & 0x8:
41
        return 'Alt'
42
    elif state & 0x1:
43
        return 'Shift'
44
45
46
tk_modifiers = ('Alt_L', 'Alt_R',
47
                'Control_L', 'Control_R',
48
                'Shift_L', 'Shift_R',
49
                'Win_L', 'Win_R')
50
51
52
KEY_TABLE = {
53
    'slash': '/',
54
    'backslash': '\\',
55
    'asciicircumf': '^',
56
    'at': '@',
57
    'numbersign': '#',
58
    'dollar': '$',
59
    'percent': '%',
60
    'ampersand': '&',
61
    'asterisk': '*',
62
    'parenleft': '(',
63
    'parenright': ')',
64
    'underscore': '_',
65
    'plus': '+',
66
    'minus': '-',
67
    'bracketleft': '[',
68
    'bracketright': ']',
69
    'braceleft': '{',
70
    'braceright': '}',
71
    'quotedbl': '"',
72
    'apostrophe': "'",
73
    'less': "<",
74
    'greater': ">",
75
    'comma': ",",
76
    'period': ".",
77
    'BackSpace': 'BS',
78
    'Return': 'CR',
79
    'Escape': 'Esc',
80
    'Delete': 'Del',
81
    'Next': 'PageUp',
82
    'Prior': 'PageDown',
83
    'Enter': 'CR',
84
}
85
86
87
class MixTk():
88
    '''
89
    Tkinter actions we bind and use to communicate to neovim
90
    '''
91
    def tk_key_pressed(self,event, **k):
92
        keysym = event.keysym
93
        state = parse_tk_state(event.state)
94
        if event.char not in ('', ' ') \
95
                    and state in (None, 'Shift'):
96
            if event.keysym_num == ord(event.char):
97
                # Send through normal keys
98
                self._bridge.input(event.char)
99
                return
100
        if keysym in tk_modifiers:
101
            # We don't need to track the state of modifier bits
102
            return
103
        if keysym.startswith('KP_'):
104
            keysym = keysym[3:]
105
106
        # Translated so vim understands
107
        input_str = _stringify_key( KEY_TABLE.get(keysym, keysym), state)
108
        self._bridge.input(input_str)
109
110
111
    def _tk_quit(self, *args):
112
        self._bridge.exit()
113
114
115
    # @rate_limited(1/RESIZE_DELAY, mode='kill')
116
    def _tk_resize(self, event):
117
        '''Let Neovim know we are changing size'''
118
        cols = int(math.floor(event.width / self._colsize))
119
        rows = int(math.floor(event.height / self._rowsize))
120
        if self._screen.columns == cols:
121
            if self._screen.rows == rows:
122
                return
123
        self.current_cols = cols
124
        self.current_rows = rows
125
        self._bridge.resize(cols, rows)
126
        if self.debug_echo:
127
            print('resizing c, r, w, h',
128
                    cols,rows, event.width, event.height)
129
130
131
    def bind_resize(self):
132
        '''
133
        after calling,
134
        widget changes will now be passed along to neovim
135
        '''
136
        print('binding resize to', self, self.text)
137
        self._configure_id = self.text.bind('<Configure>', self._tk_resize)
138
139
140
    def unbind_resize(self):
141
        '''
142
        after calling,
143
        widget size changes will not be passed along to nvim
144
        '''
145
        print('unbinding resize from', self)
146
        self.text.unbind('<Configure>', self._configure_id)
147
148
149
    def _get_row(self, screen_row):
150
        '''change a screen row to a tkinter row,
151
        defaults to screen.row'''
152
        if screen_row is None:
153
            screen_row = self._screen.row
154
        return screen_row + 1
155
156
157
    def _get_col(self, screen_col):
158
        '''change a screen col to a tkinter row,
159
        defaults to screen.col'''
160
        if screen_col is None:
161
            screen_col = self._screen.col
162
        return screen_col
163
164
165
    def tk_delete_line(self, screen_col=None, screen_row=None,
166
                                       del_eol=False, count=1):
167
        '''
168
        To specifiy where to start the delete from
169
        screen_col (defualts to screen.row)
170
        screen_row (defaults to screen.col)
171
172
        To delete the eol char aswell
173
        del_eol (defaults to False)
174
175
        count is the number of lines to delete
176
        '''
177
        line = self._get_row(screen_row)
178
        col = self._get_col(screen_col)
179
        start = "%d.%d" % (line, col)
180
        if del_eol:
181
            end = "%d.0" % (line + count)
182
        else:
183
            end = "%d.end" % (line + count - 1)
184
        self.text.delete(start, end)
185
        gotten = self.text.get(start, end)
186
        if self.debug_echo == True:
187
            print('deleted  from ' + start + ' to end ' +end)
188
            print('deleted '+repr(gotten))
189
190
191
    def tk_pad_line(self, screen_col=None, add_eol=False,
192
                                    screen_row=None, count=1):
193
        '''
194
        add required blank spaces at the end of the line
195
        can apply action to multiple rows py passing a count
196
        in
197
        '''
198
        line = self._get_row(screen_row)
199
        col = self._get_col(screen_col)
200
        for n in range(0, count):
201
            start = "%d.%d" % (line + n, col)
202
            spaces = " " * (self.current_cols - col)
203
            if add_eol:
204
                spaces += '\n'
205
            if self.debug_echo:
206
                pass
207
                # print('padding from ', start, ' with %d: '
208
                                                # % len(spaces))
209
                # print(repr(spaces))
210
            self.text.insert(start, spaces)
211
212
213
    def _start_blinking(self):
214
        # cursor is drawn seperatley in the window
215
        row, col = self._screen.row, self._screen.col
216
        text, attrs = self._screen.get_cursor()
217
        pos = "%d.%d" % (row +1, col)
218
219
        if not attrs:
220
            attrs = self._get_tk_attrs(None)
221
        fg = attrs[1].get('foreground')
222
        bg = attrs[1].get('background')
223
        try:
224
            self.text.stop_blink()
225
        except Exception:
226
            pass
227
        self.text.blink_cursor(pos, fg, bg)
228
229
230
class NvimHandler(MixTk):
231
    '''These methods get called by neovim'''
232
233
    def __init__(self, text, toplevel, address=-1, debug_echo=False):
234
        self.text = text
235
        self.toplevel = toplevel
236
        self.debug_echo = debug_echo
237
238
        self._insert_cursor = False
239
        self._screen = None
240
        self._foreground = -1
241
        self._background = -1
242
        self._pending = [0,0,0]
243
        self._attrs = {}
244
        self._reset_attrs_cache()
245
        self._colsize = None
246
        self._rowsize = None
247
248
        # Have we connected to an nvim instance?
249
        self.connected = False
250
        # Connecition Info for neovim
251
        self.address = address
252
        cols = 80
253
        rows = 24
254
        self.current_cols = cols
255
        self.current_rows = rows
256
257
        self._screen = Screen(cols, rows)
258
        self._bridge = UIBridge()
259
260
    @debug_echo
261
    def connect(self, *nvim_args, address=None, headless=False, exec_name='nvim'):
262
        # Value has been set, otherwise default to this functions default value
263
        if self.address != -1 and not address:
264
            address = self.address
265
266
        if headless:
267
            nvim = attach_headless(nvim_args, address)
268
        elif address:
269
            nvim = attach('socket', path=address, argv=nvim_args)
270
        else:
271
            nvim = attach_child(nvim_args=nvim_args, exec_name=exec_name)
272
273
        self._bridge.connect(nvim, self.text)
274
        self._screen = Screen(self.current_cols, self.current_rows)
275
        self._bridge.attach(self.current_cols, self.current_rows, rgb=True)
276
        # if len(sys.argv) > 1:
277
            # nvim.command('edit ' + sys.argv[1])
278
        self.connected = True
279
        self.text.nvim = nvim
280
        return nvim
281
282
    @debug_echo
283
    def _nvim_resize(self, cols, rows):
284
        '''Let neovim update tkinter when neovim changes size'''
285
        # TODO
286
        # Make sure it works when user changes font,
287
        # only can support mono font i think..
288
        self._screen = Screen(cols, rows)
289
290
    @debug_echo
291
    def _nvim_clear(self):
292
        '''
293
        wipe everyything, even the ~ and status bar
294
        '''
295
        self._screen.clear()
296
297
        self.tk_delete_line(del_eol=True,
298
                            screen_row=0,
299
                            screen_col=0,
300
                            count=self.current_rows)
301
        # Add spaces everywhere
302
        self.tk_pad_line(screen_row=0,
303
                         screen_col=0,
304
                         count=self.current_rows,
305
                         add_eol=True,)
306
307
308
    @debug_echo
309
    def _nvim_eol_clear(self):
310
        '''
311
        delete from index to end of line,
312
        fill with whitespace
313
        leave eol intact
314
        '''
315
        self._screen.eol_clear()
316
        self.tk_delete_line(del_eol=False)
317
        self.tk_pad_line(screen_col=self._screen.col,
318
                         add_eol=False)
319
320
321
    @debug_echo
322
    def _nvim_cursor_goto(self, row, col):
323
        '''Move gui cursor to position'''
324
        self._screen.cursor_goto(row, col)
325
        self.text.see("1.0")
326
327
328
    @debug_echo
329
    def _nvim_busy_start(self):
330
        self._busy = True
331
332
333
    @debug_echo
334
    def _nvim_busy_stop(self):
335
        self._busy = False
336
337
338
    @debug_echo
339
    def _nvim_mouse_on(self):
340
        self.mouse_enabled = True
341
342
343
    @debug_echo
344
    def _nvim_mouse_off(self):
345
        self.mouse_enabled = False
346
347
348
    @debug_echo
349
    def _nvim_mode_change(self, mode):
350
        self._insert_cursor = mode == 'insert'
351
352
353
    @debug_echo
354
    def _nvim_set_scroll_region(self, top, bot, left, right):
355
        self._screen.set_scroll_region(top, bot, left, right)
356
357
358
    @debug_echo
359
    def _nvim_scroll(self, count):
360
        self._flush()
361
        self._screen.scroll(count)
362
        abs_count = abs(count)
363
        # The minus 1 is because we want our tk_* functions
364
        # to operate on the row passed in
365
        delta = abs_count - 1
366
        # Down
367
        if count > 0:
368
            delete_row = self._screen.top
369
            pad_row = self._screen.bot - delta
370
        # Up
371
        else:
372
            delete_row = self._screen.bot - delta
373
            pad_row = self._screen.top
374
375
        self.tk_delete_line(screen_row=delete_row,
376
                            screen_col=0,
377
                            del_eol=True,
378
                            count=abs_count)
379
        self.tk_pad_line(screen_row=pad_row,
380
                         screen_col=0,
381
                         add_eol=True,
382
                         count=abs_count)
383
        # self.text.yview_scroll(count, 'units')
384
385
386
    # @debug_echo
387
    def _nvim_highlight_set(self, attrs):
388
        self._attrs = self._get_tk_attrs(attrs)
389
390
391
    # @debug_echo
392
    def _reset_attrs_cache(self):
393
        self._tk_text_cache = {}
394
        self._tk_attrs_cache = {}
395
396
397
    @debug_echo
398
    def _get_tk_attrs(self, attrs):
399
        key = tuple(sorted((k, v,) for k, v in (attrs or {}).items()))
400
        rv = self._tk_attrs_cache.get(key, None)
401
        if rv is None:
402
            fg = self._foreground if self._foreground != -1\
403
                                                else 0
404
            bg = self._background if self._background != -1\
405
                                                else 0xffffff
406
            n = {'foreground': _split_color(fg),
407
                'background': _split_color(bg),}
408
            if attrs:
409
                # make sure that fg and bg are assigned first
410
                for k in ['foreground', 'background']:
411
                    if k in attrs:
412
                        n[k] = _split_color(attrs[k])
413
                for k, v in attrs.items():
414
                    if k == 'reverse':
415
                        n['foreground'], n['background'] = \
416
                            n['background'], n['foreground']
417
                    elif k == 'italic':
418
                        n['slant'] = 'italic'
419
                    elif k == 'bold':
420
                        n['weight'] = 'bold'
421
                        # TODO
422
                        # if self._bold_spacing:
423
                            # n['letter_spacing'] \
424
                                    # = str(self._bold_spacing)
425
                    elif k == 'underline':
426
                        n['underline'] = '1'
427
            c = dict(n)
428
            c['foreground'] = _invert_color(*_split_color(fg))
429
            c['background'] = _invert_color(*_split_color(bg))
430
            c['foreground'] = _stringify_color(*c['foreground'])
431
            c['background'] = _stringify_color(*c['background'])
432
            n['foreground'] = _stringify_color(*n['foreground'])
433
            n['background'] = _stringify_color(*n['background'])
434
            # n = normal, c = cursor
435
            rv = (n, c)
436
            self._tk_attrs_cache[key] = (n, c)
437
        return rv
438
439
440
    # @debug_echo
441
    def _nvim_put(self, text):
442
        '''
443
        put a charachter into position, we only write the lines
444
        when a new row is being edited
445
        '''
446
        if self._screen.row != self._pending[0]:
447
            # write to screen if vim puts stuff on  a new line
448
            self._flush()
449
450
        self._screen.put(text, self._attrs)
451
        self._pending[1] = min(self._screen.col - 1,
452
                               self._pending[1])
453
        self._pending[2] = max(self._screen.col,
454
                               self._pending[2])
455
456
457
    # @debug_echo
458
    def _nvim_bell(self):
459
        pass
460
461
462
    # @debug_echo
463
    def _nvim_visual_bell(self):
464
        pass
465
466
467
    # @debug_echo
468
    def _nvim_update_fg(self, fg):
469
        self._foreground = fg
470
        self._reset_attrs_cache()
471
        foreground = self._get_tk_attrs(None)[0]['foreground']
472
        self.text.config(foreground=foreground)
473
474
475
    # @debug_echo
476
    def _nvim_update_bg(self, bg):
477
        self._background = bg
478
        self._reset_attrs_cache()
479
        background = self._get_tk_attrs(None)[0]['background']
480
        self.text.config(background=background)
481
482
483
    # @debug_echo
484
    def _nvim_update_suspend(self, arg):
485
        self.root.iconify()
486
487
488
    # @debug_echo
489
    def _nvim_set_title(self, title):
490
        self.root.title(title)
491
492
493
    # @debug_echo
494
    def _nvim_set_icon(self, icon):
495
        self._icon = tk.PhotoImage(file=icon)
496
        self.root.tk.call('wm', 'iconphoto',
497
                          self.root._w, self._icon)
498
499
500
    # @debug_echo
501
    def _flush(self):
502
        row, startcol, endcol = self._pending
503
        self._pending[0] = self._screen.row
504
        self._pending[1] = self._screen.col
505
        self._pending[2] = self._screen.col
506
        if startcol == endcol:
507
            #print('startcol is endcol return, row %s col %s'% (self._screen.row, self._screen.col))
508
            return
509
        ccol = startcol
510
        buf = []
511
        bold = False
512
        for _, col, text, attrs in self._screen.iter(row,
513
                                    row, startcol, endcol - 1):
514
            newbold = attrs and 'bold' in attrs[0]
515
            if newbold != bold or not text:
516
                if buf:
517
                    self._draw(row, ccol, buf)
518
                bold = newbold
519
                buf = [(text, attrs,)]
520
                ccol = col
521
            else:
522
                buf.append((text, attrs,))
523
        if buf:
524
            self._draw(row, ccol, buf)
525
        else:
526
            pass
527
            # print('flush with no draw')
528
529
530
    @debug_echo
531
    def _draw(self, row, col, data):
532
        '''
533
        updates a line :)
534
        '''
535
        for text, attrs in data:
536
            try:
537
                start = end
538
            except UnboundLocalError:
539
                start = "{}.{}".format(row + 1, col)
540
            end = start+'+{0}c'.format(len(text))
541
542
            if not attrs:
543
                attrs = self._get_tk_attrs(None)
544
            attrs = attrs[0]
545
546
            # if self.debug_echo:
547
                # print('replacing ', repr(self.text.get(start, end)))
548
                # print('with ', repr(text), ' at ', start, ' ',end)
549
            self.text.replace(start, end, text)
550
551
            if attrs:
552
                self.text.apply_attribute(attrs, start, end)
553
            start
554
555
556
    @debug_echo
557
    def _nvim_exit(self, arg):
558
        print('in exit')
559
        import pdb;pdb.set_trace()
560
        # self.root.destroy()
561
562
    @debug_echo
563
    def _nvim_update_sp(self, *args):
564
        pass
565
566
567
class NvimTk(tk_util.Text):
568
    '''namespace for neovim related methods,
569
    requests are generally prefixed with _tk_,
570
    responses are prefixed with _nvim_
571
    '''
572
    # we get keys, mouse movements inside tkinter, using binds,
573
    # These binds are handed off to neovim using _input
574
575
    # Neovim interpruts the actions and calls certain
576
    # functions which are defined and implemented in tk
577
578
    # The api from neovim does stuff line by line,
579
    # so each callback from neovim produces a series
580
    # of miniscule actions which in the end updates a line
581
582
    # So we can shutdown the neovim connections
583
    instances = []
584
585
    def __init__(self, parent, *_, address=False, toplevel=False, **kwargs):
586
        '''
587
        :parent: normal tkinter parent or master of the widget
588
        :toplevel: , if true will resize based off the toplevel etc
589
        :address: neovim connection info
590
            named pipe /tmp/nvim/1231
591
            tcp/ip socket 127.0.0.1:4444
592
            'child'
593
            'headless'
594
        :kwargs: config options for text widget
595
        '''
596
        tk_util.Text.__init__(self, parent, **kwargs)
597
        self.nvim_handler = NvimHandler(text=self,
598
                                        toplevel=toplevel,
599
                                        address=address,
600
                                        debug_echo=False)
601
602
        # TODO weak ref?
603
        NvimTk.instances.append(self)
604
605
    def _nvimtk_config(self, *args):
606
        '''required config'''
607
        # Hide tkinter cursor
608
        self.config(insertontime=0)
609
610
        # Remove Default Bindings and what happens on insert etc
611
        bindtags = list(self.bindtags())
612
        bindtags.remove("Text")
613
        self.bindtags(tuple(bindtags))
614
615
        self.bind('<Key>', self.nvim_handler.tk_key_pressed)
616
617
        self.bind('<Button-1>', lambda e: self.focus_set())
618
619
        # The negative number makes it pixels instead of point sizes
620
        size = self.make_font_size(13)
621
        self._fnormal = tkfont.Font(family='Monospace', size=size)
622
        self._fbold = tkfont.Font(family='Monospace', weight='bold', size=size)
623
        self._fitalic = tkfont.Font(family='Monospace', slant='italic', size=size)
624
        self._fbolditalic = tkfont.Font(family='Monospace', weight='bold',
625
                                 slant='italic', size=size)
626
        self.config(font=self._fnormal, wrap=tk.NONE)
627
628
        self.nvim_handler._colsize = self._fnormal.measure('M')
629
        self.nvim_handler._rowsize = self._fnormal.metrics('linespace')
630
631
632
    def nvim_connect(self, *a, **k):
633
        ''' force connection to neovim '''
634
        self.nvim_handler.connect(*a, **k)
635
        self._nvimtk_config()
636
637
    @staticmethod
638
    def kill_all():
639
        ''' Kill all the neovim connections '''
640
        raise NotImplementedError
641
        for self in NvimTk.instances:
642
            if self.nvim_handler.connected:
643
                # Function hangs us..
644
                # self.after(1, self.nvim_handler._bridge.exit)
645
                self.nvim_handler._bridge.exit()
646
647
648
    def pack(self, *arg, **kwarg):
649
        ''' connect to neovim if required'''
650
        tk_util.Text.pack(self, *arg, **kwarg)
651
        if not self.nvim_handler.connected:
652
            self.nvim_connect()
653
654
        self.nvim_handler.bind_resize()
655
656
657
    def grid(self, *arg, **kwarg):
658
        ''' connect to neovim if required'''
659
        tk_util.Text.grid(self, *arg, **kwarg)
660
        if not self.nvim_handler.connected:
661
            self.nvim_connect()
662
663
        self.nvim_handler.bind_resize()
664
665
666
    def schedule_screen_update(self, apply_updates):
667
        '''This function is called from the bridge,
668
           apply_updates calls the required nvim actions'''
669
        # if time.time() - self.start_time > 1:
670
            # print()
671
        # self.start_time = time.time()
672
        def do():
673
            apply_updates()
674
            self.nvim_handler._flush()
675
            self.nvim_handler._start_blinking()
676
        self.master.after_idle(do)
677
678
679
    def quit(self):
680
        ''' destroy the widget, called from the bridge'''
681
        self.after_idle(self.destroy)
682
683
# if __name__ == '__main__':
684
    # main()
685