glances_curses.py 39.4 KB
Newer Older
1 2
# -*- coding: utf-8 -*-
#
3
# This file is part of Glances.
4
#
N
nicolargo 已提交
5
# Copyright (C) 2019 Nicolargo <nicolas@nicolargo.com>
6 7 8 9 10 11 12 13 14 15 16 17 18 19
#
# Glances is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Glances is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

A
PEP 257  
Alessio Sergi 已提交
20 21
"""Curses interface class."""

N
Nicolargo 已提交
22
import re
A
flake8  
Alessio Sergi 已提交
23
import sys
24

25
from glances.compat import u, itervalues
A
Alessio Sergi 已提交
26
from glances.globals import MACOS, WINDOWS
27
from glances.logger import logger
28
from glances.events import glances_events
29 30
from glances.processes import glances_processes
from glances.timer import Timer
31

A
Alessio Sergi 已提交
32
# Import curses library for "normal" operating system
A
Alessio Sergi 已提交
33
if not WINDOWS:
34 35 36
    try:
        import curses
        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

A
PEP 257  
Alessio Sergi 已提交
45 46 47
    """This class manages the curses display (and key pressed).

    Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser.
48
    """
49

50 51 52 53 54
    _hotkeys = {
        '0': {'switch': 'disable_irix'},
        '1': {'switch': 'percpu'},
        '2': {'switch': 'disable_left_sidebar'},
        '3': {'switch': 'disable_quicklook'},
55
        '6': {'switch': 'meangpu'},
56 57 58 59
        '/': {'switch': 'process_short_name'},
        'A': {'switch': 'disable_amps'},
        'b': {'switch': 'byte'},
        'B': {'switch': 'diskio_iops'},
60
        'C': {'switch': 'disable_cloud'},
61
        'D': {'switch': 'disable_docker'},
62
        'd': {'switch': 'disable_diskio'},
63
        'F': {'switch': 'fs_free_space'},
64
        'g': {'switch': 'generate_graph'},
65
        'G': {'switch': 'disable_gpu'},
66 67 68 69 70
        'h': {'switch': 'help_tag'},
        'I': {'switch': 'disable_ip'},
        'l': {'switch': 'disable_alert'},
        'M': {'switch': 'reset_minmax_tag'},
        'n': {'switch': 'disable_network'},
N
nicolargo 已提交
71
        'N': {'switch': 'disable_now'},
72
        'P': {'switch': 'disable_ports'},
73
        'Q': {'switch': 'enable_irq'},
74 75 76 77 78 79 80 81 82 83 84 85
        'R': {'switch': 'disable_raid'},
        's': {'switch': 'disable_sensors'},
        'T': {'switch': 'network_sum'},
        'U': {'switch': 'network_cumul'},
        'W': {'switch': 'disable_wifi'},
        # Processes sort hotkeys
        'a': {'auto_sort': True, 'sort_key': 'cpu_percent'},
        'c': {'auto_sort': False, 'sort_key': 'cpu_percent'},
        'i': {'auto_sort': False, 'sort_key': 'io_counters'},
        'm': {'auto_sort': False, 'sort_key': 'memory_percent'},
        'p': {'auto_sort': False, 'sort_key': 'name'},
        't': {'auto_sort': False, 'sort_key': 'cpu_times'},
86
        'u': {'auto_sort': False, 'sort_key': 'username'},
87 88
    }

89 90 91
    _sort_loop = ['cpu_percent', 'memory_percent', 'username',
                  'cpu_times', 'io_counters', 'name']

N
nicolargo 已提交
92
    # Define top menu
N
nicolargo 已提交
93
    _top = ['quicklook', 'cpu', 'percpu', 'gpu', 'mem', 'memswap', 'load']
94
    _quicklook_max_width = 68
N
nicolargo 已提交
95 96

    # Define left sidebar
97 98 99
    _left_sidebar = ['network', 'wifi', 'ports', 'diskio', 'fs',
                     'irq', 'folders', 'raid', 'sensors', 'now']
    _left_sidebar_min_width = 23
100
    _left_sidebar_max_width = 34
101

N
nicolargo 已提交
102 103 104
    # Define right sidebar
    _right_sidebar = ['docker', 'processcount', 'amps', 'processlist', 'alert']

105 106 107
    def __init__(self, config=None, args=None):
        # Init
        self.config = config
108
        self.args = args
N
Nicolas Hennion 已提交
109

110 111 112 113 114 115 116 117 118 119 120
        # Init windows positions
        self.term_w = 80
        self.term_h = 24

        # Space between stats
        self.space_between_column = 3
        self.space_between_line = 2

        # Init the curses screen
        self.screen = curses.initscr()
        if not self.screen:
A
Alessio Sergi 已提交
121
            logger.critical("Cannot init the curses library.\n")
N
Nicolas Hennion 已提交
122
            sys.exit(1)
123

124 125 126 127
        # Load the 'outputs' section of the configuration file
        # - Init the theme (default is black)
        self.theme = {'name': 'black'}

128 129 130
        # Load configuration file
        self.load_config(config)

131 132 133 134 135 136 137 138 139 140 141 142
        # Init cursor
        self._init_cursor()

        # Init the colors
        self._init_colors()

        # Init main window
        self.term_window = self.screen.subwin(0, 0)

        # Init edit filter tag
        self.edit_filter = False

