Completed
Push — master ( bcff18...adb900 )
by Nicolas
01:15
created

glances.outputs._GlancesCurses.display()   F

Complexity

Conditions 48

Size

Total Lines 301

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 48
dl 0
loc 301
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like glances.outputs._GlancesCurses.display() 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
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2015 Nicolargo <[email protected]>
6
#
7
# Glances is free software; you can redistribute it and/or modify
8
# it under the terms of the GNU Lesser General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Glances is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Lesser General Public License for more details.
16
#
17
# You should have received a copy of the GNU Lesser General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20
"""Curses interface class."""
21
22
import re
23
import sys
24
25
from glances.compat import u
26
from glances.globals import OSX, WINDOWS
27
from glances.logger import logger
28
from glances.logs import glances_logs
29
from glances.processes import glances_processes
30
from glances.timer import Timer
31
32
# Import curses lib for "normal" operating system and consolelog for Windows
33
if not WINDOWS:
34
    try:
35
        import curses
36
        import curses.panel
37
        from curses.textpad import Textbox
38
    except ImportError:
39
        logger.critical(
40
            "Curses module not found. Glances cannot start in standalone mode.")
41
        sys.exit(1)
42
else:
43
    from glances.outputs.glances_colorconsole import WCurseLight
44
    curses = WCurseLight()
45
46
47
class _GlancesCurses(object):
48
49
    """This class manages the curses display (and key pressed).
50
51
    Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser.
52
    """
53
54
    def __init__(self, args=None):
55
        # Init args
56
        self.args = args
57
58
        # Init windows positions
59
        self.term_w = 80
60
        self.term_h = 24
61
62
        # Space between stats
63
        self.space_between_column = 3
64
        self.space_between_line = 2
65
66
        # Init the curses screen
67
        self.screen = curses.initscr()
68
        if not self.screen:
69
            logger.critical("Cannot init the curses library.\n")
70
            sys.exit(1)
71
72
        # Init cursor
73
        self._init_cursor()
74
75
        # Init the colors
76
        self._init_colors()
77
78
        # Init main window
79
        self.term_window = self.screen.subwin(0, 0)
80
81
        # Init refresh time
82
        self.__refresh_time = args.time
83
84
        # Init edit filter tag
85
        self.edit_filter = False
86
87
        # Init the process min/max reset
88
        self.args.reset_minmax_tag = False
89
90
        # Catch key pressed with non blocking mode
91
        self.no_flash_cursor()
92
        self.term_window.nodelay(1)
93
        self.pressedkey = -1
94
95
        # History tag
96
        self._init_history()
97
98
    def _init_history(self):
99
        '''Init the history option'''
100
101
        self.reset_history_tag = False
102
        self.history_tag = False
103
        if self.args.enable_history:
104
            logger.info('Stats history enabled with output path %s' %
105
                        self.args.path_history)
106
            from glances.exports.glances_history import GlancesHistory
107
            self.glances_history = GlancesHistory(self.args.path_history)
108
            if not self.glances_history.graph_enabled():
109
                self.args.enable_history = False
110
                logger.error(
111
                    'Stats history disabled because MatPlotLib is not installed')
112
113
    def _init_cursor(self):
114
        '''Init cursors'''
115
116
        if hasattr(curses, 'noecho'):
117
            curses.noecho()
118
        if hasattr(curses, 'cbreak'):
119
            curses.cbreak()
120
        self.set_cursor(0)
121
122
    def _init_colors(self):
123
        '''Init the Curses color layout'''
124
125
        # Set curses options
126
        if hasattr(curses, 'start_color'):
127
            curses.start_color()
128
        if hasattr(curses, 'use_default_colors'):
129
            curses.use_default_colors()
130
131
        # Init colors
132
        if self.args.disable_bold:
133
            A_BOLD = 0
134
            self.args.disable_bg = True
135
        else:
136
            A_BOLD = curses.A_BOLD
137
138
        self.title_color = A_BOLD
139
        self.title_underline_color = A_BOLD | curses.A_UNDERLINE
140
        self.help_color = A_BOLD
141
142
        if curses.has_colors():
143
            # The screen is compatible with a colored design
144
            if self.args.theme_white:
145
                # White theme: black ==> white
146
                curses.init_pair(1, curses.COLOR_BLACK, -1)
147
            else:
148
                curses.init_pair(1, curses.COLOR_WHITE, -1)
149
            if self.args.disable_bg:
150
                curses.init_pair(2, curses.COLOR_RED, -1)
151
                curses.init_pair(3, curses.COLOR_GREEN, -1)
152
                curses.init_pair(4, curses.COLOR_BLUE, -1)
153
                curses.init_pair(5, curses.COLOR_MAGENTA, -1)
154
            else:
155
                curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED)
156
                curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_GREEN)
157
                curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE)
158
                curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
159
            curses.init_pair(6, curses.COLOR_RED, -1)
160
            curses.init_pair(7, curses.COLOR_GREEN, -1)
161
            curses.init_pair(8, curses.COLOR_BLUE, -1)
162
163
            # Colors text styles
164
            if curses.COLOR_PAIRS > 8:
165
                try:
166
                    curses.init_pair(9, curses.COLOR_MAGENTA, -1)
167
                except Exception:
168
                    if self.args.theme_white:
169
                        curses.init_pair(9, curses.COLOR_BLACK, -1)
170
                    else:
171
                        curses.init_pair(9, curses.COLOR_WHITE, -1)
172
                try:
173
                    curses.init_pair(10, curses.COLOR_CYAN, -1)
174
                except Exception:
175
                    if self.args.theme_white:
176
                        curses.init_pair(10, curses.COLOR_BLACK, -1)
177
                    else:
178
                        curses.init_pair(10, curses.COLOR_WHITE, -1)
179
180
                self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
181
                self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
182
                self.filter_color = curses.color_pair(10) | A_BOLD
183
184
            self.no_color = curses.color_pair(1)
185
            self.default_color = curses.color_pair(3) | A_BOLD
