glances.outputs.glances_curses   F
last analyzed

Complexity

Total Complexity 242

Size/Duplication

Total Lines 1313
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 242
eloc 780
dl 0
loc 1313
rs 1.82
c 0
b 0
f 0

62 Methods

Rating   Name   Duplication   Size   Complexity  
A _GlancesCurses.new_column() 0 3 1
A _GlancesCurses._handle_clean_logs() 0 2 1
A _GlancesCurses._handle_sort_left() 0 3 1
A _GlancesCurses._handle_cursor_up() 0 3 2
A _GlancesCurses.enable_fullquicklook() 0 5 2
A _GlancesCurses.nice_increase() 0 2 1
A _GlancesCurses._handle_sort_key() 0 2 1
A _GlancesCurses.disable_top() 0 4 2
C _GlancesCurses.update() 0 57 11
A _GlancesCurses._handle_quicklook() 0 6 2
A _GlancesCurses.__init__() 0 66 4
A _GlancesCurses.end() 0 12 5
A _GlancesCurses.__get_stat_display() 0 38 5
A _GlancesCurses._handle_disable_process() 0 6 2
A _GlancesCurses._handle_quit() 0 5 2
A _GlancesCurses.flush() 0 11 1
B _GlancesCurses.get_stats_display_width() 0 31 5
F _GlancesCurses.display() 0 134 25
A _GlancesCurses.nice_decrease() 0 2 1
A _GlancesCurses.erase() 0 7 1
A GlancesTextboxYesNo.__init__() 0 2 1
A _GlancesCurses.new_line() 0 3 1
A _GlancesCurses._handle_erase_filter() 0 2 1
A _GlancesCurses.clear() 0 7 1
D _GlancesCurses.display_popup() 0 98 13
A _GlancesCurses.init_column() 0 4 1
A _GlancesCurses.get_stats_display_height() 0 12 3
B _GlancesCurses.__display_left() 0 11 6
A _GlancesCurses._handle_kill_process() 0 2 1
A _GlancesCurses._handle_cursor_down() 0 3 2
A _GlancesCurses._handle_clean_critical_logs() 0 2 1
A _GlancesCurses.init_line() 0 4 1
A _GlancesCurses.wait() 0 3 1
F _GlancesCurses.__display_top() 0 88 16
A _GlancesCurses.disable_fullquicklook() 0 4 2
A _GlancesCurses._handle_refresh() 0 2 1
A _GlancesCurses._init_cursor() 0 8 3
A GlancesTextbox.do_command() 0 6 3
A _GlancesCurses.init_line_column() 0 4 1
A _GlancesCurses._handle_process_extended() 0 7 2
B _GlancesCurses.__display_right() 0 31 8
A _GlancesCurses._handle_increase_nice() 0 2 1
A _GlancesCurses._init_history() 0 4 1
D _GlancesCurses._init_colors() 0 122 10
F _GlancesCurses.__catch_key() 0 37 25
A _GlancesCurses.get_key() 0 4 1
A _GlancesCurses.enable_top() 0 4 2
A _GlancesCurses.loop_position() 0 6 3
B _GlancesCurses.kill() 0 29 6
A _GlancesCurses._handle_decrease_nice() 0 2 1
A _GlancesCurses._handle_sort_right() 0 3 1
A _GlancesCurses.separator_line() 0 13 2
A GlancesTextboxYesNo.do_command() 0 2 1
B _GlancesCurses.__display_header() 0 27 5
A _GlancesCurses.load_config() 0 10 3
A GlancesTextbox.__init__() 0 2 1
A _GlancesCurses._handle_switch() 0 17 5
A _GlancesCurses.set_cursor() 0 12 3
A _GlancesCurses._handle_fs_stats() 0 3 1
F _GlancesCurses.display_plugin() 0 94 23
A _GlancesCurses._handle_top_menu() 0 6 2
A _GlancesCurses._handle_enter() 0 2 1

How to fix   Complexity   

Complexity

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