143 144 145
        # Init the process min/max reset
        self.args.reset_minmax_tag = False

146
        # Catch key pressed with non blocking mode
147
        self.term_window.keypad(1)
148 149 150 151 152 153
        self.term_window.nodelay(1)
        self.pressedkey = -1

        # History tag
        self._init_history()

154
    def load_config(self, config):
155
        """Load the outputs section of the configuration file."""
156
        # Load the theme
157
        if config is not None and config.has_section('outputs'):
158 159
            logger.debug('Read the outputs section in the configuration file')
            self.theme['name'] = config.get_value('outputs', 'curse_theme', default='black')
160
            logger.debug('Theme for the curse interface: {}'.format(self.theme['name']))
161 162

    def is_theme(self, name):
163
        """Return True if the theme *name* should be used."""
164 165
        return getattr(self.args, 'theme_' + name) or self.theme['name'] == name

166
    def _init_history(self):
167
        """Init the history option."""
168 169 170 171

        self.reset_history_tag = False

    def _init_cursor(self):
172
        """Init cursors."""
173

174 175 176 177
        if hasattr(curses, 'noecho'):
            curses.noecho()
        if hasattr(curses, 'cbreak'):
            curses.cbreak()
N
Nicolargo 已提交
178
        self.set_cursor(0)
179

180
    def _init_colors(self):
181
        """Init the Curses color layout."""
182 183

        # Set curses options
184 185 186 187 188 189 190
        try:
            if hasattr(curses, 'start_color'):
                curses.start_color()
            if hasattr(curses, 'use_default_colors'):
                curses.use_default_colors()
        except Exception as e:
            logger.warning('Error initializing terminal color ({})'.format(e))
191

192
        # Init colors
193 194
        if self.args.disable_bold:
            A_BOLD = 0
195
            self.args.disable_bg = True
196 197
        else:
            A_BOLD = curses.A_BOLD
198 199 200 201 202 203 204

        self.title_color = A_BOLD
        self.title_underline_color = A_BOLD | curses.A_UNDERLINE
        self.help_color = A_BOLD

        if curses.has_colors():
            # The screen is compatible with a colored design
205
            if self.is_theme('white'):
206
                # White theme: black ==> white
N
Nicolargo 已提交
207 208 209
                curses.init_pair(1, curses.COLOR_BLACK, -1)
            else:
                curses.init_pair(1, curses.COLOR_WHITE, -1)
210
            if self.args.disable_bg:
211 212 213 214
                curses.init_pair(2, curses.COLOR_RED, -1)
                curses.init_pair(3, curses.COLOR_GREEN, -1)
                curses.init_pair(4, curses.COLOR_BLUE, -1)
                curses.init_pair(5, curses.COLOR_MAGENTA, -1)
215 216 217 218 219
            else:
                curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED)
                curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_GREEN)
                curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE)
                curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
220 221 222 223 224
            curses.init_pair(6, curses.COLOR_RED, -1)
            curses.init_pair(7, curses.COLOR_GREEN, -1)
            curses.init_pair(8, curses.COLOR_BLUE, -1)

            # Colors text styles
225 226 227 228
            if curses.COLOR_PAIRS > 8:
                try:
                    curses.init_pair(9, curses.COLOR_MAGENTA, -1)
                except Exception:
229
                    if self.is_theme('white'):
230 231 232 233 234 235
                        curses.init_pair(9, curses.COLOR_BLACK, -1)
                    else:
                        curses.init_pair(9, curses.COLOR_WHITE, -1)
                try:
                    curses.init_pair(10, curses.COLOR_CYAN, -1)
                except Exception:
236
                    if self.is_theme('white'):
237 238 239 240
                        curses.init_pair(10, curses.COLOR_BLACK, -1)
                    else:
                        curses.init_pair(10, curses.COLOR_WHITE, -1)

241 242
                self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
                self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
243 244
                self.filter_color = curses.color_pair(10) | A_BOLD

245
            self.no_color = curses.color_pair(1)
A
Alessio Sergi 已提交
246
            self.default_color = curses.color_pair(3) | A_BOLD
247 248
            self.nice_color = curses.color_pair(9)
            self.cpu_time_color = curses.color_pair(9)
249 250 251
            self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD
            self.ifWARNING_color = curses.color_pair(5) | A_BOLD
            self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD
252
            self.default_color2 = curses.color_pair(7)
253
            self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
254

255
        else:
256 257
            # The screen is NOT compatible with a colored design
            # switch to B&W text styles
258 259
            self.no_color = curses.A_NORMAL
            self.default_color = curses.A_NORMAL
260
            self.nice_color = A_BOLD
261
            self.cpu_time_color = A_BOLD
262 263 264 265 266 267 268
            self.ifCAREFUL_color = curses.A_UNDERLINE
            self.ifWARNING_color = A_BOLD
            self.ifCRITICAL_color = curses.A_REVERSE
            self.default_color2 = curses.A_NORMAL
            self.ifCAREFUL_color2 = curses.A_UNDERLINE
            self.ifWARNING_color2 = A_BOLD
            self.ifCRITICAL_color2 = curses.A_REVERSE
N
Nicolargo 已提交
269
            self.filter_color = A_BOLD
270 271

        # Define the colors list (hash table) for stats
