Test Failed
Push — develop ( 29e3c5...0d1a1d )
by Nicolas
03:07
created

glances/outputs/glances_curses.py (1 issue)

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