186
            self.nice_color = curses.color_pair(9) | A_BOLD
187
            self.cpu_time_color = curses.color_pair(9) | A_BOLD
188
            self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD
189
            self.ifWARNING_color = curses.color_pair(5) | A_BOLD
190
            self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD
191
            self.default_color2 = curses.color_pair(7) | A_BOLD
192
            self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
193
194
        else:
195
            # The screen is NOT compatible with a colored design
196
            # switch to B&W text styles
197
            self.no_color = curses.A_NORMAL
198
            self.default_color = curses.A_NORMAL
199
            self.nice_color = A_BOLD
200
            self.cpu_time_color = A_BOLD
201
            self.ifCAREFUL_color = curses.A_UNDERLINE
202
            self.ifWARNING_color = A_BOLD
203
            self.ifCRITICAL_color = curses.A_REVERSE
204
            self.default_color2 = curses.A_NORMAL
205
            self.ifCAREFUL_color2 = curses.A_UNDERLINE
206
            self.ifWARNING_color2 = A_BOLD
207
            self.ifCRITICAL_color2 = curses.A_REVERSE
208
            self.filter_color = A_BOLD
209
210
        # Define the colors list (hash table) for stats
211
        self.colors_list = {
212
            'DEFAULT': self.no_color,
213
            'UNDERLINE': curses.A_UNDERLINE,
214
            'BOLD': A_BOLD,
215
            'SORT': A_BOLD,
216
            'OK': self.default_color2,
217
            'FILTER': self.filter_color,
218
            'TITLE': self.title_color,
219
            'PROCESS': self.default_color2,
220
            'STATUS': self.default_color2,
221
            'NICE': self.nice_color,
222
            'CPU_TIME': self.cpu_time_color,
223
            'CAREFUL': self.ifCAREFUL_color2,
224
            'WARNING': self.ifWARNING_color2,
225
            'CRITICAL': self.ifCRITICAL_color2,
226
            'OK_LOG': self.default_color,
227
            'CAREFUL_LOG': self.ifCAREFUL_color,
228
            'WARNING_LOG': self.ifWARNING_color,
229
            'CRITICAL_LOG': self.ifCRITICAL_color,
230
            'PASSWORD': curses.A_PROTECT
231
        }
232
233
    def flash_cursor(self):
234
        self.term_window.keypad(1)
235
236
    def no_flash_cursor(self):
237
        self.term_window.keypad(0)
238
239
    def set_cursor(self, value):
240
        """Configure the curse cursor apparence.
241
242
        0: invisible
243
        1: visible
244
        2: very visible
245
        """
246
        if hasattr(curses, 'curs_set'):
247
            try:
248
                curses.curs_set(value)
249
            except Exception:
250
                pass
251
252
    def get_key(self, window):
253
        # Catch ESC key AND numlock key (issue #163)
254
        keycode = [0, 0]
255
        keycode[0] = window.getch()
256
        keycode[1] = window.getch()
257
258
        if keycode != [-1, -1]:
259
            logger.debug("Keypressed (code: %s)" % keycode)
260
261
        if keycode[0] == 27 and keycode[1] != -1:
262
            # Do not escape on specials keys
263
            return -1
264
        else:
265
            return keycode[0]
266
267
    def __catch_key(self, return_to_browser=False):
268
        # Catch the pressed key
269
        self.pressedkey = self.get_key(self.term_window)
270
271
        # Actions...
272
        if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
273
            # 'ESC'|'q' > Quit
274
            if return_to_browser:
275
                logger.info("Stop Glances client and return to the browser")
276
            else:
277
                self.end()
278
                logger.info("Stop Glances")
279
                sys.exit(0)
280
        elif self.pressedkey == 10:
281
            # 'ENTER' > Edit the process filter
282
            self.edit_filter = not self.edit_filter
283
        elif self.pressedkey == ord('0'):
284
            # '0' > Switch between IRIX and Solaris mode
285
            self.args.disable_irix = not self.args.disable_irix
286
        elif self.pressedkey == ord('1'):
287
            # '1' > Switch between CPU and PerCPU information
288
            self.args.percpu = not self.args.percpu
289
        elif self.pressedkey == ord('2'):
290
            # '2' > Enable/disable left sidebar
291
            self.args.disable_left_sidebar = not self.args.disable_left_sidebar
292
        elif self.pressedkey == ord('3'):
293
            # '3' > Enable/disable quicklook
294
            self.args.disable_quicklook = not self.args.disable_quicklook
295
        elif self.pressedkey == ord('4'):
296
            # '4' > Enable/disable all but quick look and load
297
            self.args.full_quicklook = not self.args.full_quicklook
298
            if self.args.full_quicklook:
299
                self.args.disable_quicklook = False
300
                self.args.disable_cpu = True
301
                self.args.disable_mem = True
302
                self.args.disable_swap = True
303
            else:
304
                self.args.disable_quicklook = False
305
                self.args.disable_cpu = False
306
                self.args.disable_mem = False
307
                self.args.disable_swap = False
308
        elif self.pressedkey == ord('5'):
309
            # '5' > Enable/disable top menu
310
            logger.info(self.args.disable_top)
311
            self.args.disable_top = not self.args.disable_top
312
            if self.args.disable_top:
313
                self.args.disable_quicklook = True
314
                self.args.disable_cpu = True
315
                self.args.disable_mem = True
316
                self.args.disable_swap = True
317
                self.args.disable_load = True
318
            else:
319
                self.args.disable_quicklook = False
320
                self.args.disable_cpu = False
321
                self.args.disable_mem = False
322
                self.args.disable_swap = False
323
                self.args.disable_load = False
324
        elif self.pressedkey == ord('/'):
325
            # '/' > Switch between short/long name for processes
326
            self.args.process_short_name = not self.args.process_short_name
327
        elif self.pressedkey == ord('a'):
328
            # 'a' > Sort processes automatically and reset to 'cpu_percent'
329
            glances_processes.auto_sort = True
