Completed
Push — master ( 1806d1...053f07 )
by Nicolas
01:42
created

glances/outputs/glances_curses.py (2 issues)

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, args=None):
55
        # Init args
56
        self.args = args
57
58
        # Init windows positions
59
        self.term_w = 80
60
        self.term_h = 24
61
62
        # Space between stats
63
        self.space_between_column = 3
64
        self.space_between_line = 2
65
66
        # Init the curses screen
67
        self.screen = curses.initscr()
68
        if not self.screen:
69
            logger.critical("Cannot init the curses library.\n")
70
            sys.exit(1)
71
72
        # Init cursor
73
        self._init_cursor()
74
75
        # Init the colors
76
        self._init_colors()
77
78
        # Init main window
79
        self.term_window = self.screen.subwin(0, 0)
80
81
        # Init refresh time
82
        self.__refresh_time = args.time
83
84
        # Init edit filter tag
85
        self.edit_filter = False
86
87
        # Init the process min/max reset
88
        self.args.reset_minmax_tag = False
89
90
        # Catch key pressed with non blocking mode
91
        self.no_flash_cursor()
92
        self.term_window.nodelay(1)
93
        self.pressedkey = -1
94
95
        # History tag
96
        self._init_history()
97
98
    def _init_history(self):
99
        '''Init the history option'''
100
101
        self.reset_history_tag = False
102
        self.history_tag = False
103
        if self.args.enable_history:
104
            logger.info('Stats history enabled with output path %s' %
105
                        self.args.path_history)
106
            from glances.exports.glances_history import GlancesHistory
107
            self.glances_history = GlancesHistory(self.args.path_history)
108
            if not self.glances_history.graph_enabled():
109
                self.args.enable_history = False
110
                logger.error(
111
                    'Stats history disabled because MatPlotLib is not installed')
112
113
    def _init_cursor(self):
114
        '''Init cursors'''
115
116
        if hasattr(curses, 'noecho'):
117
            curses.noecho()
118
        if hasattr(curses, 'cbreak'):
119
            curses.cbreak()
120
        self.set_cursor(0)
121
122
    def _init_colors(self):
123
        '''Init the Curses color layout'''
124
125
        # Set curses options
126
        if hasattr(curses, 'start_color'):
127
            curses.start_color()
128
        if hasattr(curses, 'use_default_colors'):
129
            curses.use_default_colors()
130
131
        # Init colors
132
        if self.args.disable_bold:
133
            A_BOLD = 0
134
            self.args.disable_bg = True
135
        else:
136
            A_BOLD = curses.A_BOLD
137
138
        self.title_color = A_BOLD
139
        self.title_underline_color = A_BOLD | curses.A_UNDERLINE
140
        self.help_color = A_BOLD
141
142
        if curses.has_colors():
143
            # The screen is compatible with a colored design
144
            if self.args.theme_white:
145
                # White theme: black ==> white
146
                curses.init_pair(1, curses.COLOR_BLACK, -1)
147
            else:
148
                curses.init_pair(1, curses.COLOR_WHITE, -1)
149
            if self.args.disable_bg:
150
                curses.init_pair(2, curses.COLOR_RED, -1)
151
                curses.init_pair(3, curses.COLOR_GREEN, -1)
152
                curses.init_pair(4, curses.COLOR_BLUE, -1)
153
                curses.init_pair(5, curses.COLOR_MAGENTA, -1)
154
            else:
155
                curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED)
156
                curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_GREEN)
157
                curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE)
158
                curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
159
            curses.init_pair(6, curses.COLOR_RED, -1)
160
            curses.init_pair(7, curses.COLOR_GREEN, -1)
161
            curses.init_pair(8, curses.COLOR_BLUE, -1)
162
163
            # Colors text styles
164
            if curses.COLOR_PAIRS > 8:
165
                try:
166
                    curses.init_pair(9, curses.COLOR_MAGENTA, -1)
167
                except Exception:
168
                    if self.args.theme_white:
169
                        curses.init_pair(9, curses.COLOR_BLACK, -1)
170
                    else:
171
                        curses.init_pair(9, curses.COLOR_WHITE, -1)
172
                try:
173
                    curses.init_pair(10, curses.COLOR_CYAN, -1)
174
                except Exception:
175
                    if self.args.theme_white:
176
                        curses.init_pair(10, curses.COLOR_BLACK, -1)
177
                    else:
178
                        curses.init_pair(10, curses.COLOR_WHITE, -1)
179
180
                self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
181
                self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
182
                self.filter_color = curses.color_pair(10) | A_BOLD
183
184
            self.no_color = curses.color_pair(1)
185
            self.default_color = curses.color_pair(3) | A_BOLD
186
            self.nice_color = curses.color_pair(9) | A_BOLD