272
        self.colors_list = {
273 274 275
            'DEFAULT': self.no_color,
            'UNDERLINE': curses.A_UNDERLINE,
            'BOLD': A_BOLD,
276
            'SORT': A_BOLD,
277
            'OK': self.default_color2,
278
            'MAX': self.default_color2 | curses.A_BOLD,
N
Nicolargo 已提交
279
            'FILTER': self.filter_color,
280
            'TITLE': self.title_color,
281 282
            'PROCESS': self.default_color2,
            'STATUS': self.default_color2,
283
            'NICE': self.nice_color,
284
            'CPU_TIME': self.cpu_time_color,
285 286 287 288 289 290
            'CAREFUL': self.ifCAREFUL_color2,
            'WARNING': self.ifWARNING_color2,
            'CRITICAL': self.ifCRITICAL_color2,
            'OK_LOG': self.default_color,
            'CAREFUL_LOG': self.ifCAREFUL_color,
            'WARNING_LOG': self.ifWARNING_color,
291 292
            'CRITICAL_LOG': self.ifCRITICAL_color,
            'PASSWORD': curses.A_PROTECT
293 294
        }

N
Nicolargo 已提交
295
    def set_cursor(self, value):
A
PEP 257  
Alessio Sergi 已提交
296 297 298 299 300 301
        """Configure the curse cursor apparence.

        0: invisible
        1: visible
        2: very visible
        """
N
Nicolargo 已提交
302 303 304 305 306 307
        if hasattr(curses, 'curs_set'):
            try:
                curses.curs_set(value)
            except Exception:
                pass

308 309 310 311 312 313 314 315 316 317 318 319 320 321
    # def get_key(self, window):
    #     # Catch ESC key AND numlock key (issue #163)
    #     keycode = [0, 0]
    #     keycode[0] = window.getch()
    #     keycode[1] = window.getch()
    #
    #     if keycode != [-1, -1]:
    #         logger.debug("Keypressed (code: %s)" % keycode)
    #
    #     if keycode[0] == 27 and keycode[1] != -1:
    #         # Do not escape on specials keys
    #         return -1
    #     else:
    #         return keycode[0]
N
Nicolargo 已提交
322

323 324 325 326 327
    def get_key(self, window):
        # @TODO: Check issue #163
        ret = window.getch()
        logger.debug("Keypressed (code: %s)" % ret)
        return ret
328