330
            glances_processes.sort_key = 'cpu_percent'
331
        elif self.pressedkey == ord('b'):
332
            # 'b' > Switch between bit/s and Byte/s for network IO
333
            self.args.byte = not self.args.byte
334
        elif self.pressedkey == ord('B'):
335
            # 'B' > Switch between bit/s and IO/s for Disk IO
336
            self.args.diskio_iops = not self.args.diskio_iops
337
        elif self.pressedkey == ord('c'):
338
            # 'c' > Sort processes by CPU usage
339
            glances_processes.auto_sort = False
340
            glances_processes.sort_key = 'cpu_percent'
341
        elif self.pressedkey == ord('d'):
342
            # 'd' > Show/hide disk I/O stats
343
            self.args.disable_diskio = not self.args.disable_diskio
344
        elif self.pressedkey == ord('D'):
345
            # 'D' > Show/hide Docker stats
346
            self.args.disable_docker = not self.args.disable_docker
347
        elif self.pressedkey == ord('e'):
348
            # 'e' > Enable/Disable extended stats for top process
349
            self.args.enable_process_extended = not self.args.enable_process_extended
350
            if not self.args.enable_process_extended:
351
                glances_processes.disable_extended()
352
            else:
353
                glances_processes.enable_extended()
354
        elif self.pressedkey == ord('E'):
355
            # 'E' > Erase the process filter
356
            logger.info("Erase process filter")
357
            glances_processes.process_filter = None
358
        elif self.pressedkey == ord('F'):
359
            # 'F' > Switch between FS available and free space
360
            self.args.fs_free_space = not self.args.fs_free_space
361
        elif self.pressedkey == ord('f'):
362
            # 'f' > Show/hide fs / folder stats
363
            self.args.disable_fs = not self.args.disable_fs
364
            self.args.disable_folder = not self.args.disable_folder
365
        elif self.pressedkey == ord('g'):
366
            # 'g' > History
367
            self.history_tag = not self.history_tag
368
        elif self.pressedkey == ord('h'):
369
            # 'h' > Show/hide help
370
            self.args.help_tag = not self.args.help_tag
371
        elif self.pressedkey == ord('i'):
372
            # 'i' > Sort processes by IO rate (not available on OS X)
373
            glances_processes.auto_sort = False
374
            glances_processes.sort_key = 'io_counters'
375
        elif self.pressedkey == ord('I'):
376
            # 'I' > Show/hide IP module
377
            self.args.disable_ip = not self.args.disable_ip
378
        elif self.pressedkey == ord('l'):
379
            # 'l' > Show/hide log messages
380
            self.args.disable_log = not self.args.disable_log
381
        elif self.pressedkey == ord('m'):
382
            # 'm' > Sort processes by MEM usage
383
            glances_processes.auto_sort = False
384
            glances_processes.sort_key = 'memory_percent'
385
        elif self.pressedkey == ord('M'):
386
            # 'M' > Reset processes summary min/max
387
            self.args.reset_minmax_tag = not self.args.reset_minmax_tag
388
        elif self.pressedkey == ord('n'):
389
            # 'n' > Show/hide network stats
390
            self.args.disable_network = not self.args.disable_network
391
        elif self.pressedkey == ord('p'):
392
            # 'p' > Sort processes by name
393
            glances_processes.auto_sort = False
394
            glances_processes.sort_key = 'name'
395
        elif self.pressedkey == ord('r'):
396
            # 'r' > Reset history
397
            self.reset_history_tag = not self.reset_history_tag
398
        elif self.pressedkey == ord('R'):
399
            # 'R' > Hide RAID plugins
400
            self.args.disable_raid = not self.args.disable_raid
401
        elif self.pressedkey == ord('s'):
402
            # 's' > Show/hide sensors stats (Linux-only)
403
            self.args.disable_sensors = not self.args.disable_sensors
404
        elif self.pressedkey == ord('t'):
405
            # 't' > Sort processes by TIME usage
406
            glances_processes.auto_sort = False
407
            glances_processes.sort_key = 'cpu_times'
408
        elif self.pressedkey == ord('T'):
409
            # 'T' > View network traffic as sum Rx+Tx
410
            self.args.network_sum = not self.args.network_sum
411
        elif self.pressedkey == ord('u'):
412
            # 'u' > Sort processes by USER
413
            glances_processes.auto_sort = False
414
            glances_processes.sort_key = 'username'
415
        elif self.pressedkey == ord('U'):
416
            # 'U' > View cumulative network I/O (instead of bitrate)
417
            self.args.network_cumul = not self.args.network_cumul
418
        elif self.pressedkey == ord('w'):
419
            # 'w' > Delete finished warning logs
420
            glances_logs.clean()
421
        elif self.pressedkey == ord('x'):
422
            # 'x' > Delete finished warning and critical logs
423
            glances_logs.clean(critical=True)
424
        elif self.pressedkey == ord('z'):
425
            # 'z' > Enable/Disable processes stats (count + list + monitor)
426
            # Enable/Disable display
427
            self.args.disable_process = not self.args.disable_process
428
            # Enable/Disable update
429
            if self.args.disable_process:
430
                glances_processes.disable()
431
            else:
432
                glances_processes.enable()
433
        # Return the key code
434
        return self.pressedkey
435
436
    def end(self):
437
        """Shutdown the curses window."""
438
        if hasattr(curses, 'echo'):
439
            curses.echo()
440
        if hasattr(curses, 'nocbreak'):
441
            curses.nocbreak()
442
        if hasattr(curses, 'curs_set'):
443
            try:
444
                curses.curs_set(1)
445
            except Exception:
446
                pass
447
        curses.endwin()
448
449
    def init_line_column(self):
450
        """Init the line and column position for the curses inteface."""
451
        self.init_line()
452
        self.init_column()
453
454
    def init_line(self):
455
        """Init the line position for the curses inteface."""
456
        self.line = 0
457
        self.next_line = 0
458
459
    def init_column(self):
460
        """Init the column position for the curses inteface."""