187
            self.cpu_time_color = curses.color_pair(9) | A_BOLD
188
            self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD
189
            self.ifWARNING_color = curses.color_pair(5) | A_BOLD
190
            self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD
191
            self.default_color2 = curses.color_pair(7) | A_BOLD
192
            self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
193
194
        else:
195
            # The screen is NOT compatible with a colored design
196
            # switch to B&W text styles
197
            self.no_color = curses.A_NORMAL
198
            self.default_color = curses.A_NORMAL
199
            self.nice_color = A_BOLD
200
            self.cpu_time_color = A_BOLD
201
            self.ifCAREFUL_color = curses.A_UNDERLINE
202
            self.ifWARNING_color = A_BOLD
203
            self.ifCRITICAL_color = curses.A_REVERSE
204
            self.default_color2 = curses.A_NORMAL
205
            self.ifCAREFUL_color2 = curses.A_UNDERLINE
206
            self.ifWARNING_color2 = A_BOLD
207
            self.ifCRITICAL_color2 = curses.A_REVERSE
208
            self.filter_color = A_BOLD
209
210
        # Define the colors list (hash table) for stats
211
        self.colors_list = {
212
            'DEFAULT': self.no_color,
213
            'UNDERLINE': curses.A_UNDERLINE,
214
            'BOLD': A_BOLD,
215
            'SORT': A_BOLD,
216
            'OK': self.default_color2,
217
            'FILTER': self.filter_color,
218
            'TITLE': self.title_color,
219
            'PROCESS': self.default_color2,
220
            'STATUS': self.default_color2,
221
            'NICE': self.nice_color,
222
            'CPU_TIME': self.cpu_time_color,
223
            'CAREFUL': self.ifCAREFUL_color2,
224
            'WARNING': self.ifWARNING_color2,
225
            'CRITICAL': self.ifCRITICAL_color2,
226
            'OK_LOG': self.default_color,
227
            'CAREFUL_LOG': self.ifCAREFUL_color,
228
            'WARNING_LOG': self.ifWARNING_color,
229
            'CRITICAL_LOG': self.ifCRITICAL_color,
230
            'PASSWORD': curses.A_PROTECT
231
        }
232
233
    def flash_cursor(self):
234
        self.term_window.keypad(1)
235
236
    def no_flash_cursor(self):
237
        self.term_window.keypad(0)
238
239
    def set_cursor(self, value):
240
        """Configure the curse cursor apparence.
241
242
        0: invisible
243
        1: visible
244
        2: very visible
245
        """
246
        if hasattr(curses, 'curs_set'):
247
            try:
248
                curses.curs_set(value)
249
            except Exception:
250
                pass
251
252
    def get_key(self, window):
253
        # Catch ESC key AND numlock key (issue #163)
254
        keycode = [0, 0]
255
        keycode[0] = window.getch()
256
        keycode[1] = window.getch()
257
258
        if keycode != [-1, -1]:
259
            logger.debug("Keypressed (code: %s)" % keycode)
260
261
        if keycode[0] == 27 and keycode[1] != -1:
262
            # Do not escape on specials keys
263
            return -1
264
        else:
265
            return keycode[0]
266
267
    def __catch_key(self, return_to_browser=False):
268
        # Catch the pressed key
269
        self.pressedkey = self.get_key(self.term_window)
270
271
        # Actions...
272
        if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
273
            # 'ESC'|'q' > Quit
274
            if return_to_browser:
275
                logger.info("Stop Glances client and return to the browser")
276
            else:
277
                self.end()
278
                logger.info("Stop Glances")
279
                sys.exit(0)
280
        elif self.pressedkey == 10:
281
            # 'ENTER' > Edit the process filter
282
            self.edit_filter = not self.edit_filter
283
        elif self.pressedkey == ord('0'):
284
            # '0' > Switch between IRIX and Solaris mode
285
            self.args.disable_irix = not self.args.disable_irix
286
        elif self.pressedkey == ord('1'):
287
            # '1' > Switch between CPU and PerCPU information
288
            self.args.percpu = not self.args.percpu
289
        elif self.pressedkey == ord('2'):
290
            # '2' > Enable/disable left sidebar
291
            self.args.disable_left_sidebar = not self.args.disable_left_sidebar
292
        elif self.pressedkey == ord('3'):
293
            # '3' > Enable/disable quicklook
294
            self.args.disable_quicklook = not self.args.disable_quicklook
295
        elif self.pressedkey == ord('4'):
296
            # '4' > Enable/disable all but quick look and load
297
            self.args.full_quicklook = not self.args.full_quicklook
298
            if self.args.full_quicklook:
299
                self.args.disable_quicklook = False
300
                self.args.disable_cpu = True
301
                self.args.disable_mem = True
302
                self.args.disable_swap = True
303
            else:
304
                self.args.disable_quicklook = False
