Test Failed
Push — master ( d0fde6...8e443d )
by Nicolas
03:51 queued 15s
created

glances.outputs.glances_curses   F

Complexity

Total Complexity 222

Size/Duplication

Total Lines 1262
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 732
dl 0
loc 1262
rs 1.868
c 0
b 0
f 0
wmc 222

42 Methods

Rating   Name   Duplication   Size   Complexity  
A _GlancesCurses.new_column() 0 3 1
A _GlancesCurses.enable_fullquicklook() 0 5 2
A _GlancesCurses.nice_increase() 0 2 1
A _GlancesCurses.disable_top() 0 4 2
C _GlancesCurses.update() 0 56 10
A _GlancesCurses.__init__() 0 58 2
A _GlancesCurses.end() 0 12 5
A _GlancesCurses.__get_stat_display() 0 33 5
A _GlancesCurses.flush() 0 11 1
B _GlancesCurses.get_stats_display_width() 0 31 5
F _GlancesCurses.display() 0 131 24
A _GlancesCurses.nice_decrease() 0 2 1
A _GlancesCurses.erase() 0 3 1
A GlancesTextboxYesNo.__init__() 0 2 1
A _GlancesCurses.new_line() 0 3 1
D _GlancesCurses.display_popup() 0 97 12
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.init_line() 0 4 1
F _GlancesCurses.__display_top() 0 88 16
A _GlancesCurses.disable_fullquicklook() 0 4 2
A _GlancesCurses.wait() 0 3 1
A _GlancesCurses._init_cursor() 0 8 3
A GlancesTextbox.do_command() 0 6 3
B _GlancesCurses.__display_right() 0 31 8
A _GlancesCurses.init_line_column() 0 4 1
A _GlancesCurses._init_history() 0 4 1
F _GlancesCurses._init_colors() 0 117 12
F _GlancesCurses.__catch_key() 0 124 40
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.separator_line() 0 13 2
B _GlancesCurses.__display_header() 0 27 5
A GlancesTextboxYesNo.do_command() 0 2 1
A _GlancesCurses.is_theme() 0 3 1
A GlancesTextbox.__init__() 0 2 1
A _GlancesCurses.load_config() 0 7 3
A _GlancesCurses.set_cursor() 0 12 3
F _GlancesCurses.display_plugin() 0 90 22

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