Test Failed
Push — master ( 7e7379...128504 )
by Nicolas
03:31
created

_GlancesCurses.display()   F

Complexity

Conditions 20

Size

Total Lines 123
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 60
nop 3
dl 0
loc 123
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

Complexity

Complex classes like glances.outputs.glances_curses._GlancesCurses.display() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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