305
                self.args.disable_cpu = False
306
                self.args.disable_mem = False
307
                self.args.disable_swap = False
308
        elif self.pressedkey == ord('5'):
309
            # '5' > Enable/disable top menu
310
            logger.info(self.args.disable_top)
311
            self.args.disable_top = not self.args.disable_top
312
            if self.args.disable_top:
313
                self.args.disable_quicklook = True
314
                self.args.disable_cpu = True
315
                self.args.disable_mem = True
316
                self.args.disable_swap = True
317
                self.args.disable_load = True
318
            else:
319
                self.args.disable_quicklook = False
320
                self.args.disable_cpu = False
321
                self.args.disable_mem = False
322
                self.args.disable_swap = False
323
                self.args.disable_load = False
324
        elif self.pressedkey == ord('/'):
325
            # '/' > Switch between short/long name for processes
326
            self.args.process_short_name = not self.args.process_short_name
327
        elif self.pressedkey == ord('a'):
328
            # 'a' > Sort processes automatically and reset to 'cpu_percent'
329
            glances_processes.auto_sort = True
330
            glances_processes.sort_key = 'cpu_percent'
331
        elif self.pressedkey == ord('b'):
332
            # 'b' > Switch between bit/s and Byte/s for network IO
333
            self.args.byte = not self.args.byte
334
        elif self.pressedkey == ord('B'):
335
            # 'B' > Switch between bit/s and IO/s for Disk IO
336
            self.args.diskio_iops = not self.args.diskio_iops
337
        elif self.pressedkey == ord('c'):
338
            # 'c' > Sort processes by CPU usage
339
            glances_processes.auto_sort = False
340
            glances_processes.sort_key = 'cpu_percent'
341
        elif self.pressedkey == ord('d'):
342
            # 'd' > Show/hide disk I/O stats
343
            self.args.disable_diskio = not self.args.disable_diskio
344
        elif self.pressedkey == ord('D'):
345
            # 'D' > Show/hide Docker stats
346
            self.args.disable_docker = not self.args.disable_docker
347
        elif self.pressedkey == ord('e'):
348
            # 'e' > Enable/Disable extended stats for top process
349
            self.args.enable_process_extended = not self.args.enable_process_extended
350
            if not self.args.enable_process_extended:
351
                glances_processes.disable_extended()
352
            else:
353
                glances_processes.enable_extended()
354
        elif self.pressedkey == ord('E'):
355
            # 'E' > Erase the process filter
356
            logger.info("Erase process filter")
357
            glances_processes.process_filter = None
358
        elif self.pressedkey == ord('F'):
359
            # 'F' > Switch between FS available and free space
360
            self.args.fs_free_space = not self.args.fs_free_space
361
        elif self.pressedkey == ord('f'):
362
            # 'f' > Show/hide fs / folder stats
363
            self.args.disable_fs = not self.args.disable_fs
364
            self.args.disable_folder = not self.args.disable_folder
365
        elif self.pressedkey == ord('g'):
366
            # 'g' > History
367
            self.history_tag = not self.history_tag
368
        elif self.pressedkey == ord('h'):
369
            # 'h' > Show/hide help
370
            self.args.help_tag = not self.args.help_tag
371
        elif self.pressedkey == ord('i'):
372
            # 'i' > Sort processes by IO rate (not available on OS X)
373
            glances_processes.auto_sort = False
374
            glances_processes.sort_key = 'io_counters'
375
        elif self.pressedkey == ord('I'):
376
            # 'I' > Show/hide IP module
377
            self.args.disable_ip = not self.args.disable_ip
378
        elif self.pressedkey == ord('l'):
379
            # 'l' > Show/hide log messages
380
            self.args.disable_log = not self.args.disable_log
381
        elif self.pressedkey == ord('m'):
382
            # 'm' > Sort processes by MEM usage
383
            glances_processes.auto_sort = False
384
            glances_processes.sort_key = 'memory_percent'
385
        elif self.pressedkey == ord('M'):
386
            # 'M' > Reset processes summary min/max
387
            self.args.reset_minmax_tag = not self.args.reset_minmax_tag
388
        elif self.pressedkey == ord('n'):
389
            # 'n' > Show/hide network stats
390
            self.args.disable_network = not self.args.disable_network
391
        elif self.pressedkey == ord('p'):
392
            # 'p' > Sort processes by name
393
            glances_processes.auto_sort = False
394
            glances_processes.sort_key = 'name'
395
        elif self.pressedkey == ord('r'):
396
            # 'r' > Reset history
397
            self.reset_history_tag = not self.reset_history_tag
398
        elif self.pressedkey == ord('R'):
399
            # 'R' > Hide RAID plugins
400
            self.args.disable_raid = not self.args.disable_raid
401
        elif self.pressedkey == ord('s'):
