_GlancesCurses.display()   F
last analyzed

Complexity

Conditions 25

Size

Total Lines 134
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 25
eloc 67
nop 3
dl 0
loc 134
rs 0
c 0
b 0
f 0

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.glances_curses._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
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <[email protected]>
6
#
7
# SPDX-License-Identifier: LGPL-3.0-only
8
#
9
10
"""Curses interface class."""
11
from __future__ import unicode_literals
12
13
import sys
14
15
from glances.compat import nativestr, u, itervalues, enable, disable
16
from glances.globals import MACOS, WINDOWS
17
from glances.logger import logger
18
from glances.events import glances_events
19
from glances.processes import glances_processes, sort_processes_key_list
20
from glances.outputs.glances_unicode import unicode_message
21
from glances.timer import Timer
22
23
# Import curses library for "normal" operating system
24
try:
25
    import curses
26
    import curses.panel
27
    from curses.textpad import Textbox
28
except ImportError:
29
    logger.critical("Curses module not found. Glances cannot start in standalone mode.")
30
    if WINDOWS:
31
        logger.critical("For Windows you can try installing windows-curses with pip install.")
32
    sys.exit(1)
33
34
35
class _GlancesCurses(object):
36
37
    """This class manages the curses display (and key pressed).
38
39
    Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser.
40
    """
41
42
    _hotkeys = {
43
        # 'ENTER' > Edit the process filter
44
        '0': {'switch': 'disable_irix'},
45
        '1': {'switch': 'percpu'},
46
        '2': {'switch': 'disable_left_sidebar'},
47
        '3': {'switch': 'disable_quicklook'},
48
        # '4' > Enable or disable quicklook
49
        # '5' > Enable or disable top menu
50
        '6': {'switch': 'meangpu'},
51
        '9': {'switch': 'theme_white'},
52
        '/': {'switch': 'process_short_name'},
53
        'a': {'sort_key': 'auto'},
54
        'A': {'switch': 'disable_amps'},
55
        'b': {'switch': 'byte'},
56
        'B': {'switch': 'diskio_iops'},
57
        'c': {'sort_key': 'cpu_percent'},
58
        'C': {'switch': 'disable_cloud'},
59
        'd': {'switch': 'disable_diskio'},
60
        'D': {'switch': 'disable_containers'},
61
        # 'e' > Enable/Disable process extended
62
        # 'E' > Erase the process filter
63
        # 'f' > Show/hide fs / folder stats
64
        'F': {'switch': 'fs_free_space'},
65
        'g': {'switch': 'generate_graph'},
66
        'G': {'switch': 'disable_gpu'},
67
        'h': {'switch': 'help_tag'},
68
        'i': {'sort_key': 'io_counters'},
69
        'I': {'switch': 'disable_ip'},
70
        'j': {'switch': 'programs'},
71
        # 'k' > Kill selected process
72
        'K': {'switch': 'disable_connections'},
73
        'l': {'switch': 'disable_alert'},
74
        'm': {'sort_key': 'memory_percent'},
75
        'M': {'switch': 'reset_minmax_tag'},
76
        'n': {'switch': 'disable_network'},
77
        'N': {'switch': 'disable_now'},
78
        'p': {'sort_key': 'name'},
79
        'P': {'switch': 'disable_ports'},
80
        # 'q' or ESCAPE > Quit
81
        'Q': {'switch': 'enable_irq'},
82
        'r': {'switch': 'disable_smart'},
83
        'R': {'switch': 'disable_raid'},
84
        's': {'switch': 'disable_sensors'},
85
        'S': {'switch': 'sparkline'},
86
        't': {'sort_key': 'cpu_times'},
87
        'T': {'switch': 'network_sum'},
88
        'u': {'sort_key': 'username'},
89
        'U': {'switch': 'network_cumul'},
90
        # 'w' > Delete finished warning logs
91
        'W': {'switch': 'disable_wifi'},
92
        # 'x' > Delete finished warning and critical logs
93
        # 'z' > Enable or disable processes
94
        # '+' > Increase the process nice level
95
        # '-' > Decrease the process nice level
96
        # "<" (left arrow) navigation through process sort
97
        # ">" (right arrow) navigation through process sort
98
        # 'UP' > Up in the server list
99
        # 'DOWN' > Down in the server list
100
    }
101
102
    _sort_loop = sort_processes_key_list
103
104
    # Define top menu
105
    _top = ['quicklook', 'cpu', 'percpu', 'gpu', 'mem', 'memswap', 'load']
106
    _quicklook_max_width = 68
107
108
    # Define left sidebar
109
    _left_sidebar = [
110
        'network',
111
        'connections',
112
        'wifi',
113
        'ports',
114
        'diskio',
115
        'fs',
116
        'irq',
117
        'folders',
118
        'raid',
119
        'smart',
120
        'sensors',
121
        'now',
122
    ]
123
    _left_sidebar_min_width = 23
124
    _left_sidebar_max_width = 34
125
126
    # Define right sidebar
127
    _right_sidebar = ['containers', 'processcount', 'amps', 'processlist', 'alert']
128
129
    def __init__(self, config=None, args=None):
130
        # Init
131
        self.config = config
132
        self.args = args
133
134
        # Init windows positions
135
        self.term_w = 80
136
        self.term_h = 24
137
138
        # Space between stats
139
        self.space_between_column = 3
140
        self.space_between_line = 2
141
142
        # Init the curses screen
143
        self.screen = curses.initscr()
144
        if not self.screen:
145
            logger.critical("Cannot init the curses library.\n")
146
            sys.exit(1)
147
148
        # Load the 'outputs' section of the configuration file
149
        # - Init the theme (default is black)
150
        self.theme = {'name': 'black'}
151
152
        # Load configuration file
153
        self.load_config(config)
154
155
        # Init cursor
156
        self._init_cursor()
157
158
        # Init the colors
159
        self._init_colors()
160
161
        # Init main window
162
        self.term_window = self.screen.subwin(0, 0)
163
164
        # Init edit filter tag
165
        self.edit_filter = False
166
167
        # Init nice increase/decrease tag
