Completed
Push — master ( d2dcdf...3f6257 )
by Nicolas
01:19
created

_GlancesCurses.enable_top()   A

Complexity

Conditions 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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