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

_GlancesCurses.erase()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 3
rs 10
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:
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:
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