Completed
Push — master ( 2b80fa...6ea077 )
by Nicolas
01:22
created

_GlancesCurses.__display_firstline()   A

Complexity

Conditions 1

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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