461
        self.column = 0
462
        self.next_column = 0
463
464
    def new_line(self):
465
        """New line in the curses interface."""
466
        self.line = self.next_line
467
468
    def new_column(self):
469
        """New column in the curses interface."""
470
        self.column = self.next_column
471
472
    def display(self, stats, cs_status=None):
473
        """Display stats on the screen.
474
475
        stats: Stats database to display
476
        cs_status:
477
            "None": standalone or server mode
478
            "Connected": Client is connected to a Glances server
479
            "SNMP": Client is connected to a SNMP server
480
            "Disconnected": Client is disconnected from the server
481
482
        Return:
483
            True if the stats have been displayed
484
            False if the help have been displayed
485
        """
486
        # Init the internal line/column for Glances Curses
487
        self.init_line_column()
488
489
        # Get the screen size
490
        screen_x = self.screen.getmaxyx()[1]
491
        screen_y = self.screen.getmaxyx()[0]
492
493
        # No processes list in SNMP mode
494
        if cs_status == 'SNMP':
495
            # so... more space for others plugins
496
            plugin_max_width = 43
497
        else:
498
            plugin_max_width = None
499
500
        # Update the stats messages
501
        ###########################
502
503
        # Update the client server status
504
        self.args.cs_status = cs_status
505
        stats_system = stats.get_plugin(
506
            'system').get_stats_display(args=self.args)
507
        stats_uptime = stats.get_plugin('uptime').get_stats_display()
508
        if self.args.percpu:
509
            stats_cpu = stats.get_plugin('percpu').get_stats_display(args=self.args)
510
        else:
511
            stats_cpu = stats.get_plugin('cpu').get_stats_display(args=self.args)
512
        stats_load = stats.get_plugin('load').get_stats_display(args=self.args)
513
        stats_mem = stats.get_plugin('mem').get_stats_display(args=self.args)
514
        stats_memswap = stats.get_plugin('memswap').get_stats_display(args=self.args)
515
        stats_network = stats.get_plugin('network').get_stats_display(
516
            args=self.args, max_width=plugin_max_width)
517
        try:
518
            stats_ip = stats.get_plugin('ip').get_stats_display(args=self.args)
519
        except AttributeError:
520
            stats_ip = None
521
        stats_diskio = stats.get_plugin(
522
            'diskio').get_stats_display(args=self.args)
523
        stats_fs = stats.get_plugin('fs').get_stats_display(
524
            args=self.args, max_width=plugin_max_width)
525
        stats_folders = stats.get_plugin('folders').get_stats_display(
526
            args=self.args, max_width=plugin_max_width)
527
        stats_raid = stats.get_plugin('raid').get_stats_display(
528
            args=self.args)
529
        stats_sensors = stats.get_plugin(
530
            'sensors').get_stats_display(args=self.args)
531
        stats_now = stats.get_plugin('now').get_stats_display()
532
        stats_docker = stats.get_plugin('docker').get_stats_display(
533
            args=self.args)
534
        stats_processcount = stats.get_plugin(
535
            'processcount').get_stats_display(args=self.args)
536
        stats_monitor = stats.get_plugin(
537
            'monitor').get_stats_display(args=self.args)
538
        stats_alert = stats.get_plugin(
539
            'alert').get_stats_display(args=self.args)
540
541
        # Adapt number of processes to the available space
542
        max_processes_displayed = screen_y - 11 - \
543
            self.get_stats_display_height(stats_alert) - \
544
            self.get_stats_display_height(stats_docker)
545
        try:
546
            if self.args.enable_process_extended and not self.args.process_tree:
547
                max_processes_displayed -= 4
548
        except AttributeError:
549
            pass
550
        if max_processes_displayed < 0:
551
            max_processes_displayed = 0
552
        if (glances_processes.max_processes is None or
553
                glances_processes.max_processes != max_processes_displayed):
554
            logger.debug("Set number of displayed processes to {0}".format(max_processes_displayed))
555
            glances_processes.max_processes = max_processes_displayed
556
557
        stats_processlist = stats.get_plugin(
558
            'processlist').get_stats_display(args=self.args)
559
560
        # Display the stats on the curses interface
561
        ###########################################
562
563
        # Help screen (on top of the other stats)
564
        if self.args.help_tag:
565
            # Display the stats...
566
            self.display_plugin(
567
                stats.get_plugin('help').get_stats_display(args=self.args))
568
            # ... and exit
569
            return False
570
571
        # ==================================
572
        # Display first line (system+uptime)
573
        # ==================================
574
        # Space between column
575
        self.space_between_column = 0
576
        self.new_line()
577
        l_uptime = self.get_stats_display_width(
578
            stats_system) + self.space_between_column + self.get_stats_display_width(stats_ip) + 3 + self.get_stats_display_width(stats_uptime)
579
        self.display_plugin(
580
            stats_system, display_optional=(screen_x >= l_uptime))
581
        self.new_column()
582
        self.display_plugin(stats_ip)
583
        # Space between column
584
        self.space_between_column = 3
585
        self.new_column()
586
        self.display_plugin(stats_uptime)
587
588
        # ========================================================
589
        # Display second line (<SUMMARY>+CPU|PERCPU+LOAD+MEM+SWAP)
590
        # ========================================================
591
        self.init_column()
592
        self.new_line()
593
594
        # Init quicklook
595
        stats_quicklook = {'msgdict': []}
596
        quicklook_width = 0
597
598
        # Get stats for CPU, MEM, SWAP and LOAD (if needed)
599
        if self.args.disable_cpu:
600
            cpu_width = 0
601
        else:
602
            cpu_width = self.get_stats_display_width(stats_cpu)
603
        if self.args.disable_mem:
604
            mem_width = 0
605
        else:
606
            mem_width = self.get_stats_display_width(stats_mem)
607
        if self.args.disable_swap:
608
            swap_width = 0
609
        else:
610
            swap_width = self.get_stats_display_width(stats_memswap)
611
        if self.args.disable_load:
612
            load_width = 0
613
        else:
614
            load_width = self.get_stats_display_width(stats_load)
615
616
        # Size of plugins but quicklook
617
        stats_width = cpu_width + mem_width + swap_width + load_width
618
619
        # Number of plugin but quicklook
620
        stats_number = (
621
            int(not self.args.disable_cpu and stats_cpu['msgdict'] != []) +
622
            int(not self.args.disable_mem and stats_mem['msgdict'] != []) +
623
            int(not self.args.disable_swap and stats_memswap['msgdict'] != []) +
624
            int(not self.args.disable_load and stats_load['msgdict'] != []))
625
626
        if not self.args.disable_quicklook:
627
            # Quick look is in the place !
628
            if self.args.full_quicklook:
629
                quicklook_width = screen_x - (stats_width + 8 + stats_number * self.space_between_column)
630
            else:
631
                quicklook_width = min(screen_x - (stats_width + 8 + stats_number * self.space_between_column), 79)
632
            try:
633
                stats_quicklook = stats.get_plugin(
634
                    'quicklook').get_stats_display(max_width=quicklook_width, args=self.args)
635
            except AttributeError as e:
636
                logger.debug("Quicklook plugin not available (%s)" % e)
637
            else:
638
                quicklook_width = self.get_stats_display_width(stats_quicklook)
639
                stats_width += quicklook_width + 1
640
            self.space_between_column = 1
641
            self.display_plugin(stats_quicklook)
642
            self.new_column()
643
644
        # Compute spaces between plugins
645
        # Note: Only one space between Quicklook and others
646
        display_optional_cpu = True
647
        display_optional_mem = True
648
        if stats_number > 1:
649
            self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
650
            # No space ? Remove optionnal MEM stats
651
            if self.space_between_column < 3:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
652
                display_optional_mem = False
653
                if self.args.disable_mem:
654
                    mem_width = 0
655
                else:
656
                    mem_width = self.get_stats_display_width(stats_mem, without_option=True)
657
                stats_width = quicklook_width + 1 + cpu_width + mem_width + swap_width + load_width
658
                self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
659
            # No space again ? Remove optionnal CPU stats
660
            if self.space_between_column < 3:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
661
                display_optional_cpu = False
662
                if self.args.disable_cpu:
663
                    cpu_width = 0
664
                else:
665
                    cpu_width = self.get_stats_display_width(stats_cpu, without_option=True)
666
                stats_width = quicklook_width + 1 + cpu_width + mem_width + swap_width + load_width
667
                self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
668
        else:
669
            self.space_between_column = 0
670
671
        # Display CPU, MEM, SWAP and LOAD
672
        self.display_plugin(stats_cpu, display_optional=display_optional_cpu)
673
        self.new_column()
674
        self.display_plugin(stats_mem, display_optional=display_optional_mem)
675
        self.new_column()
676
        self.display_plugin(stats_memswap)
677
        self.new_column()
678
        self.display_plugin(stats_load)
679
680
        # Space between column
681
        self.space_between_column = 3
682
683
        # Backup line position
684
        self.saved_line = self.next_line
685
686
        # ==================================================================
687
        # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time)
688
        # ==================================================================
689
        self.init_column()
690
        if not (self.args.disable_network and
691
                self.args.disable_diskio and
692
                self.args.disable_fs and
693
                self.args.disable_folder and
694
                self.args.disable_raid and
695
                self.args.disable_sensors) and not self.args.disable_left_sidebar:
696
            self.new_line()
697
            self.display_plugin(stats_network)
698
            self.new_line()
699
            self.display_plugin(stats_diskio)
700
            self.new_line()
701
            self.display_plugin(stats_fs)
702
            self.new_line()
703
            self.display_plugin(stats_folders)
704
            self.new_line()
705
            self.display_plugin(stats_raid)
706
            self.new_line()
707
            self.display_plugin(stats_sensors)
708
            self.new_line()
709
            self.display_plugin(stats_now)
710
711
        # ====================================
712
        # Display right stats (process and co)
713
        # ====================================
714
        # If space available...
715
        if screen_x > 52:
716
            # Restore line position
717
            self.next_line = self.saved_line
718
719
            # Display right sidebar
720
            # ((DOCKER)+PROCESS_COUNT+(MONITORED)+PROCESS_LIST+ALERT)
721
            self.new_column()
722
            self.new_line()
723
            self.display_plugin(stats_docker)
724
            self.new_line()
725
            self.display_plugin(stats_processcount)
726
            if glances_processes.process_filter is None and cs_status is None:
727
                # Do not display stats monitor list if a filter exist
728
                self.new_line()
729
                self.display_plugin(stats_monitor)
730
            self.new_line()
731
            self.display_plugin(stats_processlist,
732
                                display_optional=(screen_x > 102),
733
                                display_additional=(not OSX),
734
                                max_y=(screen_y - self.get_stats_display_height(stats_alert) - 2))
735
            self.new_line()
736
            self.display_plugin(stats_alert)
737
738
        # History option
739
        # Generate history graph
740
        if self.history_tag and self.args.enable_history:
741
            self.display_popup(
742
                'Generate graphs history in {0}\nPlease wait...'.format(
743
                    self.glances_history.get_output_folder()))
744
            self.display_popup(
745
                'Generate graphs history in {0}\nDone: {1} graphs generated'.format(
746
                    self.glances_history.get_output_folder(),
747
                    self.glances_history.generate_graph(stats)))
748
        elif self.reset_history_tag and self.args.enable_history:
749
            self.display_popup('Reset history')
750
            self.glances_history.reset(stats)
751
        elif (self.history_tag or self.reset_history_tag) and not self.args.enable_history:
752
            try:
753
                self.glances_history.graph_enabled()
754
            except Exception:
755
                self.display_popup('History disabled\nEnable it using --enable-history')
756
            else:
757
                self.display_popup('History disabled\nPlease install matplotlib')
758
        self.history_tag = False
759
        self.reset_history_tag = False