168
        self.increase_nice_process = False
169
        self.decrease_nice_process = False
170
171
        # Init kill process tag
172
        self.kill_process = False
173
174
        # Init the process min/max reset
175
        self.args.reset_minmax_tag = False
176
177
        # Init cursor
178
        self.args.cursor_position = 0
179
180
        # Catch key pressed with non blocking mode
181
        self.term_window.keypad(1)
182
        self.term_window.nodelay(1)
183
        self.pressedkey = -1
184
185
        # History tag
186
        self._init_history()
187
188
    def load_config(self, config):
189
        """Load the outputs section of the configuration file."""
190
        # Load the theme
191
        if config is not None and config.has_section('outputs'):
192
            logger.debug('Read the outputs section in the configuration file')
193
            self.theme['name'] = config.get_value('outputs', 'curse_theme', default='black')
194
            logger.debug('Theme for the curse interface: {}'.format(self.theme['name']))
195
196
    def is_theme(self, name):
197
        """Return True if the theme *name* should be used."""
198
        return getattr(self.args, 'theme_' + name) or self.theme['name'] == name
199
200
    def _init_history(self):
201
        """Init the history option."""
202
203
        self.reset_history_tag = False
204
205
    def _init_cursor(self):
206
        """Init cursors."""
207
208
        if hasattr(curses, 'noecho'):
209
            curses.noecho()
210
        if hasattr(curses, 'cbreak'):
211
            curses.cbreak()
212
        self.set_cursor(0)
213
214
    def _init_colors(self):
215
        """Init the Curses color layout."""
216
217
        # Set curses options
218
        try:
219
            if hasattr(curses, 'start_color'):
220
                curses.start_color()
221
                logger.debug('Curses interface compatible with {} colors'.format(curses.COLORS))
222
            if hasattr(curses, 'use_default_colors'):
223
                curses.use_default_colors()
224
        except Exception as e:
225
            logger.warning('Error initializing terminal color ({})'.format(e))
226
227
        # Init colors
228
        if self.args.disable_bold:
229
            A_BOLD = 0
230
            self.args.disable_bg = True
231
        else:
232
            A_BOLD = curses.A_BOLD
233
234
        self.title_color = A_BOLD
235
        self.title_underline_color = A_BOLD | curses.A_UNDERLINE
236
        self.help_color = A_BOLD
237
238
        if curses.has_colors():
239
            # The screen is compatible with a colored design
240
            if self.is_theme('white'):
241
                # White theme: black ==> white
242
                curses.init_pair(1, curses.COLOR_BLACK, -1)
243
            else:
244
                curses.init_pair(1, curses.COLOR_WHITE, -1)
245
            if self.args.disable_bg:
246
                curses.init_pair(2, curses.COLOR_RED, -1)
247
                curses.init_pair(3, curses.COLOR_GREEN, -1)
248
                curses.init_pair(4, curses.COLOR_BLUE, -1)
249
                curses.init_pair(5, curses.COLOR_MAGENTA, -1)
250
            else:
251
                curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED)
252
                curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_GREEN)
253
                curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE)
254
                curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
255
            curses.init_pair(6, curses.COLOR_RED, -1)
256
            curses.init_pair(7, curses.COLOR_GREEN, -1)
257
            curses.init_pair(8, curses.COLOR_BLUE, -1)
258
259
            # Colors text styles
260
            self.no_color = curses.color_pair(1)
261
            self.default_color = curses.color_pair(3) | A_BOLD
262
            self.nice_color = curses.color_pair(5)
263
            self.cpu_time_color = curses.color_pair(5)
264
            self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD
265
            self.ifWARNING_color = curses.color_pair(5) | A_BOLD
266
            self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD
267
            self.default_color2 = curses.color_pair(7)
268
            self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
269
            self.ifWARNING_color2 = curses.color_pair(5) | A_BOLD
270
            self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
271
            self.ifINFO_color = curses.color_pair(8)
272
            self.filter_color = A_BOLD
273
            self.selected_color = A_BOLD
274
275
            if curses.COLOR_PAIRS > 8:
276
                colors_list = [curses.COLOR_MAGENTA, curses.COLOR_CYAN, curses.COLOR_YELLOW]
277
                for i in range(0, 3):
278
                    try:
279
                        curses.init_pair(i + 9, colors_list[i], -1)
280
                    except Exception:
281
                        if self.is_theme('white'):
282
                            curses.init_pair(i + 9, curses.COLOR_BLACK, -1)
283
                        else:
284
                            curses.init_pair(i + 9, curses.COLOR_WHITE, -1)
285
                self.nice_color = curses.color_pair(9)
286
                self.cpu_time_color = curses.color_pair(9)
287
                self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
288
                self.filter_color = curses.color_pair(10) | A_BOLD
289
                self.selected_color = curses.color_pair(11) | A_BOLD
290
291
        else:
292
            # The screen is NOT compatible with a colored design
293
            # switch to B&W text styles
294
            self.no_color = curses.A_NORMAL
295
            self.default_color = curses.A_NORMAL
296
            self.nice_color = A_BOLD
297
            self.cpu_time_color = A_BOLD
298
            self.ifCAREFUL_color = curses.A_UNDERLINE
299
            self.ifWARNING_color = A_BOLD
300
            self.ifCRITICAL_color = curses.A_REVERSE
301
            self.default_color2 = curses.A_NORMAL
302
            self.ifCAREFUL_color2 = curses.A_UNDERLINE
303
            self.ifWARNING_color2 = A_BOLD
304
            self.ifCRITICAL_color2 = curses.A_REVERSE
305
            self.ifINFO_color = A_BOLD
306
            self.filter_color = A_BOLD
307
            self.selected_color = A_BOLD
308
309
        # Define the colors list (hash table) for stats