402
            # 's' > Show/hide sensors stats (Linux-only)
403
            self.args.disable_sensors = not self.args.disable_sensors
404
        elif self.pressedkey == ord('t'):
405
            # 't' > Sort processes by TIME usage
406
            glances_processes.auto_sort = False
407
            glances_processes.sort_key = 'cpu_times'
408
        elif self.pressedkey == ord('T'):
409
            # 'T' > View network traffic as sum Rx+Tx
410
            self.args.network_sum = not self.args.network_sum
411
        elif self.pressedkey == ord('u'):
412
            # 'u' > Sort processes by USER
413
            glances_processes.auto_sort = False
414
            glances_processes.sort_key = 'username'
415
        elif self.pressedkey == ord('U'):
416
            # 'U' > View cumulative network I/O (instead of bitrate)
417
            self.args.network_cumul = not self.args.network_cumul
418
        elif self.pressedkey == ord('w'):
419
            # 'w' > Delete finished warning logs
420
            glances_logs.clean()
421
        elif self.pressedkey == ord('x'):
422
            # 'x' > Delete finished warning and critical logs
423
            glances_logs.clean(critical=True)
424
        elif self.pressedkey == ord('z'):
425
            # 'z' > Enable/Disable processes stats (count + list + monitor)
426
            # Enable/Disable display
427
            self.args.disable_process = not self.args.disable_process
428
            # Enable/Disable update
429
            if self.args.disable_process:
430
                glances_processes.disable()
431
            else:
432
                glances_processes.enable()
433
        # Return the key code
434
        return self.pressedkey
435
436
    def end(self):
437
        """Shutdown the curses window."""
438
        if hasattr(curses, 'echo'):
439
            curses.echo()
440
        if hasattr(curses, 'nocbreak'):
441
            curses.nocbreak()
442
        if hasattr(curses, 'curs_set'):
443
            try:
444
                curses.curs_set(1)
445
            except Exception:
446
                pass
447
        curses.endwin()
448
449
    def init_line_column(self):
450
        """Init the line and column position for the curses inteface."""
451
        self.init_line()
452
        self.init_column()
453
454
    def init_line(self):
455
        """Init the line position for the curses inteface."""
456
        self.line = 0
457
        self.next_line = 0
458
459
    def init_column(self):
460
        """Init the column position for the curses inteface."""
461
        self.column = 0
462
        self.next_column = 0
463
464
    def new_line(self):
465
        """New line in the curses interface."""
466
        self.line = self.next_line
467
468
    def new_column(self):
469
        """New column in the curses interface."""
470
        self.column = self.next_column
471
472
    def display(self, stats, cs_status=None):
473
        """Display stats on the screen.
474
475
        stats: Stats database to display
476
        cs_status:
477
            "None": standalone or server mode
478
            "Connected": Client is connected to a Glances server
479
            "SNMP": Client is connected to a SNMP server
480
            "Disconnected": Client is disconnected from the server
481
482
        Return:
483
            True if the stats have been displayed
484
            False if the help have been displayed
485
        """
486
        # Init the internal line/column for Glances Curses
487
        self.init_line_column()
488
489
        # Get the screen size
490
        screen_x = self.screen.getmaxyx()[1]
491
        screen_y = self.screen.getmaxyx()[0]
492
493
        # No processes list in SNMP mode
494
        if cs_status == 'SNMP':
495
            # so... more space for others plugins
496
            plugin_max_width = 43
497
        else:
498
            plugin_max_width = None
499
500
        # Update the stats messages
501
        ###########################
502
503
        # Update the client server status
504
        self.args.cs_status = cs_status
505
        stats_system = stats.get_plugin(
506
            'system').get_stats_display(args=self.args)
507
        stats_uptime = stats.get_plugin('uptime').get_stats_display()
508
        if self.args.percpu:
509
            stats_cpu = stats.get_plugin('percpu').get_stats_display(args=self.args)
510
        else:
511
            stats_cpu = stats.get_plugin('cpu').get_stats_display(args=self.args)
512
        stats_load = stats.get_plugin('load').get_stats_display(args=self.args)
513
        stats_mem = stats.get_plugin('mem').get_stats_display(args=self.args)
514
        stats_memswap = stats.get_plugin('memswap').get_stats_display(args=self.args)
515
        stats_network = stats.get_plugin('network').get_stats_display(
516
            args=self.args, max_width=plugin_max_width)
517
        try:
518
            stats_ip = stats.get_plugin('ip').get_stats_display(args=self.args)
519
        except AttributeError:
520
            stats_ip = None
521
        stats_diskio = stats.get_plugin(
522
            'diskio').get_stats_display(args=self.args)
523
        stats_fs = stats.get_plugin('fs').get_stats_display(
524
            args=self.args, max_width=plugin_max_width)
