Test Failed
Push — master ( 69b639...e7fa0a )
by Nicolas
04:05 queued 01:05
created

_GlancesCurses.__catch_key()   F

Complexity

Conditions 36

Size

Total Lines 111
Code Lines 75

Duplication

Lines 0
Ratio 0 %

Importance

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