760
761
        # Display edit filter popup
762
        # Only in standalone mode (cs_status is None)
763
        if self.edit_filter and cs_status is None:
764
            new_filter = self.display_popup(
765
                'Process filter pattern: ', is_input=True,
766
                input_value=glances_processes.process_filter)
767
            glances_processes.process_filter = new_filter
768
        elif self.edit_filter and cs_status != 'None':
769
            self.display_popup('Process filter only available in standalone mode')
770
        self.edit_filter = False
771
772
        return True
773
774
    def display_popup(self, message,
775
                      size_x=None, size_y=None,
776
                      duration=3,
777
                      is_input=False,
778
                      input_size=30,
779
                      input_value=None):
780
        """
781
        Display a centered popup.
782
783
        If is_input is False:
784
         Display a centered popup with the given message during duration seconds
785
         If size_x and size_y: set the popup size
786
         else set it automatically
787
         Return True if the popup could be displayed
788
789
        If is_input is True:
790
         Display a centered popup with the given message and a input field
791
         If size_x and size_y: set the popup size
792
         else set it automatically
793
         Return the input string or None if the field is empty
794
        """
795
        # Center the popup
796
        sentence_list = message.split('\n')
797
        if size_x is None:
798
            size_x = len(max(sentence_list, key=len)) + 4
799
            # Add space for the input field
800
            if is_input:
801
                size_x += input_size
802
        if size_y is None:
803
            size_y = len(sentence_list) + 4
804
        screen_x = self.screen.getmaxyx()[1]
805
        screen_y = self.screen.getmaxyx()[0]
806
        if size_x > screen_x or size_y > screen_y:
807
            # No size to display the popup => abord
808
            return False
809
        pos_x = int((screen_x - size_x) / 2)
810
        pos_y = int((screen_y - size_y) / 2)
811
812
        # Create the popup
813
        popup = curses.newwin(size_y, size_x, pos_y, pos_x)
814
815
        # Fill the popup
816
        popup.border()
817
818
        # Add the message
819
        for y, m in enumerate(message.split('\n')):
820
            popup.addnstr(2 + y, 2, m, len(m))
821
822
        if is_input and not WINDOWS:
823
            # Create a subwindow for the text field
824
            subpop = popup.derwin(1, input_size, 2, 2 + len(m))
825
            subpop.attron(self.colors_list['FILTER'])
826
            # Init the field with the current value
827
            if input_value is not None:
828
                subpop.addnstr(0, 0, input_value, len(input_value))
829
            # Display the popup
830
            popup.refresh()
831
            subpop.refresh()
832
            # Create the textbox inside the subwindows
833
            self.set_cursor(2)
834
            self.flash_cursor()
835
            textbox = GlancesTextbox(subpop, insert_mode=False)
836
            textbox.edit()
837
            self.set_cursor(0)
838
            self.no_flash_cursor()
839
            if textbox.gather() != '':
840
                logger.debug(
841
                    "User enters the following string: %s" % textbox.gather())
842
                return textbox.gather()[:-1]
843
            else:
844
                logger.debug("User centers an empty string")
845
                return None
846
        else:
847
            # Display the popup
848
            popup.refresh()
849
            curses.napms(duration * 1000)
850
            return True
851
852
    def display_plugin(self, plugin_stats,
853
                       display_optional=True,
854
                       display_additional=True,
855
                       max_y=65535):
856
        """Display the plugin_stats on the screen.
857
858
        If display_optional=True display the optional stats
859
        If display_additional=True display additionnal stats
860
        max_y do not display line > max_y
861
        """
862
        # Exit if:
863
        # - the plugin_stats message is empty
864
        # - the display tag = False
865
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
866
            # Exit
867
            return 0
868
869
        # Get the screen size
870
        screen_x = self.screen.getmaxyx()[1]
871
        screen_y = self.screen.getmaxyx()[0]
872
873
        # Set the upper/left position of the message
874
        if plugin_stats['align'] == 'right':
875
            # Right align (last column)
876
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
877
        else:
878
            display_x = self.column
879
        if plugin_stats['align'] == 'bottom':
880
            # Bottom (last line)
881
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
882
        else:
883
            display_y = self.line
884
885
        # Display
886
        x = display_x
887
        x_max = x
888
        y = display_y
889
        for m in plugin_stats['msgdict']:
890
            # New line
891
            if m['msg'].startswith('\n'):
892
                # Go to the next line
893
                y += 1
894
                # Return to the first column
895
                x = display_x
896
                continue
897
            # Do not display outside the screen
898
            if x < 0:
899
                continue
900
            if not m['splittable'] and (x + len(m['msg']) > screen_x):
901
                continue
902
            if y < 0 or (y + 1 > screen_y) or (y > max_y):
903
                break
904
            # If display_optional = False do not display optional stats
905
            if not display_optional and m['optional']:
906
                continue
907
            # If display_additional = False do not display additional stats
908
            if not display_additional and m['additional']:
909
                continue
910
            # Is it possible to display the stat with the current screen size
911
            # !!! Crach if not try/except... Why ???
912
            try:
913
                self.term_window.addnstr(y, x,
914
                                         m['msg'],
915
                                         # Do not disply outside the screen
916
                                         screen_x - x,
917
                                         self.colors_list[m['decoration']])
918
            except Exception:
919
                pass
920
            else:
921
                # New column
922
                # Python 2: we need to decode to get real screen size because
923
                # UTF-8 special tree chars occupy several bytes.
924
                # Python 3: strings are strings and bytes are bytes, all is
925
                # good.
926
                try:
927
                    x += len(u(m['msg']))
928
                except UnicodeDecodeError:
929
                    # Quick and dirty hack for issue #745
930
                    pass
931
                if x > x_max:
932
                    x_max = x
933
934
        # Compute the next Glances column/line position
935
        self.next_column = max(
936
            self.next_column, x_max + self.space_between_column)
937
        self.next_line = max(self.next_line, y + self.space_between_line)
938
939
    def erase(self):
