Completed
Push — master ( 3dcd25...20576f )
by Nicolas
01:26
created

_GlancesCurses   F

Complexity

Total Complexity 189

Size/Duplication

Total Lines 1023
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 1023
rs 1.1177
wmc 189

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 49 2
A is_theme() 0 3 1
A _init_history() 0 13 3
F _init_colors() 0 109 12
A _init_cursor() 0 8 3
A flash_cursor() 0 2 1
A no_flash_cursor() 0 2 1
A set_cursor() 0 12 3
A get_key() 0 14 4
A load_config() 0 7 3
A new_column() 0 3 1
B get_stats_display_width() 0 18 7
A get_stats_display_height() 0 11 4
B update() 0 37 5
A new_line() 0 3 1
A init_line() 0 4 1
A init_line_column() 0 4 1
B end() 0 12 5
F display_plugin() 0 86 22
A erase() 0 3 1
A wait() 0 3 1
A flush() 0 11 1
A init_column() 0 4 1
F display_popup() 0 77 11
F __catch_key() 0 176 48
F display() 0 319 46

How to fix   Complexity   

Complex Class

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