Test Failed
Push — master ( ee826a...d9056e )
by Nicolas
03:09
created

_GlancesCurses.display_popup()   F

Complexity

Conditions 15

Size

Total Lines 113
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 67
nop 9
dl 0
loc 113
rs 2.9998
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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_popup() 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.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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