940
        """Erase the content of the screen."""
941
        self.term_window.erase()
942
943
    def flush(self, stats, cs_status=None):
944
        """Clear and update the screen.
945
946
        stats: Stats database to display
947
        cs_status:
948
            "None": standalone or server mode
949
            "Connected": Client is connected to the server
950
            "Disconnected": Client is disconnected from the server
951
        """
952
        self.erase()
953
        self.display(stats, cs_status=cs_status)
954
955
    def update(self, stats, cs_status=None, return_to_browser=False):
956
        """Update the screen.
957
958
        Wait for __refresh_time sec / catch key every 100 ms.
959
960
        INPUT
961
        stats: Stats database to display
962
        cs_status:
963
            "None": standalone or server mode
964
            "Connected": Client is connected to the server
965
            "Disconnected": Client is disconnected from the server
966
        return_to_browser:
967
            True: Do not exist, return to the browser list
968
            False: Exit and return to the shell
969
970
        OUPUT
971
        True: Exit key has been pressed
972
        False: Others cases...
973
        """
974
        # Flush display
975
        self.flush(stats, cs_status=cs_status)
976
977
        # Wait
978
        exitkey = False
979
        countdown = Timer(self.__refresh_time)
980
        while not countdown.finished() and not exitkey:
981
            # Getkey
982
            pressedkey = self.__catch_key(return_to_browser=return_to_browser)
983
            # Is it an exit key ?
984
            exitkey = (pressedkey == ord('\x1b') or pressedkey == ord('q'))
985
            if not exitkey and pressedkey > -1:
986
                # Redraw display
987
                self.flush(stats, cs_status=cs_status)
988
            # Wait 100ms...
989
            curses.napms(100)
990
991
        return exitkey
992
993
    def get_stats_display_width(self, curse_msg, without_option=False):
994
        """Return the width of the formatted curses message.
995
996
        The height is defined by the maximum line.
997
        """
998
        try:
999
            if without_option:
1000
                # Size without options