310
        self.colors_list = {
311
            'DEFAULT': self.no_color,
312
            'UNDERLINE': curses.A_UNDERLINE,
313
            'BOLD': A_BOLD,
314
            'SORT': curses.A_UNDERLINE | A_BOLD,
315
            'OK': self.default_color2,
316
            'MAX': self.default_color2 | A_BOLD,
317
            'FILTER': self.filter_color,
318
            'TITLE': self.title_color,
319
            'PROCESS': self.default_color2,
320
            'PROCESS_SELECTED': self.default_color2 | curses.A_UNDERLINE,
321
            'STATUS': self.default_color2,
322
            'NICE': self.nice_color,
323
            'CPU_TIME': self.cpu_time_color,
324
            'CAREFUL': self.ifCAREFUL_color2,
325
            'WARNING': self.ifWARNING_color2,
326
            'CRITICAL': self.ifCRITICAL_color2,
327
            'OK_LOG': self.default_color,
328
            'CAREFUL_LOG': self.ifCAREFUL_color,
329
            'WARNING_LOG': self.ifWARNING_color,
330
            'CRITICAL_LOG': self.ifCRITICAL_color,
331
            'PASSWORD': curses.A_PROTECT,
332
            'SELECTED': self.selected_color,
333
            'INFO': self.ifINFO_color,
334
        }
335
336
    def set_cursor(self, value):
337
        """Configure the curse cursor appearance.
338
339
        0: invisible
340
        1: visible
341
        2: very visible
342
        """
343
        if hasattr(curses, 'curs_set'):
344
            try:
345
                curses.curs_set(value)
346
            except Exception:
347
                pass
348
349
    def get_key(self, window):
350
        # @TODO: Check issue #163
351
        ret = window.getch()
352
        return ret
353
354
    def __catch_key(self, return_to_browser=False):
355
        # Catch the pressed key
356
        self.pressedkey = self.get_key(self.term_window)
357
        if self.pressedkey == -1:
358
            return -1
359
360
        # Actions (available in the global hotkey dict)...
361
        logger.debug("Keypressed (code: {})".format(self.pressedkey))
362
        for hotkey in self._hotkeys:
363
            if self.pressedkey == ord(hotkey) and 'switch' in self._hotkeys[hotkey]:
364
                # Get the option name
365
                # Ex: disable_foo return foo
366
                #     enable_foo_bar return foo_bar
367
                option = '_'.join(self._hotkeys[hotkey]['switch'].split('_')[1:])
368
                if self._hotkeys[hotkey]['switch'].startswith('disable_'):
369
                    # disable_ switch
370
                    if getattr(self.args, self._hotkeys[hotkey]['switch']):
371
                        enable(self.args, option)
372
                    else:
373
                        disable(self.args, option)
374
                elif self._hotkeys[hotkey]['switch'].startswith('enable_'):
375
                    # enable_ switch
376
                    if getattr(self.args, self._hotkeys[hotkey]['switch']):
377
                        disable(self.args, option)
378
                    else:
379
                        enable(self.args, option)
380
                else:
381
                    # Others switchs options (with no enable_ or disable_)
382
                    setattr(
383
                        self.args,
384
                        self._hotkeys[hotkey]['switch'],
385
                        not getattr(self.args, self._hotkeys[hotkey]['switch']),
386
                    )
387
            if self.pressedkey == ord(hotkey) and 'sort_key' in self._hotkeys[hotkey]:
388
                glances_processes.set_sort_key(
389
                    self._hotkeys[hotkey]['sort_key'], self._hotkeys[hotkey]['sort_key'] == 'auto'
390
                )
391
392
        # Other actions...
393
        if self.pressedkey == ord('\n'):
394
            # 'ENTER' > Edit the process filter
395
            self.edit_filter = not self.edit_filter
396
        elif self.pressedkey == ord('4'):
397
            # '4' > Enable or disable quicklook
398
            self.args.full_quicklook = not self.args.full_quicklook
399
            if self.args.full_quicklook:
400
                self.enable_fullquicklook()
401
            else:
402
                self.disable_fullquicklook()
403
        elif self.pressedkey == ord('5'):
404
            # '5' > Enable or disable top menu
405
            self.args.disable_top = not self.args.disable_top
406
            if self.args.disable_top:
407
                self.disable_top()
408
            else:
409
                self.enable_top()
410
        elif self.pressedkey == ord('9'):
411
            # '9' > Theme from black to white and reverse
412
            self._init_colors()
413
        elif self.pressedkey == ord('e') and not self.args.programs:
414
            # 'e' > Enable/Disable process extended
415
            self.args.enable_process_extended = not self.args.enable_process_extended
416
            if not self.args.enable_process_extended:
417
                glances_processes.disable_extended()
418
            else:
419
                glances_processes.enable_extended()
420
            # When a process is selected (and only in standalone mode), disable the cursor
421
            self.args.disable_cursor = self.args.enable_process_extended and self.args.is_standalone
422
        elif self.pressedkey == ord('E'):
423
            # 'E' > Erase the process filter
424
            glances_processes.process_filter = None
425
        elif self.pressedkey == ord('f'):
426
            # 'f' > Show/hide fs / folder stats
427
            self.args.disable_fs = not self.args.disable_fs
428
            self.args.disable_folders = not self.args.disable_folders
429
        elif self.pressedkey == ord('+'):
430
            # '+' > Increase process nice level
431
            self.increase_nice_process = not self.increase_nice_process
432
        elif self.pressedkey == ord('-'):
433
            # '+' > Decrease process nice level
434
            self.decrease_nice_process = not self.decrease_nice_process
435
        elif self.pressedkey == ord('k') and not self.args.disable_cursor:
436
            # 'k' > Kill selected process (after confirmation)
437
            self.kill_process = not self.kill_process
438
        elif self.pressedkey == ord('w'):
439
            # 'w' > Delete finished warning logs
440
            glances_events.clean()
441
        elif self.pressedkey == ord('x'):
442
            # 'x' > Delete finished warning and critical logs
443
            glances_events.clean(critical=True)
444
        elif self.pressedkey == ord('z'):
445
            # 'z' > Enable or disable processes
446
            self.args.disable_process = not self.args.disable_process
447
            if self.args.disable_process:
448
                glances_processes.disable()
449
            else:
450
                glances_processes.enable()
451
        elif self.pressedkey == curses.KEY_LEFT:
452
            # "<" (left arrow) navigation through process sort
453
            next_sort = (self.loop_position() - 1) % len(self._sort_loop)
454
            glances_processes.set_sort_key(self._sort_loop[next_sort], False)
455
        elif self.pressedkey == curses.KEY_RIGHT:
456
            # ">" (right arrow) navigation through process sort
457
            next_sort = (self.loop_position() + 1) % len(self._sort_loop)
458
            glances_processes.set_sort_key(self._sort_loop[next_sort], False)
459
        elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65 and not self.args.disable_cursor:
460
            # 'UP' > Up in the server list
461
            if self.args.cursor_position > 0:
462
                self.args.cursor_position -= 1
463
        elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66 and not self.args.disable_cursor:
464
            # 'DOWN' > Down in the server list
465
            # if self.args.cursor_position < glances_processes.max_processes - 2:
466
            if self.args.cursor_position < glances_processes.processes_count:
467
                self.args.cursor_position += 1
468
        elif self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
469
            # 'ESC'|'q' > Quit
470
            if return_to_browser:
471
                logger.info("Stop Glances client and return to the browser")
472
            else:
473
                logger.info("Stop Glances (keypressed: {})".format(self.pressedkey))
474
        elif self.pressedkey == curses.KEY_F5:
475
            # "F5" manual refresh requested
476
            pass
477
478
        # Return the key code
479
        return self.pressedkey
480
481
    def loop_position(self):
482
        """Return the current sort in the loop"""
483
        for i, v in enumerate(self._sort_loop):
484
            if v == glances_processes.sort_key:
485
                return i
486
        return 0
487
488
    def disable_top(self):
489
        """Disable the top panel"""
490
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']:
491
            setattr(self.args, 'disable_' + p, True)
492
493
    def enable_top(self):
494
        """Enable the top panel"""
495
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']:
496
            setattr(self.args, 'disable_' + p, False)
497
498
    def disable_fullquicklook(self):
499
        """Disable the full quicklook mode"""
500
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap']:
501
            setattr(self.args, 'disable_' + p, False)
502
503
    def enable_fullquicklook(self):
504
        """Disable the full quicklook mode"""
505
        self.args.disable_quicklook = False
506
        for p in ['cpu', 'gpu', 'mem', 'memswap']:
507
            setattr(self.args, 'disable_' + p, True)
508
509
    def end(self):
510
        """Shutdown the curses window."""
511
        if hasattr(curses, 'echo'):
512
            curses.echo()
513
        if hasattr(curses, 'nocbreak'):
514
            curses.nocbreak()
515
        if hasattr(curses, 'curs_set'):
516
            try:
517
                curses.curs_set(1)
518
            except Exception:
519
                pass
520
        curses.endwin()
521
522
    def init_line_column(self):
523
        """Init the line and column position for the curses interface."""
524
        self.init_line()
525
        self.init_column()
526
527
    def init_line(self):
528
        """Init the line position for the curses interface."""
529
        self.line = 0
530
        self.next_line = 0
531
532
    def init_column(self):
533
        """Init the column position for the curses interface."""
534
        self.column = 0
535
        self.next_column = 0
536
537
    def new_line(self, separator=False):
538
        """New line in the curses interface."""
539
        self.line = self.next_line
540
541
    def new_column(self):
542
        """New column in the curses interface."""
543
        self.column = self.next_column
544
545
    def separator_line(self, color='TITLE'):
546
        """New separator line in the curses interface."""
547
        if not self.args.enable_separator:
548
            return
549
        self.new_line()
550
        self.line -= 1
551
        line_width = self.term_window.getmaxyx()[1] - self.column
552
        self.term_window.addnstr(
553
            self.line,
554
            self.column,
555
            unicode_message('MEDIUM_LINE', self.args) * line_width,
556
            line_width,
557
            self.colors_list[color],
558
        )
559
560
    def __get_stat_display(self, stats, layer):
561
        """Return a dict of dict with all the stats display.
562
        # TODO: Drop extra parameter
563
564
        :param stats: Global stats dict
565
        :param layer: ~ cs_status
566
            "None": standalone or server mode
567
            "Connected": Client is connected to a Glances server
568
            "SNMP": Client is connected to a SNMP server
569
            "Disconnected": Client is disconnected from the server
570
571
        :returns: dict of dict
572
            * key: plugin name
573
            * value: dict returned by the get_stats_display Plugin method
574
        """
575
        ret = {}
576
577
        for p in stats.getPluginsList(enable=False):
578
            if p == 'quicklook' or p == 'processlist':
579
                # processlist is done later
580
                # because we need to know how many processes could be displayed
581
                continue
582
583
            # Compute the plugin max size
584
            plugin_max_width = None
585
            if p in self._left_sidebar:
586
                plugin_max_width = max(self._left_sidebar_min_width, self.term_window.getmaxyx()[1] - 105)
587
                plugin_max_width = min(self._left_sidebar_max_width, plugin_max_width)
588
589
            # Get the view
590
            ret[p] = stats.get_plugin(p).get_stats_display(args=self.args, max_width=plugin_max_width)
591
592
        return ret
593
594
    def display(self, stats, cs_status=None):
595
        """Display stats on the screen.
596
597
        :param stats: Stats database to display
598
        :param cs_status:
599
            "None": standalone or server mode
600
            "Connected": Client is connected to a Glances server
601
            "SNMP": Client is connected to a SNMP server
602
            "Disconnected": Client is disconnected from the server
603
604
        :return: True if the stats have been displayed else False if the help have been displayed
605
        """
606
        # Init the internal line/column for Glances Curses
607
        self.init_line_column()
608
609
        # Update the stats messages
610
        ###########################
611
612
        # Get all the plugins but quicklook and process list
613
        self.args.cs_status = cs_status
614
        __stat_display = self.__get_stat_display(stats, layer=cs_status)
615
616
        # Adapt number of processes to the available space