N
Nicolargo 已提交
329
    def __catch_key(self, return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
330
        # Catch the pressed key
331
        self.pressedkey = self.get_key(self.term_window)
332

333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
        # Actions (available in the global hotkey dict)...
        for hotkey in self._hotkeys:
            if self.pressedkey == ord(hotkey) and 'switch' in self._hotkeys[hotkey]:
                setattr(self.args,
                        self._hotkeys[hotkey]['switch'],
                        not getattr(self.args,
                                    self._hotkeys[hotkey]['switch']))
            if self.pressedkey == ord(hotkey) and 'auto_sort' in self._hotkeys[hotkey]:
                setattr(glances_processes,
                        'auto_sort',
                        self._hotkeys[hotkey]['auto_sort'])
            if self.pressedkey == ord(hotkey) and 'sort_key' in self._hotkeys[hotkey]:
                setattr(glances_processes,
                        'sort_key',
                        self._hotkeys[hotkey]['sort_key'])

        # Other actions...
350 351
        if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
            # 'ESC'|'q' > Quit
N
Nicolargo 已提交
352 353 354
            if return_to_browser:
                logger.info("Stop Glances client and return to the browser")
            else:
355
                logger.info("Stop Glances (keypressed: {})".format(self.pressedkey))
356
        elif self.pressedkey == ord('\n'):
N
Nicolargo 已提交
357 358
            # 'ENTER' > Edit the process filter
            self.edit_filter = not self.edit_filter
N
nicolargo 已提交
359
        elif self.pressedkey == ord('4'):
N
nicolargo 已提交
360
            self.args.full_quicklook = not self.args.full_quicklook
N
nicolargo 已提交
361 362 363 364 365
            if self.args.full_quicklook:
                self.enable_fullquicklook()
            else:
                self.disable_fullquicklook()
        elif self.pressedkey == ord('5'):
N
nicolargo 已提交
366
            self.args.disable_top = not self.args.disable_top
N
nicolargo 已提交
367 368 369 370
            if self.args.disable_top:
                self.disable_top()
            else:
                self.enable_top()
371 372 373 374 375 376 377
        elif self.pressedkey == ord('e'):
            # 'e' > Enable/Disable process extended
            self.args.enable_process_extended = not self.args.enable_process_extended
            if not self.args.enable_process_extended:
                glances_processes.disable_extended()
            else:
                glances_processes.enable_extended()
378 379 380
        elif self.pressedkey == ord('E'):
            # 'E' > Erase the process filter
            glances_processes.process_filter = None
381 382 383
        elif self.pressedkey == ord('f'):
            # 'f' > Show/hide fs / folder stats
            self.args.disable_fs = not self.args.disable_fs
N
nicolargo 已提交
384
            self.args.disable_folders = not self.args.disable_folders
385 386
        elif self.pressedkey == ord('w'):
            # 'w' > Delete finished warning logs
387
            glances_events.clean()
388 389
        elif self.pressedkey == ord('x'):
            # 'x' > Delete finished warning and critical logs
390
            glances_events.clean(critical=True)
391
        elif self.pressedkey == ord('z'):
392
            # 'z' > Enable or disable processes
393 394 395 396 397
            self.args.disable_process = not self.args.disable_process
            if self.args.disable_process:
                glances_processes.disable()
            else:
                glances_processes.enable()
398 399 400 401 402 403 404 405 406 407
        elif self.pressedkey == curses.KEY_LEFT:
            # "<" (left arrow) navigation through process sort
            setattr(glances_processes, 'auto_sort', False)
            next_sort = (self.loop_position() - 1) % len(self._sort_loop)
            glances_processes.sort_key = self._sort_loop[next_sort]
        elif self.pressedkey == curses.KEY_RIGHT:
            # ">" (right arrow) navigation through process sort
            setattr(glances_processes, 'auto_sort', False)
            next_sort = (self.loop_position() + 1) % len(self._sort_loop)
            glances_processes.sort_key = self._sort_loop[next_sort]
408

409 410 411
        # Return the key code
        return self.pressedkey

412 413 414 415 416 417 418
    def loop_position(self):
        """Return the current sort in the loop"""
        for i, v in enumerate(self._sort_loop):
            if v == glances_processes.sort_key:
                return i
        return 0

419 420
    def disable_top(self):
        """Disable the top panel"""
421 422
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']:
            setattr(self.args, 'disable_' + p, True)
423 424 425

    def enable_top(self):
        """Enable the top panel"""
426 427
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']:
            setattr(self.args, 'disable_' + p, False)
428 429 430

    def disable_fullquicklook(self):
        """Disable the full quicklook mode"""
431 432
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap']:
            setattr(self.args, 'disable_' + p, False)
433 434 435 436

    def enable_fullquicklook(self):
        """Disable the full quicklook mode"""
        self.args.disable_quicklook = False
437 438
        for p in ['cpu', 'gpu', 'mem', 'memswap']:
            setattr(self.args, 'disable_' + p, True)
439

440
    def end(self):
441
        """Shutdown the curses window."""
N
Nicolas Hennion 已提交
442 443 444 445 446 447 448 449 450
        if hasattr(curses, 'echo'):
            curses.echo()
        if hasattr(curses, 'nocbreak'):
            curses.nocbreak()
        if hasattr(curses, 'curs_set'):
            try:
                curses.curs_set(1)
            except Exception:
                pass
451
        curses.endwin()
452

453
    def init_line_column(self):
454
        """Init the line and column position for the curses interface."""
455 456
        self.init_line()
        self.init_column()
457 458

    def init_line(self):
459
        """Init the line position for the curses interface."""
460 461 462 463
        self.line = 0
        self.next_line = 0

    def init_column(self):
464
        """Init the column position for the curses interface."""
465 466 467 468
        self.column = 0
        self.next_column = 0

    def new_line(self):
A
PEP 257  
Alessio Sergi 已提交
469
        """New line in the curses interface."""
470 471 472
        self.line = self.next_line

    def new_column(self):
A
PEP 257  
Alessio Sergi 已提交
473
        """New column in the curses interface."""
474 475
        self.column = self.next_column

476 477 478 479 480 481 482 483
    def __get_stat_display(self, stats, layer):
        """Return a dict of dict with all the stats display.
        stats: Global stats dict
        layer: ~ cs_status
            "None": standalone or server mode
            "Connected": Client is connected to a Glances server
            "SNMP": Client is connected to a SNMP server
            "Disconnected": Client is disconnected from the server
484 485

        :returns: dict of dict
486 487
            * key: plugin name
            * value: dict returned by the get_stats_display Plugin method
488
        """
N
nicolargo 已提交
489
        ret = {}
490

N
nicolargo 已提交
491
        for p in stats.getPluginsList(enable=False):
492 493 494
            if p == 'quicklook' or p == 'processlist':
                # processlist is done later
                # because we need to know how many processes could be displayed
495
                continue
496 497 498 499 500 501 502 503 504 505 506 507 508

            # Compute the plugin max size
            plugin_max_width = None
            if p in self._left_sidebar:
                plugin_max_width = max(self._left_sidebar_min_width,
                                       self.screen.getmaxyx()[1] - 105)
                plugin_max_width = min(self._left_sidebar_max_width,
                                       plugin_max_width)

            # Get the view
            ret[p] = stats.get_plugin(p).get_stats_display(args=self.args,
                                                           max_width=plugin_max_width)

N
nicolargo 已提交
509 510
        return ret

511
    def display(self, stats, cs_status=None):
A
PEP 257  
Alessio Sergi 已提交
512
        """Display stats on the screen.
513

514 515 516
        stats: Stats database to display
        cs_status:
            "None": standalone or server mode
517 518
            "Connected": Client is connected to a Glances server
            "SNMP": Client is connected to a SNMP server
519
            "Disconnected": Client is disconnected from the server
520 521 522 523

        Return:
            True if the stats have been displayed
            False if the help have been displayed
524
        """
525 526
        # Init the internal line/column for Glances Curses
        self.init_line_column()
527

528 529 530
        # Update the stats messages
        ###########################

531
        # Get all the plugins but quicklook and proceslist
532
        self.args.cs_status = cs_status
533
        __stat_display = self.__get_stat_display(stats, layer=cs_status)
534

535
        # Adapt number of processes to the available space
536 537
        max_processes_displayed = (
            self.screen.getmaxyx()[0] - 11 -
538
            (0 if 'docker' not in __stat_display else
N
nicolargo 已提交
539 540 541 542 543 544 545
                self.get_stats_display_height(__stat_display["docker"])) -
            (0 if 'processcount' not in __stat_display else
                self.get_stats_display_height(__stat_display["processcount"])) -
            (0 if 'amps' not in __stat_display else
                self.get_stats_display_height(__stat_display["amps"])) -
            (0 if 'alert' not in __stat_display else
                self.get_stats_display_height(__stat_display["alert"])))
546

547
        try:
N
nicolargo 已提交
548
            if self.args.enable_process_extended:
549 550 551
                max_processes_displayed -= 4
        except AttributeError:
            pass
552
        if max_processes_displayed < 0:
553
            max_processes_displayed = 0
554 555
        if (glances_processes.max_processes is None or
                glances_processes.max_processes != max_processes_displayed):
556
            logger.debug("Set number of displayed processes to {}".format(max_processes_displayed))
557
            glances_processes.max_processes = max_processes_displayed
558

559
        # Get the processlist
N
nicolargo 已提交
560
        __stat_display["processlist"] = stats.get_plugin(
561
            'processlist').get_stats_display(args=self.args)
562

563 564 565 566
        # Display the stats on the curses interface
        ###########################################

        # Help screen (on top of the other stats)
567
        if self.args.help_tag:
568
            # Display the stats...
569 570
            self.display_plugin(
                stats.get_plugin('help').get_stats_display(args=self.args))
571 572 573
            # ... and exit
            return False

574 575
        # =====================================
        # Display first line (system+ip+uptime)
576
        # Optionnaly: Cloud on second line
577
        # =====================================
N
nicolargo 已提交
578
        self.__display_header(__stat_display)
579 580 581 582

        # ==============================================================
        # Display second line (<SUMMARY>+CPU|PERCPU+<GPU>+LOAD+MEM+SWAP)
        # ==============================================================
N
nicolargo 已提交
583
        self.__display_top(__stat_display, stats)
584 585 586 587 588 589 590 591 592 593 594

        # ==================================================================
        # Display left sidebar (NETWORK+PORTS+DISKIO+FS+SENSORS+Current time)
        # ==================================================================
        self.__display_left(__stat_display)

        # ====================================
        # Display right stats (process and co)
        # ====================================
        self.__display_right(__stat_display)

595 596 597 598
        # =====================
        # Others popup messages
        # =====================

599 600 601 602 603 604 605 606
        # Display edit filter popup
        # Only in standalone mode (cs_status is None)
        if self.edit_filter and cs_status is None:
            new_filter = self.display_popup(
                'Process filter pattern: \n\n' +
                'Examples:\n' +
                '- python\n' +
                '- .*python.*\n' +
N
nicolargo 已提交
607
                '- /usr/lib.*\n' +
608 609 610 611 612 613 614 615 616 617 618
                '- name:.*nautilus.*\n' +
                '- cmdline:.*glances.*\n' +
                '- username:nicolargo\n' +
                '- username:^root        ',
                is_input=True,
                input_value=glances_processes.process_filter_input)
            glances_processes.process_filter = new_filter
        elif self.edit_filter and cs_status is not None:
            self.display_popup('Process filter only available in standalone mode')
        self.edit_filter = False

619 620 621 622
        # Display graph generation popup
        if self.args.generate_graph:
            self.display_popup('Generate graph in {}'.format(self.args.export_graph_path))

623 624
        return True

N
nicolargo 已提交
625 626
    def __display_header(self, stat_display):
        """Display the firsts lines (header) in the Curses interface.
627 628

        system + ip + uptime
N
nicolargo 已提交
629
        (cloud)
630
        """
N
nicolargo 已提交
631
        # First line
632
        self.new_line()
N
nicolargo 已提交
633
        self.space_between_column = 0
634
        l_uptime = (self.get_stats_display_width(stat_display["system"]) +
N
nicolargo 已提交
635 636
                    self.get_stats_display_width(stat_display["ip"]) +
                    self.get_stats_display_width(stat_display["uptime"]) + 1)
637
        self.display_plugin(
638 639
            stat_display["system"],
            display_optional=(self.screen.getmaxyx()[1] >= l_uptime))
N
nicolargo 已提交
640
        self.space_between_column = 3
641
        self.new_column()
642
        self.display_plugin(stat_display["ip"])
643
        self.new_column()
N
nicolargo 已提交
644 645
        self.display_plugin(
            stat_display["uptime"],
N
nicolargo 已提交
646
            add_space=-(self.get_stats_display_width(stat_display["cloud"]) != 0))
N
nicolargo 已提交
647
        # Second line (optional)
648 649 650
        self.init_column()
        self.new_line()
        self.display_plugin(stat_display["cloud"])
A
Alessio Sergi 已提交
651

N
nicolargo 已提交
652
    def __display_top(self, stat_display, stats):
653 654 655 656
        """Display the second line in the Curses interface.

        <QUICKLOOK> + CPU|PERCPU + <GPU> + MEM + SWAP + LOAD
        """
657
        self.init_column()
658
        self.new_line()
659

N
Nicolargo 已提交
660
        # Init quicklook
661
        stat_display['quicklook'] = {'msgdict': []}
662

663
        # Dict for plugins width
N
nicolargo 已提交
664 665 666
        plugin_widths = {}
        for p in self._top:
            plugin_widths[p] = self.get_stats_display_width(stat_display.get(p, 0)) if hasattr(self.args, 'disable_' + p) else 0
667

668 669
        # Width of all plugins
        stats_width = sum(itervalues(plugin_widths))
670 671

        # Number of plugin but quicklook
N
nicolargo 已提交
672
        stats_number = sum([int(stat_display[p]['msgdict'] != []) for p in self._top if not getattr(self.args, 'disable_' + p)])
673 674 675

        if not self.args.disable_quicklook:
            # Quick look is in the place !
676
            if self.args.full_quicklook:
677
                quicklook_width = self.screen.getmaxyx()[1] - (stats_width + 8 + stats_number * self.space_between_column)
678
            else:
N
nicolargo 已提交
679
                quicklook_width = min(self.screen.getmaxyx()[1] - (stats_width + 8 + stats_number * self.space_between_column),
680
                                      self._quicklook_max_width - 5)
N
Nicolargo 已提交
681
            try:
682
                stat_display["quicklook"] = stats.get_plugin(
N
Nicolargo 已提交
683 684 685 686
                    'quicklook').get_stats_display(max_width=quicklook_width, args=self.args)
            except AttributeError as e:
                logger.debug("Quicklook plugin not available (%s)" % e)
            else:
687 688
                plugin_widths['quicklook'] = self.get_stats_display_width(stat_display["quicklook"])
                stats_width = sum(itervalues(plugin_widths)) + 1
689
            self.space_between_column = 1
690
            self.display_plugin(stat_display["quicklook"])
691 692 693 694
            self.new_column()

        # Compute spaces between plugins
        # Note: Only one space between Quicklook and others
695
        plugin_display_optional = {}
N
nicolargo 已提交
696
        for p in self._top:
697
            plugin_display_optional[p] = True
698
        if stats_number > 1:
699
            self.space_between_column = max(1, int((self.screen.getmaxyx()[1] - stats_width) / (stats_number - 1)))
700 701 702 703 704 705 706
            for p in ['mem', 'cpu']:
                # No space ? Remove optional stats
                if self.space_between_column < 3:
                    plugin_display_optional[p] = False
                    plugin_widths[p] = self.get_stats_display_width(stat_display[p], without_option=True) if hasattr(self.args, 'disable_' + p) else 0
                    stats_width = sum(itervalues(plugin_widths)) + 1
                    self.space_between_column = max(1, int((self.screen.getmaxyx()[1] - stats_width) / (stats_number - 1)))
707 708 709 710
        else:
            self.space_between_column = 0

        # Display CPU, MEM, SWAP and LOAD
N
nicolargo 已提交
711 712 713
        for p in self._top:
            if p == 'quicklook':
                continue
N
nicolargo 已提交
714 715 716
            if p in stat_display:
                self.display_plugin(stat_display[p],
                                    display_optional=plugin_display_optional[p])
717 718 719
            if p is not 'load':
                # Skip last column
                self.new_column()
720

721 722 723
        # Space between column
        self.space_between_column = 3

724 725 726
        # Backup line position
        self.saved_line = self.next_line

727
    def __display_left(self, stat_display):
728
        """Display the left sidebar in the Curses interface."""
729
        self.init_column()
N
nicolargo 已提交
730 731 732 733 734 735 736 737 738

        if self.args.disable_left_sidebar:
            return

        for s in self._left_sidebar:
            if ((hasattr(self.args, 'enable_' + s) or
                 hasattr(self.args, 'disable_' + s)) and s in stat_display):
                self.new_line()
                self.display_plugin(stat_display[s])
739

740 741 742 743 744
    def __display_right(self, stat_display):
        """Display the right sidebar in the Curses interface.

        docker + processcount + amps + processlist + alert
        """
N
nicolargo 已提交
745 746 747
        # Do not display anything if space is not available...
        if self.screen.getmaxyx()[1] < self._left_sidebar_min_width:
            return
748

N
nicolargo 已提交
749 750 751 752 753 754
        # Restore line position
        self.next_line = self.saved_line

        # Display right sidebar
        self.new_column()
        for p in self._right_sidebar:
755
            self.new_line()
N
nicolargo 已提交
756 757 758 759 760 761 762
            if p == 'processlist':
                self.display_plugin(stat_display['processlist'],
                                    display_optional=(self.screen.getmaxyx()[1] > 102),
                                    display_additional=(not MACOS),
                                    max_y=(self.screen.getmaxyx()[0] - self.get_stats_display_height(stat_display['alert']) - 2))
            else:
                self.display_plugin(stat_display[p])
763

764 765
    def display_popup(self, message,
                      size_x=None, size_y=None,
N
Nicolargo 已提交
766 767
                      duration=3,
                      is_input=False,
N
Nicolargo 已提交
768
                      input_size=30,
N
Nicolargo 已提交
769
                      input_value=None):
770
        """
A
PEP 257  
Alessio Sergi 已提交
771 772
        Display a centered popup.

N
Nicolargo 已提交
773 774 775 776 777
        If is_input is False:
         Display a centered popup with the given message during duration seconds
         If size_x and size_y: set the popup size
         else set it automatically
         Return True if the popup could be displayed
A
PEP 257  
Alessio Sergi 已提交
778

N
Nicolargo 已提交
779 780 781 782
        If is_input is True:
         Display a centered popup with the given message and a input field
         If size_x and size_y: set the popup size
         else set it automatically
783
         Return the input string or None if the field is empty
784 785
        """
        # Center the popup
N
Nicolargo 已提交
786
        sentence_list = message.split('\n')
787
        if size_x is None:
N
Nicolargo 已提交
788
            size_x = len(max(sentence_list, key=len)) + 4
N
Nicolargo 已提交
789 790 791
            # Add space for the input field
            if is_input:
                size_x += input_size
792
        if size_y is None:
N
Nicolargo 已提交
793
            size_y = len(sentence_list) + 4
794 795 796 797
        screen_x = self.screen.getmaxyx()[1]
        screen_y = self.screen.getmaxyx()[0]
        if size_x > screen_x or size_y > screen_y:
            # No size to display the popup => abord
798
            return False
799 800
        pos_x = int((screen_x - size_x) / 2)
        pos_y = int((screen_y - size_y) / 2)
801 802 803

        # Create the popup
        popup = curses.newwin(size_y, size_x, pos_y, pos_x)
804

805 806 807 808
        # Fill the popup
        popup.border()

        # Add the message
N
nicolargo 已提交
809
        for y, m in enumerate(message.split('\n')):
810 811
            popup.addnstr(2 + y, 2, m, len(m))

A
Alessio Sergi 已提交
812
        if is_input and not WINDOWS:
N
Nicolargo 已提交
813 814
            # Create a subwindow for the text field
            subpop = popup.derwin(1, input_size, 2, 2 + len(m))
815
            subpop.attron(self.colors_list['FILTER'])
N
Nicolargo 已提交
816 817 818 819 820 821 822 823
            # Init the field with the current value
            if input_value is not None:
                subpop.addnstr(0, 0, input_value, len(input_value))
            # Display the popup
            popup.refresh()
            subpop.refresh()
            # Create the textbox inside the subwindows
            self.set_cursor(2)
824
            self.term_window.keypad(1)
A
Alessio Sergi 已提交
825
            textbox = GlancesTextbox(subpop, insert_mode=False)
N
Nicolargo 已提交
826 827
            textbox.edit()
            self.set_cursor(0)
828
            self.term_window.keypad(0)
N
Nicolargo 已提交
829
            if textbox.gather() != '':
N
Nicolargo 已提交
830
                logger.debug(
831
                    "User enters the following string: %s" % textbox.gather())
N
Nicolargo 已提交
832 833
                return textbox.gather()[:-1]
            else:
834
                logger.debug("User centers an empty string")
N
Nicolargo 已提交
835 836 837 838
                return None
        else:
            # Display the popup
            popup.refresh()
839
            self.wait(duration * 1000)
N
Nicolargo 已提交
840
            return True
841

842
    def display_plugin(self, plugin_stats,
843
                       display_optional=True,
844
                       display_additional=True,
845
                       max_y=65535,
N
nicolargo 已提交
846
                       add_space=0):
A
PEP 257  
Alessio Sergi 已提交
847 848
        """Display the plugin_stats on the screen.

849 850
        If display_optional=True display the optional stats
        If display_additional=True display additionnal stats
N
nicolargo 已提交
851 852
        max_y: do not display line > max_y
        add_space: add x space (line) after the plugin
853
        """
854 855 856
        # Exit if:
        # - the plugin_stats message is empty
        # - the display tag = False
857
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
858
            # Exit
859 860 861 862 863 864 865
            return 0

        # Get the screen size
        screen_x = self.screen.getmaxyx()[1]
        screen_y = self.screen.getmaxyx()[0]

        # Set the upper/left position of the message
866
        if plugin_stats['align'] == 'right':
867
            # Right align (last column)
868
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
869
        else:
870
            display_x = self.column
871
        if plugin_stats['align'] == 'bottom':
872
            # Bottom (last line)
873
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
874
        else:
875
            display_y = self.line
876

877 878
        # Display
        x = display_x
879
        x_max = x
880 881 882
        y = display_y
        for m in plugin_stats['msgdict']:
            # New line
883
            if m['msg'].startswith('\n'):
884
                # Go to the next line
885
                y += 1
886 887 888 889
                # Return to the first column
                x = display_x
                continue
            # Do not display outside the screen
890
            if x < 0:
891
                continue
892
            if not m['splittable'] and (x + len(m['msg']) > screen_x):
893
                continue
894
            if y < 0 or (y + 1 > screen_y) or (y > max_y):
895 896
                break
            # If display_optional = False do not display optional stats
897
            if not display_optional and m['optional']:
898
                continue
899 900 901
            # If display_additional = False do not display additional stats
            if not display_additional and m['additional']:
                continue
902 903 904
            # Is it possible to display the stat with the current screen size
            # !!! Crach if not try/except... Why ???
            try:
A
Alessio Sergi 已提交
905 906
                self.term_window.addnstr(y, x,
                                         m['msg'],
907 908
                                         # Do not disply outside the screen
                                         screen_x - x,
909
                                         self.colors_list[m['decoration']])
A
Alessio Sergi 已提交
910
            except Exception:
911 912 913
                pass
            else:
                # New column
A
Alessio Sergi 已提交
914 915 916 917
                # Python 2: we need to decode to get real screen size because
                # UTF-8 special tree chars occupy several bytes.
                # Python 3: strings are strings and bytes are bytes, all is
                # good.
N
Nicolargo 已提交
918 919 920 921 922
                try:
                    x += len(u(m['msg']))
                except UnicodeDecodeError:
                    # Quick and dirty hack for issue #745
                    pass
923 924
                if x > x_max:
                    x_max = x
925 926

        # Compute the next Glances column/line position
N
Nicolargo 已提交
927 928
        self.next_column = max(
            self.next_column, x_max + self.space_between_column)
929
        self.next_line = max(self.next_line, y + self.space_between_line)
930

N
nicolargo 已提交
931 932
        # Have empty lines after the plugins
        self.next_line += add_space
933

934
    def erase(self):
A
PEP 257  
Alessio Sergi 已提交
935
        """Erase the content of the screen."""
936 937
        self.term_window.erase()

938
    def flush(self, stats, cs_status=None):
A
PEP 257  
Alessio Sergi 已提交
939 940
        """Clear and update the screen.

941 942 943 944 945 946 947
        stats: Stats database to display
        cs_status:
            "None": standalone or server mode
            "Connected": Client is connected to the server
            "Disconnected": Client is disconnected from the server
        """
        self.erase()
948
        self.display(stats, cs_status=cs_status)
949

950 951 952 953 954
    def update(self,
               stats,
               duration=3,
               cs_status=None,
               return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
955 956
        """Update the screen.

N
Nicolargo 已提交
957
        INPUT
958
        stats: Stats database to display
959
        duration: duration of the loop
960 961 962 963
        cs_status:
            "None": standalone or server mode
            "Connected": Client is connected to the server
            "Disconnected": Client is disconnected from the server
N
Nicolargo 已提交
964 965 966 967
        return_to_browser:
            True: Do not exist, return to the browser list
            False: Exit and return to the shell

968
        OUTPUT
N
Nicolargo 已提交
969 970
        True: Exit key has been pressed
        False: Others cases...
971 972
        """
        # Flush display
973
        self.flush(stats, cs_status=cs_status)
974

975 976 977
        # If the duration is < 0 (update + export time > refresh_time)
        # Then display the interface and log a message
        if duration <= 0:
978
            logger.warning('Update and export time higher than refresh_time.')
979 980
            duration = 0.1

981
        # Wait duration (in s) time
N
Nicolargo 已提交
982
        exitkey = False
983
        countdown = Timer(duration)
984 985
        # Set the default timeout (in ms) for the getch method
        self.term_window.timeout(int(duration * 1000))
N
Nicolargo 已提交
986
        while not countdown.finished() and not exitkey:
987
            # Getkey
N
Nicolargo 已提交
988 989 990 991
            pressedkey = self.__catch_key(return_to_browser=return_to_browser)
            # Is it an exit key ?
            exitkey = (pressedkey == ord('\x1b') or pressedkey == ord('q'))
            if not exitkey and pressedkey > -1:
992
                # Redraw display
993
                self.flush(stats, cs_status=cs_status)
N
nicolargo 已提交
994 995
                # Overwrite the timeout with the countdown
                self.term_window.timeout(int(countdown.get() * 1000))
996

N
Nicolargo 已提交
997 998
        return exitkey

999 1000 1001 1002
    def wait(self, delay=100):
        """Wait delay in ms"""
        curses.napms(100)

1003
    def get_stats_display_width(self, curse_msg, without_option=False):
N
nicolargo 已提交
1004
        """Return the width of the formatted curses message."""
1005
        try:
1006
            if without_option:
1007
                # Size without options
N
nicolargo 已提交
1008
                c = len(max(''.join([(i['msg'].decode('utf-8').encode('ascii', 'replace') if not i['optional'] else "")
1009
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
1010 1011
            else:
                # Size with all options
N
nicolargo 已提交
1012
                c = len(max(''.join([i['msg'].decode('utf-8').encode('ascii', 'replace')
1013
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
A
Alessio Sergi 已提交
1014
        except Exception:
1015 1016 1017 1018
            return 0
        else:
            return c

1019
    def get_stats_display_height(self, curse_msg):
A
PEP 257  
Alessio Sergi 已提交
1020
        r"""Return the height of the formatted curses message.
1021

A
PEP 257  
Alessio Sergi 已提交
1022 1023
        The height is defined by the number of '\n' (new line).
        """
1024
        try:
A
Alessio Sergi 已提交
1025
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
A
Alessio Sergi 已提交
1026
        except Exception:
1027 1028 1029
            return 0
        else:
            return c + 1
N
Nicolargo 已提交
1030

1031

1032 1033
class GlancesCursesStandalone(_GlancesCurses):

A
PEP 257  
Alessio Sergi 已提交
1034
    """Class for the Glances curse standalone."""
1035 1036 1037 1038

    pass


1039 1040
class GlancesCursesClient(_GlancesCurses):

A
PEP 257  
Alessio Sergi 已提交
1041
    """Class for the Glances curse client."""
1042 1043 1044 1045

    pass


A
Alessio Sergi 已提交
1046
if not WINDOWS:
1047
    class GlancesTextbox(Textbox, object):
1048

1049 1050
        def __init__(self, *args, **kwargs):
            super(GlancesTextbox, self).__init__(*args, **kwargs)
1051

N
Nicolargo 已提交
1052
        def do_command(self, ch):
1053
            if ch == 10:  # Enter
N
Nicolargo 已提交
1054
                return 0
1055
            if ch == 127:  # Back
N
Nicolargo 已提交
1056
                return 8
1057
            return super(GlancesTextbox, self).do_command(ch)