1001
                c = len(max(''.join([(re.sub(r'[^\x00-\x7F]+', ' ', i['msg']) if not i['optional'] else "")
1002
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
1003
            else:
1004
                # Size with all options
1005
                c = len(max(''.join([re.sub(r'[^\x00-\x7F]+', ' ', i['msg'])
1006
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
1007
        except Exception:
1008
            return 0
1009
        else:
1010
            return c
1011
1012
    def get_stats_display_height(self, curse_msg):
1013
        r"""Return the height of the formatted curses message.
1014
1015
        The height is defined by the number of '\n' (new line).
1016
        """
1017
        try:
1018
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
1019
        except Exception:
1020
            return 0
1021
        else:
1022
            return c + 1
1023
1024
1025
class GlancesCursesStandalone(_GlancesCurses):
1026
1027
    """Class for the Glances curse standalone."""
1028
1029
    pass
1030
1031
1032
class GlancesCursesClient(_GlancesCurses):
1033
1034
    """Class for the Glances curse client."""
1035
1036
    pass
1037
1038
1039
class GlancesCursesBrowser(_GlancesCurses):
1040
1041
    """Class for the Glances curse client browser."""
1042
1043
    def __init__(self, args=None):
1044
        # Init the father class
1045
        super(GlancesCursesBrowser, self).__init__(args=args)
1046
1047
        _colors_list = {
1048
            'UNKNOWN': self.no_color,
1049
            'SNMP': self.default_color2,
1050
            'ONLINE': self.default_color2,
1051
            'OFFLINE': self.ifCRITICAL_color2,
1052
            'PROTECTED': self.ifWARNING_color2,
1053
        }
1054
        self.colors_list.update(_colors_list)
1055
1056
        # First time scan tag
1057
        # Used to display a specific message when the browser is started
1058
        self.first_scan = True
1059
1060
        # Init refresh time
1061
        self.__refresh_time = args.time
1062
1063
        # Init the cursor position for the client browser
1064
        self.cursor_position = 0
1065
1066
        # Active Glances server number
1067
        self._active_server = None
1068
1069
    @property
1070
    def active_server(self):
1071
        """Return the active server or None if it's the browser list."""
1072
        return self._active_server
1073
1074
    @active_server.setter
1075
    def active_server(self, index):
1076
        """Set the active server or None if no server selected."""
1077
        self._active_server = index
1078
1079
    @property
1080
    def cursor(self):
1081
        """Get the cursor position."""
1082
        return self.cursor_position
1083
1084
    @cursor.setter
1085
    def cursor(self, position):
1086
        """Set the cursor position."""
1087
        self.cursor_position = position
1088
1089
    def cursor_up(self, servers_list):
1090
        """Set the cursor to position N-1 in the list."""
1091
        if self.cursor_position > 0:
1092
            self.cursor_position -= 1
1093
        else:
1094
            self.cursor_position = len(servers_list) - 1
1095
1096
    def cursor_down(self, servers_list):
1097
        """Set the cursor to position N-1 in the list."""
1098
        if self.cursor_position < len(servers_list) - 1:
1099
            self.cursor_position += 1
1100
        else:
1101
            self.cursor_position = 0
1102
1103
    def __catch_key(self, servers_list):
1104
        # Catch the browser pressed key
1105
        self.pressedkey = self.get_key(self.term_window)
1106
1107
        # Actions...
1108
        if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
1109
            # 'ESC'|'q' > Quit
1110
            self.end()
1111
            logger.info("Stop Glances client browser")
1112
            sys.exit(0)
1113
        elif self.pressedkey == 10:
1114
            # 'ENTER' > Run Glances on the selected server
1115
            logger.debug("Server number {0} selected".format(self.cursor + 1))
1116
            self.active_server = self.cursor
1117
        elif self.pressedkey == 259:
1118
            # 'UP' > Up in the server list
1119
            self.cursor_up(servers_list)
1120
        elif self.pressedkey == 258:
1121
            # 'DOWN' > Down in the server list
1122
            self.cursor_down(servers_list)
1123
1124
        # Return the key code
1125
        return self.pressedkey
1126
1127
    def update(self, servers_list):
1128
        """Update the servers' list screen.
1129
1130
        Wait for __refresh_time sec / catch key every 100 ms.
1131
1132
        servers_list: Dict of dict with servers stats
1133
        """
1134
        # Flush display
1135
        logger.debug('Servers list: {0}'.format(servers_list))
1136
        self.flush(servers_list)
1137
1138
        # Wait
1139
        exitkey = False
1140
        countdown = Timer(self.__refresh_time)
1141
        while not countdown.finished() and not exitkey:
1142
            # Getkey
1143
            pressedkey = self.__catch_key(servers_list)
1144
            # Is it an exit or select server key ?
1145
            exitkey = (
1146
                pressedkey == ord('\x1b') or pressedkey == ord('q') or pressedkey == 10)
1147
            if not exitkey and pressedkey > -1:
1148
                # Redraw display
1149
                self.flush(servers_list)
1150
            # Wait 100ms...
1151
            curses.napms(100)
1152
1153
        return self.active_server
1154
1155
    def flush(self, servers_list):
1156
        """Update the servers' list screen.
1157
1158
        servers_list: List of dict with servers stats
1159
        """
1160
        self.erase()
1161
        self.display(servers_list)
1162
1163
    def display(self, servers_list):
1164
        """Display the servers list.
1165
1166
        Return:
1167
            True if the stats have been displayed
1168
            False if the stats have not been displayed (no server available)
1169
        """
1170
        # Init the internal line/column for Glances Curses
1171
        self.init_line_column()
1172
1173
        # Get the current screen size
1174
        screen_x = self.screen.getmaxyx()[1]
1175
        screen_y = self.screen.getmaxyx()[0]
1176
1177
        # Init position
1178
        x = 0
1179
        y = 0
1180
1181
        # Display top header
1182
        if len(servers_list) == 0:
1183
            if self.first_scan and not self.args.disable_autodiscover:
1184
                msg = 'Glances is scanning your network. Please wait...'
1185
                self.first_scan = False
1186
            else:
1187
                msg = 'No Glances server available'
1188
        elif len(servers_list) == 1:
1189
            msg = 'One Glances server available'
1190
        else:
1191
            msg = '{0} Glances servers available'.format(len(servers_list))
1192
        if self.args.disable_autodiscover:
1193
            msg += ' ' + '(auto discover is disabled)'
1194
        self.term_window.addnstr(y, x,
1195
                                 msg,
1196
                                 screen_x - x,
1197
                                 self.colors_list['TITLE'])
1198
1199
        if len(servers_list) == 0:
1200
            return False
1201
1202
        # Display the Glances server list
1203
        # ================================
1204
1205
        # Table of table
1206
        # Item description: [stats_id, column name, column size]
1207
        column_def = [
1208
            ['name', 'Name', 16],
1209
            ['alias', None, None],
1210
            ['load_min5', 'LOAD', 6],
1211
            ['cpu_percent', 'CPU%', 5],
1212
            ['mem_percent', 'MEM%', 5],
1213
            ['status', 'STATUS', 9],
1214
            ['ip', 'IP', 15],
1215
            # ['port', 'PORT', 5],
1216
            ['hr_name', 'OS', 16],
1217
        ]
1218
        y = 2
1219
1220
        # Display table header
1221
        xc = x + 2
1222
        for cpt, c in enumerate(column_def):
1223
            if xc < screen_x and y < screen_y and c[1] is not None:
1224
                self.term_window.addnstr(y, xc,
1225
                                         c[1],
1226
                                         screen_x - x,
1227
                                         self.colors_list['BOLD'])
1228
                xc += c[2] + self.space_between_column
1229
        y += 1
1230
1231
        # If a servers has been deleted from the list...
1232
        # ... and if the cursor is in the latest position
1233
        if self.cursor > len(servers_list) - 1:
1234
            # Set the cursor position to the latest item
1235
            self.cursor = len(servers_list) - 1
1236
1237
        # Display table
1238
        line = 0
1239
        for v in servers_list:
1240
            # Get server stats
1241
            server_stat = {}
1242
            for c in column_def:
1243
                try:
1244
                    server_stat[c[0]] = v[c[0]]
1245
                except KeyError as e:
1246
                    logger.debug(
1247
                        "Cannot grab stats {0} from server (KeyError: {1})".format(c[0], e))
1248
                    server_stat[c[0]] = '?'
1249
                # Display alias instead of name
1250
                try:
1251
                    if c[0] == 'alias' and v[c[0]] is not None:
1252
                        server_stat['name'] = v[c[0]]
1253
                except KeyError:
1254
                    pass
1255
1256
            # Display line for server stats
1257
            cpt = 0
1258
            xc = x
1259
1260
            # Is the line selected ?
1261
            if line == self.cursor:
1262
                # Display cursor
1263
                self.term_window.addnstr(
1264
                    y, xc, ">", screen_x - xc, self.colors_list['BOLD'])
1265
1266
            # Display the line
1267
            xc += 2
1268
            for c in column_def:
1269
                if xc < screen_x and y < screen_y and c[1] is not None:
1270
                    # Display server stats
1271
                    self.term_window.addnstr(
1272
                        y, xc, format(server_stat[c[0]]), c[2], self.colors_list[v['status']])
1273
                    xc += c[2] + self.space_between_column
1274
                cpt += 1
1275
            # Next line, next server...
1276
            y += 1
1277
            line += 1
1278
1279
        return True
1280
1281
if not WINDOWS:
1282
    class GlancesTextbox(Textbox, object):
1283
1284
        def __init__(self, *args, **kwargs):
1285
            super(GlancesTextbox, self).__init__(*args, **kwargs)
1286
1287
        def do_command(self, ch):
1288
            if ch == 10:  # Enter
1289
                return 0
1290
            if ch == 127:  # Back
1291
                return 8
1292
            return super(GlancesTextbox, self).do_command(ch)
1293