617
        max_processes_displayed = (
618
            self.term_window.getmaxyx()[0]
619
            - 11
620
            - (0 if 'containers' not in __stat_display else self.get_stats_display_height(__stat_display["containers"]))
621
            - (
622
                0
623
                if 'processcount' not in __stat_display
624
                else self.get_stats_display_height(__stat_display["processcount"])
625
            )
626
            - (0 if 'amps' not in __stat_display else self.get_stats_display_height(__stat_display["amps"]))
627
            - (0 if 'alert' not in __stat_display else self.get_stats_display_height(__stat_display["alert"]))
628
        )
629
630
        try:
631
            if self.args.enable_process_extended:
632
                max_processes_displayed -= 4
633
        except AttributeError:
634
            pass
635
        if max_processes_displayed < 0:
636
            max_processes_displayed = 0
637
        if glances_processes.max_processes is None or glances_processes.max_processes != max_processes_displayed:
638
            logger.debug("Set number of displayed processes to {}".format(max_processes_displayed))
639
            glances_processes.max_processes = max_processes_displayed
640
641
        # Get the processlist
642
        __stat_display["processlist"] = stats.get_plugin('processlist').get_stats_display(args=self.args)
643
644
        # Display the stats on the curses interface
645
        ###########################################
646
647
        # Help screen (on top of the other stats)
648
        if self.args.help_tag:
649
            # Display the stats...
650
            self.display_plugin(stats.get_plugin('help').get_stats_display(args=self.args))
651
            # ... and exit
652
            return False
653
654
        # =====================================
655
        # Display first line (system+ip+uptime)
656
        # Optionally: Cloud on second line
657
        # =====================================
658
        self.__display_header(__stat_display)
659
        self.separator_line()
660
661
        # ==============================================================
662
        # Display second line (<SUMMARY>+CPU|PERCPU+<GPU>+LOAD+MEM+SWAP)
663
        # ==============================================================
664
        self.__display_top(__stat_display, stats)
665
        self.init_column()
666
        self.separator_line()
667
668
        # ==================================================================
669
        # Display left sidebar (NETWORK+PORTS+DISKIO+FS+SENSORS+Current time)
670
        # ==================================================================
671
        self.__display_left(__stat_display)
672
673
        # ====================================
674
        # Display right stats (process and co)
675
        # ====================================
676
        self.__display_right(__stat_display)
677
678
        # =====================
679
        # Others popup messages
680
        # =====================
681
682
        # Display edit filter popup
683
        # Only in standalone mode (cs_status is None)
684
        if self.edit_filter and cs_status is None:
685
            new_filter = self.display_popup(
686
                'Process filter pattern: \n\n'
687
                + 'Examples:\n'
688
                + '- .*python.*\n'
689
                + '- /usr/lib.*\n'
690
                + '- name:.*nautilus.*\n'
691
                + '- cmdline:.*glances.*\n'
692
                + '- username:nicolargo\n'
693
                + '- username:^root        ',
694
                popup_type='input',
695
                input_value=glances_processes.process_filter_input,
696
            )
697
            glances_processes.process_filter = new_filter
698
        elif self.edit_filter and cs_status is not None:
699
            self.display_popup('Process filter only available in standalone mode')
700
        self.edit_filter = False
701
702
        # Manage increase/decrease nice level of the selected process
703
        # Only in standalone mode (cs_status is None)
704
        if self.increase_nice_process and cs_status is None:
705
            self.nice_increase(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
706
        self.increase_nice_process = False
707
        if self.decrease_nice_process and cs_status is None:
708
            self.nice_decrease(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
709
        self.decrease_nice_process = False
710
711
        # Display kill process confirmation popup
712
        # Only in standalone mode (cs_status is None)
713
        if self.kill_process and cs_status is None:
714
            self.kill(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
715
        elif self.kill_process and cs_status is not None:
716
            self.display_popup('Kill process only available for local processes')
717
        self.kill_process = False
718
719
        # Display graph generation popup
720
        if self.args.generate_graph:
721
            if 'graph' in stats.getExportsList():
722
                self.display_popup('Generate graph in {}'.format(self.args.export_graph_path))
723
            else:
724
                logger.warning('Graph export module is disable. Run Glances with --export graph to enable it.')
725
                self.args.generate_graph = False
726
727
        return True
728
729
    def nice_increase(self, process):
730
        glances_processes.nice_increase(process['pid'])
731
732
    def nice_decrease(self, process):
733
        glances_processes.nice_decrease(process['pid'])
734
735
    def kill(self, process):
736
        """Kill a process, or a list of process if the process has a childrens field.
737
738
        :param process
739
        :return: None
740
        """
741
        logger.debug("Selected process to kill: {}".format(process))
742
743
        if 'childrens' in process:
744
            pid_to_kill = process['childrens']
745
        else:
746
            pid_to_kill = [process['pid']]
747
748
        confirm = self.display_popup(
749
            'Kill process: {} (pid: {}) ?\n\nConfirm ([y]es/[n]o): '.format(
750
                process['name'],
751
                ', '.join(map(str, pid_to_kill)),
752
            ),
753
            popup_type='yesno',
754
        )
755
756
        if confirm.lower().startswith('y'):
757
            for pid in pid_to_kill:
758
                try:
759
                    ret_kill = glances_processes.kill(pid)
760
                except Exception as e:
761
                    logger.error('Can not kill process {} ({})'.format(pid, e))
762
                else:
763
                    logger.info('Kill signal has been sent to process {} (return code: {})'.format(pid, ret_kill))
764
765
    def __display_header(self, stat_display):
766
        """Display the firsts lines (header) in the Curses interface.
767
768
        system + ip + uptime
769
        (cloud)
770
        """
771
        # First line
772
        self.new_line()
773
        self.space_between_column = 0
774
        l_uptime = 1
775
        for i in ['system', 'ip', 'uptime']:
776
            if i in stat_display:
777
                l_uptime += self.get_stats_display_width(stat_display[i])
778
        self.display_plugin(stat_display["system"], display_optional=(self.term_window.getmaxyx()[1] >= l_uptime))
779
        self.space_between_column = 3
780
        if 'ip' in stat_display:
781
            self.new_column()
782
            self.display_plugin(stat_display["ip"], display_optional=(self.term_window.getmaxyx()[1] >= 100))
783
        self.new_column()
784
        self.display_plugin(
785
            stat_display["uptime"], add_space=-(self.get_stats_display_width(stat_display["cloud"]) != 0)
786
        )
787
        self.init_column()
788
        if self.get_stats_display_width(stat_display["cloud"]) != 0:
789
            # Second line (optional)
790
            self.new_line()
791
            self.display_plugin(stat_display["cloud"])
792
793
    def __display_top(self, stat_display, stats):
794
        """Display the second line in the Curses interface.
795
796
        <QUICKLOOK> + CPU|PERCPU + <GPU> + MEM + SWAP + LOAD
797
        """
798
        self.init_column()
799
        self.new_line()
800
801
        # Init quicklook
802
        stat_display['quicklook'] = {'msgdict': []}
803
804
        # Dict for plugins width
805
        plugin_widths = {}
806
        for p in self._top:
807
            plugin_widths[p] = (
808
                self.get_stats_display_width(stat_display.get(p, 0)) if hasattr(self.args, 'disable_' + p) else 0
809
            )
810
811
        # Width of all plugins
812
        stats_width = sum(itervalues(plugin_widths))
813
814
        # Number of plugin but quicklook
815
        stats_number = sum(
816
            [int(stat_display[p]['msgdict'] != []) for p in self._top if not getattr(self.args, 'disable_' + p)]
817
        )
818
819
        if not self.args.disable_quicklook:
820
            # Quick look is in the place !
821
            if self.args.full_quicklook:
822
                quicklook_width = self.term_window.getmaxyx()[1] - (
823
                    stats_width + 8 + stats_number * self.space_between_column
824
                )
825
            else:
826
                quicklook_width = min(
827
                    self.term_window.getmaxyx()[1] - (stats_width + 8 + stats_number * self.space_between_column),
828
                    self._quicklook_max_width - 5,
829
                )
830
            try:
831
                stat_display["quicklook"] = stats.get_plugin('quicklook').get_stats_display(
832
                    max_width=quicklook_width, args=self.args
833
                )
834
            except AttributeError as e:
835
                logger.debug("Quicklook plugin not available (%s)" % e)
836
            else:
837
                plugin_widths['quicklook'] = self.get_stats_display_width(stat_display["quicklook"])
838
                stats_width = sum(itervalues(plugin_widths)) + 1
839
            self.space_between_column = 1
840
            self.display_plugin(stat_display["quicklook"])
841
            self.new_column()
842
843
        # Compute spaces between plugins
844
        # Note: Only one space between Quicklook and others
845
        plugin_display_optional = {}
846
        for p in self._top:
847
            plugin_display_optional[p] = True
848
        if stats_number > 1:
849
            self.space_between_column = max(1, int((self.term_window.getmaxyx()[1] - stats_width) / (stats_number - 1)))
850
            for p in ['mem', 'cpu']:
851
                # No space ? Remove optional stats
852
                if self.space_between_column < 3:
853
                    plugin_display_optional[p] = False
854
                    plugin_widths[p] = (
855
                        self.get_stats_display_width(stat_display[p], without_option=True)
856
                        if hasattr(self.args, 'disable_' + p)
857
                        else 0
858
                    )
859
                    stats_width = sum(itervalues(plugin_widths)) + 1
860
                    self.space_between_column = max(
861
                        1, int((self.term_window.getmaxyx()[1] - stats_width) / (stats_number - 1))
862
                    )
863
        else:
864
            self.space_between_column = 0
865
866
        # Display CPU, MEM, SWAP and LOAD
867
        for p in self._top:
868
            if p == 'quicklook':
869
                continue
870
            if p in stat_display:
871
                self.display_plugin(stat_display[p], display_optional=plugin_display_optional[p])
872
            if p != 'load':
873
                # Skip last column
874
                self.new_column()
875
876
        # Space between column
877
        self.space_between_column = 3
878
879
        # Backup line position
880
        self.saved_line = self.next_line
881
882
    def __display_left(self, stat_display):
883
        """Display the left sidebar in the Curses interface."""
884
        self.init_column()
885
886
        if self.args.disable_left_sidebar:
887
            return
888
889
        for p in self._left_sidebar:
890
            if (hasattr(self.args, 'enable_' + p) or hasattr(self.args, 'disable_' + p)) and p in stat_display:
891
                self.new_line()
892
                self.display_plugin(stat_display[p])
893
894
    def __display_right(self, stat_display):
895
        """Display the right sidebar in the Curses interface.
896
897
        docker + processcount + amps + processlist + alert
898
        """
899
        # Do not display anything if space is not available...
900
        if self.term_window.getmaxyx()[1] < self._left_sidebar_min_width:
901
            return
902
903
        # Restore line position
904
        self.next_line = self.saved_line
905
906
        # Display right sidebar
907
        self.new_column()
908
        for p in self._right_sidebar:
909
            if (hasattr(self.args, 'enable_' + p) or hasattr(self.args, 'disable_' + p)) and p in stat_display:
910
                if p not in p:
911
                    # Catch for issue #1470
912
                    continue
913
                self.new_line()
914
                if p == 'processlist':
915
                    self.display_plugin(
916
                        stat_display['processlist'],
917
                        display_optional=(self.term_window.getmaxyx()[1] > 102),
918
                        display_additional=(not MACOS),
919
                        max_y=(
920
                            self.term_window.getmaxyx()[0] - self.get_stats_display_height(stat_display['alert']) - 2
921
                        ),
922
                    )
923
                else:
924
                    self.display_plugin(stat_display[p])
925
926
    def display_popup(
927
        self, message, size_x=None, size_y=None, duration=3, popup_type='info', input_size=30, input_value=None
928
    ):
929
        """
930
        Display a centered popup.
931
932
         popup_type: ='info'
933
         Just an information popup, no user interaction
934
         Display a centered popup with the given message during duration seconds
935
         If size_x and size_y: set the popup size
936
         else set it automatically
937
         Return True if the popup could be displayed
938
939
        popup_type='input'
940
         Display a centered popup with the given message and a input field
941
         If size_x and size_y: set the popup size
942
         else set it automatically
943
         Return the input string or None if the field is empty
944
945
        popup_type='yesno'
946
         Display a centered popup with the given message
947
         If size_x and size_y: set the popup size
948
         else set it automatically
949
         Return True (yes) or False (no)
950
        """
951
        # Center the popup
952
        sentence_list = message.split('\n')
953
        if size_x is None:
954
            size_x = len(max(sentence_list, key=len)) + 4
955
            # Add space for the input field
956
            if popup_type == 'input':
957
                size_x += input_size
958
        if size_y is None:
959
            size_y = len(sentence_list) + 4
960
        screen_x = self.term_window.getmaxyx()[1]
961
        screen_y = self.term_window.getmaxyx()[0]
962
        if size_x > screen_x or size_y > screen_y:
963
            # No size to display the popup => abord
964
            return False
965
        pos_x = int((screen_x - size_x) / 2)
966
        pos_y = int((screen_y - size_y) / 2)
967
968
        # Create the popup
969
        popup = curses.newwin(size_y, size_x, pos_y, pos_x)
970
971
        # Fill the popup
972
        popup.border()
973
974
        # Add the message
975
        for y, m in enumerate(sentence_list):
976
            popup.addnstr(2 + y, 2, m, len(m))
977
978
        if popup_type == 'info':
979
            # Display the popup
980
            popup.refresh()
981
            self.wait(duration * 1000)
982
            return True
983
        elif popup_type == 'input':
984
            # Create a sub-window for the text field
985
            sub_pop = popup.derwin(1, input_size, 2, 2 + len(m))
0 ignored issues
show
introduced by
The variable m does not seem to be defined in case the for loop on line 975 is not entered. Are you sure this can never be the case?
Loading history...
986
            sub_pop.attron(self.colors_list['FILTER'])
987
            # Init the field with the current value
988
            if input_value is not None:
989
                sub_pop.addnstr(0, 0, input_value, len(input_value))
990
            # Display the popup
991
            popup.refresh()
992
            sub_pop.refresh()
993
            # Create the textbox inside the sub-windows
994
            self.set_cursor(2)
995
            self.term_window.keypad(1)
996
            textbox = GlancesTextbox(sub_pop, insert_mode=True)
997
            textbox.edit()
998
            self.set_cursor(0)
999
            # self.term_window.keypad(0)
1000
            if textbox.gather() != '':
1001
                logger.debug("User enters the following string: %s" % textbox.gather())
1002
                return textbox.gather()[:-1]
1003
            else:
1004
                logger.debug("User centers an empty string")
1005
                return None
1006
        elif popup_type == 'yesno':
1007
            # # Create a sub-window for the text field
1008
            sub_pop = popup.derwin(1, 2, len(sentence_list) + 1, len(m) + 2)
1009
            sub_pop.attron(self.colors_list['FILTER'])
1010
            # Init the field with the current value
1011
            sub_pop.addnstr(0, 0, '', 0)
1012
            # Display the popup
1013
            popup.refresh()
1014
            sub_pop.refresh()
1015
            # Create the textbox inside the sub-windows
1016
            self.set_cursor(2)
1017
            self.term_window.keypad(1)
1018
            textbox = GlancesTextboxYesNo(sub_pop, insert_mode=False)
1019
            textbox.edit()
1020
            self.set_cursor(0)
1021
            # self.term_window.keypad(0)
1022
            return textbox.gather()
1023
1024
    def display_plugin(self, plugin_stats, display_optional=True, display_additional=True, max_y=65535, add_space=0):
1025
        """Display the plugin_stats on the screen.
1026
1027
        :param plugin_stats:
1028
        :param display_optional: display the optional stats if True
1029
        :param display_additional: display additional stats if True
1030
        :param max_y: do not display line > max_y
1031
        :param add_space: add x space (line) after the plugin
1032
        """
1033
        # Exit if:
1034
        # - the plugin_stats message is empty
1035
        # - the display tag = False
1036
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
1037
            # Exit
1038
            return 0
1039
1040
        # Get the screen size
1041
        screen_x = self.term_window.getmaxyx()[1]
1042
        screen_y = self.term_window.getmaxyx()[0]
1043
1044
        # Set the upper/left position of the message
1045
        if plugin_stats['align'] == 'right':
1046
            # Right align (last column)
1047
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
1048
        else:
1049
            display_x = self.column
1050
        if plugin_stats['align'] == 'bottom':
1051
            # Bottom (last line)
1052
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
1053
        else:
1054
            display_y = self.line
1055
1056
        # Display
1057
        x = display_x
1058
        x_max = x
1059
        y = display_y
1060
        for m in plugin_stats['msgdict']:
1061
            # New line
1062
            try:
1063
                if m['msg'].startswith('\n'):
1064
                    # Go to the next line
1065
                    y += 1
1066
                    # Return to the first column
1067
                    x = display_x
1068
                    continue
1069
            except Exception:
1070
                # Avoid exception (see issue #1692)
1071
                pass
1072
            # Do not display outside the screen
1073
            if x < 0:
1074
                continue
1075
            if not m['splittable'] and (x + len(m['msg']) > screen_x):
1076
                continue
1077
            if y < 0 or (y + 1 > screen_y) or (y > max_y):
1078
                break
1079
            # If display_optional = False do not display optional stats
1080
            if not display_optional and m['optional']:
1081
                continue
1082
            # If display_additional = False do not display additional stats
1083
            if not display_additional and m['additional']:
1084
                continue
1085
            # Is it possible to display the stat with the current screen size
1086
            # !!! Crash if not try/except... Why ???
1087
            try:
1088
                self.term_window.addnstr(
1089
                    y,
1090
                    x,
1091
                    m['msg'],
1092
                    # Do not display outside the screen
1093
                    screen_x - x,
1094
                    self.colors_list[m['decoration']],
1095
                )
1096
            except Exception:
1097
                pass
1098
            else:
1099
                # New column
1100
                # Python 2: we need to decode to get real screen size because
1101
                # UTF-8 special tree chars occupy several bytes.
1102
                # Python 3: strings are strings and bytes are bytes, all is
1103
                # good.
1104
                try:
1105
                    x += len(u(m['msg']))
1106
                except UnicodeDecodeError:
1107
                    # Quick and dirty hack for issue #745
1108
                    pass
1109
                if x > x_max:
1110
                    x_max = x
1111
1112
        # Compute the next Glances column/line position
1113
        self.next_column = max(self.next_column, x_max + self.space_between_column)
1114
        self.next_line = max(self.next_line, y + self.space_between_line)
1115
1116
        # Have empty lines after the plugins
1117
        self.next_line += add_space
1118
1119
    def erase(self):
1120
        """Erase the content of the screen."""
1121
        self.term_window.clear()
1122
1123
    def flush(self, stats, cs_status=None):
1124
        """Clear and update the screen.
1125
1126
        :param stats: Stats database to display
1127
        :param cs_status:
1128
            "None": standalone or server mode
1129
            "Connected": Client is connected to the server
1130
            "Disconnected": Client is disconnected from the server
1131
        """
1132
        self.erase()
1133
        self.display(stats, cs_status=cs_status)
1134
1135
    def update(self, stats, duration=3, cs_status=None, return_to_browser=False):
1136
        """Update the screen.
1137
1138
        :param stats: Stats database to display
1139
        :param duration: duration of the loop
1140
        :param cs_status:
1141
            "None": standalone or server mode
1142
            "Connected": Client is connected to the server
1143
            "Disconnected": Client is disconnected from the server
1144
        :param return_to_browser:
1145
            True: Do not exist, return to the browser list
1146
            False: Exit and return to the shell
1147
1148
        :return: True if exit key has been pressed else False
1149
        """
1150
        # Flush display
1151
        self.flush(stats, cs_status=cs_status)
1152
1153
        # If the duration is < 0 (update + export time > refresh_time)
1154
        # Then display the interface and log a message
1155
        if duration <= 0:
1156
            logger.warning('Update and export time higher than refresh_time.')
1157
            duration = 0.1
1158
1159
        # Wait duration (in s) time
1160
        isexitkey = False
1161
        countdown = Timer(duration)
1162
        # Set the default timeout (in ms) between two getch
1163
        self.term_window.timeout(100)
1164
        while not countdown.finished() and not isexitkey:
1165
            # Getkey
1166
            pressedkey = self.__catch_key(return_to_browser=return_to_browser)
1167
            isexitkey = pressedkey == ord('\x1b') or pressedkey == ord('q')
1168
1169
            if pressedkey == curses.KEY_F5:
1170
                # Were asked to refresh
1171
                return isexitkey
1172
1173
            if pressedkey in (curses.KEY_UP, 65, curses.KEY_DOWN, 66):
1174
                # Up of won key pressed, reset the countdown
1175
                # Better for user experience
1176
                countdown.reset()
1177
1178
            if isexitkey and self.args.help_tag:
1179
                # Quit from help should return to main screen, not exit #1874
1180
                self.args.help_tag = not self.args.help_tag
1181
                isexitkey = False
1182
                return isexitkey
1183
1184
            if not isexitkey and pressedkey > -1:
1185
                # Redraw display
1186
                self.flush(stats, cs_status=cs_status)
1187
                # Overwrite the timeout with the countdown
1188
                self.wait(delay=int(countdown.get() * 1000))
1189
1190
        return isexitkey
1191
1192
    def wait(self, delay=100):
1193
        """Wait delay in ms"""
1194
        curses.napms(100)
1195
1196
    def get_stats_display_width(self, curse_msg, without_option=False):
1197
        """Return the width of the formatted curses message."""
1198
        try:
1199
            if without_option:
1200
                # Size without options
1201
                c = len(
1202
                    max(
1203
                        ''.join(
1204
                            [
1205
                                (u(u(nativestr(i['msg'])).encode('ascii', 'replace')) if not i['optional'] else "")
1206
                                for i in curse_msg['msgdict']
1207
                            ]
1208
                        ).split('\n'),
1209
                        key=len,
1210
                    )
1211
                )
1212
            else:
1213
                # Size with all options
1214
                c = len(
1215
                    max(
1216
                        ''.join(
1217
                            [u(u(nativestr(i['msg'])).encode('ascii', 'replace')) for i in curse_msg['msgdict']]
1218
                        ).split('\n'),
1219
                        key=len,
1220
                    )
1221
                )
1222
        except Exception as e:
1223
            logger.debug('ERROR: Can not compute plugin width ({})'.format(e))
1224
            return 0
1225
        else:
1226
            return c
1227
1228
    def get_stats_display_height(self, curse_msg):
1229
        """Return the height of the formatted curses message.
1230
1231
        The height is defined by the number of '\n' (new line).
1232
        """
1233
        try:
1234
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
1235
        except Exception as e:
1236
            logger.debug('ERROR: Can not compute plugin height ({})'.format(e))
1237
            return 0
1238
        else:
1239
            return c + 1
1240
1241
1242
class GlancesCursesStandalone(_GlancesCurses):
1243
1244
    """Class for the Glances curse standalone."""
1245
1246
1247
class GlancesCursesClient(_GlancesCurses):
1248
1249
    """Class for the Glances curse client."""
1250
1251
1252
class GlancesTextbox(Textbox, object):
1253
    def __init__(self, *args, **kwargs):
1254
        super(GlancesTextbox, self).__init__(*args, **kwargs)
1255
1256
    def do_command(self, ch):
1257
        if ch == 10:  # Enter
1258
            return 0
1259
        if ch == 127:  # Back
1260
            return 8
1261
        return super(GlancesTextbox, self).do_command(ch)
1262
1263
1264
class GlancesTextboxYesNo(Textbox, object):
1265
    def __init__(self, *args, **kwargs):
1266
        super(GlancesTextboxYesNo, self).__init__(*args, **kwargs)
1267
1268
    def do_command(self, ch):
1269
        return super(GlancesTextboxYesNo, self).do_command(ch)
1270