525
        stats_folders = stats.get_plugin('folders').get_stats_display(
526
            args=self.args, max_width=plugin_max_width)
527
        stats_raid = stats.get_plugin('raid').get_stats_display(
528
            args=self.args)
529
        stats_sensors = stats.get_plugin(
530
            'sensors').get_stats_display(args=self.args)
531
        stats_now = stats.get_plugin('now').get_stats_display()
532
        stats_docker = stats.get_plugin('docker').get_stats_display(
533
            args=self.args)
534
        stats_processcount = stats.get_plugin(
535
            'processcount').get_stats_display(args=self.args)
536
        stats_monitor = stats.get_plugin(
537
            'monitor').get_stats_display(args=self.args)
538
        stats_alert = stats.get_plugin(
539
            'alert').get_stats_display(args=self.args)
540
541
        # Adapt number of processes to the available space
542
        max_processes_displayed = screen_y - 11 - \
543
            self.get_stats_display_height(stats_alert) - \
544
            self.get_stats_display_height(stats_docker)
545
        try:
546
            if self.args.enable_process_extended and not self.args.process_tree:
547
                max_processes_displayed -= 4
548
        except AttributeError:
549
            pass
550
        if max_processes_displayed < 0:
551
            max_processes_displayed = 0
552
        if (glances_processes.max_processes is None or
553
                glances_processes.max_processes != max_processes_displayed):
554
            logger.debug("Set number of displayed processes to {0}".format(max_processes_displayed))
555
            glances_processes.max_processes = max_processes_displayed
556
557
        stats_processlist = stats.get_plugin(
558
            'processlist').get_stats_display(args=self.args)
559
560
        # Display the stats on the curses interface
561
        ###########################################
562
563
        # Help screen (on top of the other stats)
564
        if self.args.help_tag:
565
            # Display the stats...
566
            self.display_plugin(
567
                stats.get_plugin('help').get_stats_display(args=self.args))
568
            # ... and exit
569
            return False
570
571
        # ==================================
572
        # Display first line (system+uptime)
573
        # ==================================
574
        # Space between column
575
        self.space_between_column = 0
576
        self.new_line()
577
        l_uptime = self.get_stats_display_width(
578
            stats_system) + self.space_between_column + self.get_stats_display_width(stats_ip) + 3 + self.get_stats_display_width(stats_uptime)
579
        self.display_plugin(
580
            stats_system, display_optional=(screen_x >= l_uptime))
581
        self.new_column()
582
        self.display_plugin(stats_ip)
583
        # Space between column
584
        self.space_between_column = 3
585
        self.new_column()
586
        self.display_plugin(stats_uptime)
587
588
        # ========================================================
589
        # Display second line (<SUMMARY>+CPU|PERCPU+LOAD+MEM+SWAP)
590
        # ========================================================
591
        self.init_column()
592
        self.new_line()
593
594
        # Init quicklook
595
        stats_quicklook = {'msgdict': []}
596
        quicklook_width = 0
597
598
        # Get stats for CPU, MEM, SWAP and LOAD (if needed)
599
        if self.args.disable_cpu:
600
            cpu_width = 0
601
        else:
602
            cpu_width = self.get_stats_display_width(stats_cpu)
603
        if self.args.disable_mem:
604
            mem_width = 0
605
        else:
606
            mem_width = self.get_stats_display_width(stats_mem)
607
        if self.args.disable_swap:
608
            swap_width = 0
609
        else:
610
            swap_width = self.get_stats_display_width(stats_memswap)
611
        if self.args.disable_load:
612
            load_width = 0
613
        else:
614
            load_width = self.get_stats_display_width(stats_load)
615
616
        # Size of plugins but quicklook
617
        stats_width = cpu_width + mem_width + swap_width + load_width
618
619
        # Number of plugin but quicklook
620
        stats_number = (
621
            int(not self.args.disable_cpu and stats_cpu['msgdict'] != []) +
622
            int(not self.args.disable_mem and stats_mem['msgdict'] != []) +
623
            int(not self.args.disable_swap and stats_memswap['msgdict'] != []) +
624
            int(not self.args.disable_load and stats_load['msgdict'] != []))
625
626
        if not self.args.disable_quicklook:
627
            # Quick look is in the place !
628
            if self.args.full_quicklook:
629
                quicklook_width = screen_x - (stats_width + 8 + stats_number * self.space_between_column)
630
            else:
631
                quicklook_width = min(screen_x - (stats_width + 8 + stats_number * self.space_between_column), 79)
632
            try:
633
                stats_quicklook = stats.get_plugin(
634
                    'quicklook').get_stats_display(max_width=quicklook_width, args=self.args)
635
            except AttributeError as e:
636
                logger.debug("Quicklook plugin not available (%s)" % e)
637
            else:
638
                quicklook_width = self.get_stats_display_width(stats_quicklook)
639
                stats_width += quicklook_width + 1
640
            self.space_between_column = 1
641
            self.display_plugin(stats_quicklook)
642
            self.new_column()
643
644
        # Compute spaces between plugins
645
        # Note: Only one space between Quicklook and others
646
        display_optional_cpu = True
647
        display_optional_mem = True
648
        if stats_number > 1:
649
            self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
650
            # No space ? Remove optionnal MEM stats
651
            if self.space_between_column < 3:
0 ignored issues
show
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
652
                display_optional_mem = False
653
                if self.args.disable_mem:
654
                    mem_width = 0
655
                else:
656
                    mem_width = self.get_stats_display_width(stats_mem, without_option=True)
657
                stats_width = quicklook_width + 1 + cpu_width + mem_width + swap_width + load_width
658
                self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
659
            # No space again ? Remove optionnal CPU stats
660
            if self.space_between_column < 3:
0 ignored issues
show
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
661
                display_optional_cpu = False
662
                if self.args.disable_cpu:
663
                    cpu_width = 0
664
                else:
665
                    cpu_width = self.get_stats_display_width(stats_cpu, without_option=True)
666
                stats_width = quicklook_width + 1 + cpu_width + mem_width + swap_width + load_width
667
                self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
668
        else:
669
            self.space_between_column = 0
670
671
        # Display CPU, MEM, SWAP and LOAD
672
        self.display_plugin(stats_cpu, display_optional=display_optional_cpu)
673
        self.new_column()
674
        self.display_plugin(stats_mem, display_optional=display_optional_mem)
675
        self.new_column()
676
        self.display_plugin(stats_memswap)
677
        self.new_column()
678
        self.display_plugin(stats_load)
679
680
        # Space between column
681
        self.space_between_column = 3
682
683
        # Backup line position
684
        self.saved_line = self.next_line
685
686
        # ==================================================================
687
        # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time)
688
        # ==================================================================
689
        self.init_column()
690
        if not (self.args.disable_network and
691
                self.args.disable_diskio and
692
                self.args.disable_fs and
693
                self.args.disable_folder and
694
                self.args.disable_raid and
695
                self.args.disable_sensors) and not self.args.disable_left_sidebar:
696
            self.new_line()
697
            self.display_plugin(stats_network)
698
            self.new_line()
699
            self.display_plugin(stats_diskio)
700
            self.new_line()
701
            self.display_plugin(stats_fs)
702
            self.new_line()
703
            self.display_plugin(stats_folders)
704
            self.new_line()
705
            self.display_plugin(stats_raid)
706
            self.new_line()
707
            self.display_plugin(stats_sensors)
708
            self.new_line()
709
            self.display_plugin(stats_now)
710
711
        # ====================================
712
        # Display right stats (process and co)
713
        # ====================================
714
        # If space available...
715
        if screen_x > 52:
716
            # Restore line position
717
            self.next_line = self.saved_line
718
719
            # Display right sidebar
720
            # ((DOCKER)+PROCESS_COUNT+(MONITORED)+PROCESS_LIST+ALERT)
721
            self.new_column()
722
            self.new_line()
723
            self.display_plugin(stats_docker)
724
            self.new_line()
725
            self.display_plugin(stats_processcount)
726
            if glances_processes.process_filter is None and cs_status is None:
727
                # Do not display stats monitor list if a filter exist
728
                self.new_line()
729
                self.display_plugin(stats_monitor)
730
            self.new_line()
731
            self.display_plugin(stats_processlist,
732
                                display_optional=(screen_x > 102),
733
                                display_additional=(not OSX),
734
                                max_y=(screen_y - self.get_stats_display_height(stats_alert) - 2))
735
            self.new_line()
736
            self.display_plugin(stats_alert)
737
738
        # History option
739
        # Generate history graph
740
        if self.history_tag and self.args.enable_history:
741
            self.display_popup(
742
                'Generate graphs history in {0}\nPlease wait...'.format(
743
                    self.glances_history.get_output_folder()))
744
            self.display_popup(
745
                'Generate graphs history in {0}\nDone: {1} graphs generated'.format(
746
                    self.glances_history.get_output_folder(),
747
                    self.glances_history.generate_graph(stats)))
748
        elif self.reset_history_tag and self.args.enable_history:
749
            self.display_popup('Reset history')
750
            self.glances_history.reset(stats)
751
        elif (self.history_tag or self.reset_history_tag) and not self.args.enable_history:
752
            try:
753
                self.glances_history.graph_enabled()
754
            except Exception:
755
                self.display_popup('History disabled\nEnable it using --enable-history')
756
            else:
757
                self.display_popup('History disabled\nPlease install matplotlib')
758
        self.history_tag = False
759
        self.reset_history_tag = False
760
761
        # Display edit filter popup
762
        # Only in standalone mode (cs_status is None)
763
        if self.edit_filter and cs_status is None:
764
            new_filter = self.display_popup(
765
                'Process filter pattern: ', is_input=True,
766
                input_value=glances_processes.process_filter)
767
            glances_processes.process_filter = new_filter
768
        elif self.edit_filter and cs_status != 'None':
769
            self.display_popup('Process filter only available in standalone mode')
770
        self.edit_filter = False
771
772
        return True
773
774
    def display_popup(self, message,
775
                      size_x=None, size_y=None,
776
                      duration=3,
777
                      is_input=False,
778
                      input_size=30,
779
                      input_value=None):
780
        """
781
        Display a centered popup.
782
783
        If is_input is False:
784
         Display a centered popup with the given message during duration seconds
785
         If size_x and size_y: set the popup size
786
         else set it automatically
787
         Return True if the popup could be displayed
788
789
        If is_input is True:
790
         Display a centered popup with the given message and a input field
791
         If size_x and size_y: set the popup size
792
         else set it automatically
793
         Return the input string or None if the field is empty
794
        """
795
        # Center the popup
796
        sentence_list = message.split('\n')
797
        if size_x is None:
798
            size_x = len(max(sentence_list, key=len)) + 4
799
            # Add space for the input field
800
            if is_input:
801
                size_x += input_size
802
        if size_y is None:
803
            size_y = len(sentence_list) + 4
804
        screen_x = self.screen.getmaxyx()[1]
805
        screen_y = self.screen.getmaxyx()[0]
806
        if size_x > screen_x or size_y > screen_y:
807
            # No size to display the popup => abord
808
            return False
809
        pos_x = int((screen_x - size_x) / 2)
810
        pos_y = int((screen_y - size_y) / 2)
811
812
        # Create the popup
813
        popup = curses.newwin(size_y, size_x, pos_y, pos_x)
814
815
        # Fill the popup
816
        popup.border()
817
818
        # Add the message
819
        for y, m in enumerate(message.split('\n')):
820
            popup.addnstr(2 + y, 2, m, len(m))
821
822
        if is_input and not WINDOWS:
823
            # Create a subwindow for the text field
824
            subpop = popup.derwin(1, input_size, 2, 2 + len(m))
825
            subpop.attron(self.colors_list['FILTER'])
826
            # Init the field with the current value
827
            if input_value is not None:
828
                subpop.addnstr(0, 0, input_value, len(input_value))
829
            # Display the popup
830
            popup.refresh()
831
            subpop.refresh()
832
            # Create the textbox inside the subwindows
833
            self.set_cursor(2)
834
            self.flash_cursor()
835
            textbox = GlancesTextbox(subpop, insert_mode=False)
836
            textbox.edit()
837
            self.set_cursor(0)
838
            self.no_flash_cursor()
839
            if textbox.gather() != '':
840
                logger.debug(
841
                    "User enters the following string: %s" % textbox.gather())
842
                return textbox.gather()[:-1]
843
            else:
844
                logger.debug("User centers an empty string")
845
                return None
846
        else:
847
            # Display the popup
848
            popup.refresh()
849
            self.wait(duration * 1000)
850
            return True
851
852
    def display_plugin(self, plugin_stats,
853
                       display_optional=True,
854
                       display_additional=True,
855
                       max_y=65535):
856
        """Display the plugin_stats on the screen.
857
858
        If display_optional=True display the optional stats
859
        If display_additional=True display additionnal stats
860
        max_y do not display line > max_y
861
        """
862
        # Exit if:
863
        # - the plugin_stats message is empty
864
        # - the display tag = False
865
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
866
            # Exit
867
            return 0
868
869
        # Get the screen size
870
        screen_x = self.screen.getmaxyx()[1]
871
        screen_y = self.screen.getmaxyx()[0]
872
873
        # Set the upper/left position of the message
874
        if plugin_stats['align'] == 'right':
875
            # Right align (last column)
876
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
877
        else:
878
            display_x = self.column
879
        if plugin_stats['align'] == 'bottom':
880
            # Bottom (last line)
881
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
882
        else:
883
            display_y = self.line
884
885
        # Display
886
        x = display_x
887
        x_max = x
888
        y = display_y
889
        for m in plugin_stats['msgdict']:
890
            # New line
891
            if m['msg'].startswith('\n'):
892
                # Go to the next line
893
                y += 1
894
                # Return to the first column
895
                x = display_x
896
                continue
897
            # Do not display outside the screen
898
            if x < 0:
899
                continue
900
            if not m['splittable'] and (x + len(m['msg']) > screen_x):
901
                continue
902
            if y < 0 or (y + 1 > screen_y) or (y > max_y):
903
                break
904
            # If display_optional = False do not display optional stats
905
            if not display_optional and m['optional']:
906
                continue
907
            # If display_additional = False do not display additional stats
908
            if not display_additional and m['additional']:
909
                continue
910
            # Is it possible to display the stat with the current screen size
911
            # !!! Crach if not try/except... Why ???
912
            try:
913
                self.term_window.addnstr(y, x,
914
                                         m['msg'],
915
                                         # Do not disply outside the screen
916
                                         screen_x - x,
917
                                         self.colors_list[m['decoration']])
918
            except Exception:
919
                pass
920
            else:
921
                # New column
922
                # Python 2: we need to decode to get real screen size because
923
                # UTF-8 special tree chars occupy several bytes.
924
                # Python 3: strings are strings and bytes are bytes, all is
925
                # good.
926
                try:
927
                    x += len(u(m['msg']))
928
                except UnicodeDecodeError:
929
                    # Quick and dirty hack for issue #745
930
                    pass
931
                if x > x_max:
932
                    x_max = x
933
934
        # Compute the next Glances column/line position
935
        self.next_column = max(
936
            self.next_column, x_max + self.space_between_column)
937
        self.next_line = max(self.next_line, y + self.space_between_line)
938
939
    def erase(self):
940
        """Erase the content of the screen."""
941
        self.term_window.erase()
942
943
    def flush(self, stats, cs_status=None):
944
        """Clear and update the screen.
945
946
        stats: Stats database to display
947
        cs_status:
948
            "None": standalone or server mode
949
            "Connected": Client is connected to the server
950
            "Disconnected": Client is disconnected from the server
951
        """
952
        self.erase()
953
        self.display(stats, cs_status=cs_status)
954
955
    def update(self, stats, cs_status=None, return_to_browser=False):
956
        """Update the screen.
957
958
        Wait for __refresh_time sec / catch key every 100 ms.
959
960
        INPUT
961
        stats: Stats database to display
962
        cs_status:
963
            "None": standalone or server mode
964
            "Connected": Client is connected to the server
965
            "Disconnected": Client is disconnected from the server
966
        return_to_browser:
967
            True: Do not exist, return to the browser list
968
            False: Exit and return to the shell
969
970
        OUPUT
971
        True: Exit key has been pressed
972
        False: Others cases...
973
        """
974
        # Flush display
975
        self.flush(stats, cs_status=cs_status)
976
977
        # Wait
978
        exitkey = False
979
        countdown = Timer(self.__refresh_time)
980
        while not countdown.finished() and not exitkey:
981
            # Getkey
982
            pressedkey = self.__catch_key(return_to_browser=return_to_browser)
983
            # Is it an exit key ?
984
            exitkey = (pressedkey == ord('\x1b') or pressedkey == ord('q'))
985
            if not exitkey and pressedkey > -1:
986
                # Redraw display
987
                self.flush(stats, cs_status=cs_status)
988
            # Wait 100ms...
989
            self.wait()
990
991
        return exitkey
992
993
    def wait(self, delay=100):
994
        """Wait delay in ms"""
995
        curses.napms(100)
996
997
    def get_stats_display_width(self, curse_msg, without_option=False):
998
        """Return the width of the formatted curses message.
999
1000
        The height is defined by the maximum line.
1001
        """
1002
        try:
1003
            if without_option:
1004
                # Size without options
1005
                c = len(max(''.join([(re.sub(r'[^\x00-\x7F]+', ' ', i['msg']) if not i['optional'] else "")
1006
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
1007
            else:
1008
                # Size with all options
1009
                c = len(max(''.join([re.sub(r'[^\x00-\x7F]+', ' ', i['msg'])
1010
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
1011
        except Exception:
1012
            return 0
1013
        else:
1014
            return c
1015
1016
    def get_stats_display_height(self, curse_msg):
1017
        r"""Return the height of the formatted curses message.
1018
1019
        The height is defined by the number of '\n' (new line).
1020
        """
1021
        try:
1022
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
1023
        except Exception:
1024
            return 0
1025
        else:
1026
            return c + 1
1027
1028
1029
class GlancesCursesStandalone(_GlancesCurses):
1030
1031
    """Class for the Glances curse standalone."""
1032
1033
    pass
1034
1035
1036
class GlancesCursesClient(_GlancesCurses):
1037
1038
    """Class for the Glances curse client."""
1039
1040
    pass
1041
1042
1043
if not WINDOWS:
1044
    class GlancesTextbox(Textbox, object):
1045
1046
        def __init__(self, *args, **kwargs):
1047
            super(GlancesTextbox, self).__init__(*args, **kwargs)
1048
1049
        def do_command(self, ch):
1050
            if ch == 10:  # Enter
1051
                return 0
1052
            if ch == 127:  # Back
1053
                return 8
1054
            return super(GlancesTextbox, self).do_command(ch)
1055