glances_curses.py 41.7 KB
Newer Older
1 2
# -*- coding: utf-8 -*-
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2016 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
A
Alessio Sergi 已提交
26
from glances.globals import OSX, WINDOWS
27 28 29 30
from glances.logger import logger
from glances.logs import glances_logs
from glances.processes import glances_processes
from glances.timer import Timer
31 32

# Import curses lib for "normal" operating system and consolelog for Windows
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:
N
Nicolargo 已提交
39 40
        logger.critical(
            "Curses module not found. Glances cannot start in standalone mode.")
41 42
        sys.exit(1)
else:
43 44
    from glances.outputs.glances_colorconsole import WCurseLight
    curses = WCurseLight()
45 46


47
class _GlancesCurses(object):
48

A
PEP 257  
Alessio Sergi 已提交
49 50 51
    """This class manages the curses display (and key pressed).

    Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser.
52
    """
53

54 55 56
    def __init__(self, config=None, args=None):
        # Init
        self.config = config
57
        self.args = args
N
Nicolas Hennion 已提交
58

59 60 61 62 63 64 65 66 67 68 69
        # 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 已提交
70
            logger.critical("Cannot init the curses library.\n")
N
Nicolas Hennion 已提交
71
            sys.exit(1)
72

73 74 75 76 77
        # Load the 'outputs' section of the configuration file
        # - Init the theme (default is black)
        self.theme = {'name': 'black'}
        self.load_config(self.config)

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
        # Init cursor
        self._init_cursor()

        # Init the colors
        self._init_colors()

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

        # Init refresh time
        self.__refresh_time = args.time

        # Init edit filter tag
        self.edit_filter = False

93 94 95
        # Init the process min/max reset
        self.args.reset_minmax_tag = False

96 97 98 99 100 101 102 103
        # Catch key pressed with non blocking mode
        self.no_flash_cursor()
        self.term_window.nodelay(1)
        self.pressedkey = -1

        # History tag
        self._init_history()

104 105 106 107 108 109 110 111 112 113 114 115
    def load_config(self, config):
        '''Load the outputs section of the configuration file'''
        # Load the theme
        if config.has_section('outputs'):
            logger.debug('Read the outputs section in the configuration file')
            self.theme['name'] = config.get_value('outputs', 'curse_theme', default='black')
            logger.debug('Theme for the curse interface: {0}'.format(self.theme['name']))

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

116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
    def _init_history(self):
        '''Init the history option'''

        self.reset_history_tag = False
        self.history_tag = False
        if self.args.enable_history:
            logger.info('Stats history enabled with output path %s' %
                        self.args.path_history)
            from glances.exports.glances_history import GlancesHistory
            self.glances_history = GlancesHistory(self.args.path_history)
            if not self.glances_history.graph_enabled():
                self.args.enable_history = False
                logger.error(
                    'Stats history disabled because MatPlotLib is not installed')

    def _init_cursor(self):
        '''Init cursors'''

134 135 136 137
        if hasattr(curses, 'noecho'):
            curses.noecho()
        if hasattr(curses, 'cbreak'):
            curses.cbreak()
N
Nicolargo 已提交
138
        self.set_cursor(0)
139

140 141 142 143 144 145 146 147 148
    def _init_colors(self):
        '''Init the Curses color layout'''

        # Set curses options
        if hasattr(curses, 'start_color'):
            curses.start_color()
        if hasattr(curses, 'use_default_colors'):
            curses.use_default_colors()

149
        # Init colors
150 151
        if self.args.disable_bold:
            A_BOLD = 0
152
            self.args.disable_bg = True
153 154
        else:
            A_BOLD = curses.A_BOLD
155 156 157 158 159 160 161

        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
162
            if self.is_theme('white'):
163
                # White theme: black ==> white
N
Nicolargo 已提交
164 165 166
                curses.init_pair(1, curses.COLOR_BLACK, -1)
            else:
                curses.init_pair(1, curses.COLOR_WHITE, -1)
167
            if self.args.disable_bg:
168 169 170 171
                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)
172 173 174 175 176
            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)
177 178 179 180 181
            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
182 183 184 185
            if curses.COLOR_PAIRS > 8:
                try:
                    curses.init_pair(9, curses.COLOR_MAGENTA, -1)
                except Exception:
186
                    if self.is_theme('white'):
187 188 189 190 191 192
                        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:
193
                    if self.is_theme('white'):
194 195 196 197 198 199 200 201
                        curses.init_pair(10, curses.COLOR_BLACK, -1)
                    else:
                        curses.init_pair(10, curses.COLOR_WHITE, -1)

                self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
                self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
                self.filter_color = curses.color_pair(10) | A_BOLD

202
            self.no_color = curses.color_pair(1)
A
Alessio Sergi 已提交
203
            self.default_color = curses.color_pair(3) | A_BOLD
204
            self.nice_color = curses.color_pair(9) | A_BOLD
205
            self.cpu_time_color = curses.color_pair(9) | A_BOLD
206 207 208 209 210
            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
            self.default_color2 = curses.color_pair(7) | A_BOLD
            self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
211

212
        else:
213 214
            # The screen is NOT compatible with a colored design
            # switch to B&W text styles
215 216
            self.no_color = curses.A_NORMAL
            self.default_color = curses.A_NORMAL
217
            self.nice_color = A_BOLD
218
            self.cpu_time_color = A_BOLD
219 220 221 222 223 224 225
            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 已提交
226
            self.filter_color = A_BOLD
227 228

        # Define the colors list (hash table) for stats
229
        self.colors_list = {
230 231 232
            'DEFAULT': self.no_color,
            'UNDERLINE': curses.A_UNDERLINE,
            'BOLD': A_BOLD,
233
            'SORT': A_BOLD,
234
            'OK': self.default_color2,
N
Nicolargo 已提交
235
            'FILTER': self.filter_color,
236
            'TITLE': self.title_color,
237 238
            'PROCESS': self.default_color2,
            'STATUS': self.default_color2,
239
            'NICE': self.nice_color,
240
            'CPU_TIME': self.cpu_time_color,
241 242 243 244 245 246
            '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,
247 248
            'CRITICAL_LOG': self.ifCRITICAL_color,
            'PASSWORD': curses.A_PROTECT
249 250
        }

251 252 253 254 255 256
    def flash_cursor(self):
        self.term_window.keypad(1)

    def no_flash_cursor(self):
        self.term_window.keypad(0)

N
Nicolargo 已提交
257
    def set_cursor(self, value):
A
PEP 257  
Alessio Sergi 已提交
258 259 260 261 262 263
        """Configure the curse cursor apparence.

        0: invisible
        1: visible
        2: very visible
        """
N
Nicolargo 已提交
264 265 266 267 268 269
        if hasattr(curses, 'curs_set'):
            try:
                curses.curs_set(value)
            except Exception:
                pass

270
    def get_key(self, window):
A
PEP 257  
Alessio Sergi 已提交
271
        # Catch ESC key AND numlock key (issue #163)
272 273 274 275
        keycode = [0, 0]
        keycode[0] = window.getch()
        keycode[1] = window.getch()

N
Nicolargo 已提交
276
        if keycode != [-1, -1]:
N
Nicolargo 已提交
277
            logger.debug("Keypressed (code: %s)" % keycode)
N
Nicolargo 已提交
278

279 280 281 282 283 284
        if keycode[0] == 27 and keycode[1] != -1:
            # Do not escape on specials keys
            return -1
        else:
            return keycode[0]

N
Nicolargo 已提交
285
    def __catch_key(self, return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
286
        # Catch the pressed key
287
        self.pressedkey = self.get_key(self.term_window)
288

289
        # Actions...
290 291
        if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
            # 'ESC'|'q' > Quit
N
Nicolargo 已提交
292 293 294 295 296 297
            if return_to_browser:
                logger.info("Stop Glances client and return to the browser")
            else:
                self.end()
                logger.info("Stop Glances")
                sys.exit(0)
N
Nicolargo 已提交
298 299 300
        elif self.pressedkey == 10:
            # 'ENTER' > Edit the process filter
            self.edit_filter = not self.edit_filter
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
        elif self.pressedkey == ord('0'):
            # '0' > Switch between IRIX and Solaris mode
            self.args.disable_irix = not self.args.disable_irix
        elif self.pressedkey == ord('1'):
            # '1' > Switch between CPU and PerCPU information
            self.args.percpu = not self.args.percpu
        elif self.pressedkey == ord('2'):
            # '2' > Enable/disable left sidebar
            self.args.disable_left_sidebar = not self.args.disable_left_sidebar
        elif self.pressedkey == ord('3'):
            # '3' > Enable/disable quicklook
            self.args.disable_quicklook = not self.args.disable_quicklook
        elif self.pressedkey == ord('4'):
            # '4' > Enable/disable all but quick look and load
            self.args.full_quicklook = not self.args.full_quicklook
            if self.args.full_quicklook:
                self.args.disable_quicklook = False
                self.args.disable_cpu = True
                self.args.disable_mem = True
                self.args.disable_swap = True
            else:
                self.args.disable_quicklook = False
                self.args.disable_cpu = False
                self.args.disable_mem = False
                self.args.disable_swap = False
        elif self.pressedkey == ord('5'):
            # '5' > Enable/disable top menu
            logger.info(self.args.disable_top)
            self.args.disable_top = not self.args.disable_top
            if self.args.disable_top:
                self.args.disable_quicklook = True
                self.args.disable_cpu = True
                self.args.disable_mem = True
                self.args.disable_swap = True
                self.args.disable_load = True
            else:
                self.args.disable_quicklook = False
                self.args.disable_cpu = False
                self.args.disable_mem = False
                self.args.disable_swap = False
                self.args.disable_load = False
        elif self.pressedkey == ord('/'):
            # '/' > Switch between short/long name for processes
            self.args.process_short_name = not self.args.process_short_name
345
        elif self.pressedkey == ord('a'):
346 347 348
            # 'a' > Sort processes automatically and reset to 'cpu_percent'
            glances_processes.auto_sort = True
            glances_processes.sort_key = 'cpu_percent'
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
        elif self.pressedkey == ord('A'):
            # 'A' > enable/disable AMP module
            self.args.disable_amps = not self.args.disable_amps
        elif self.pressedkey == ord('b'):
            # 'b' > Switch between bit/s and Byte/s for network IO
            self.args.byte = not self.args.byte
        elif self.pressedkey == ord('B'):
            # 'B' > Switch between bit/s and IO/s for Disk IO
            self.args.diskio_iops = not self.args.diskio_iops
        elif self.pressedkey == ord('c'):
            # 'c' > Sort processes by CPU usage
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'cpu_percent'
        elif self.pressedkey == ord('d'):
            # 'd' > Show/hide disk I/O stats
            self.args.disable_diskio = not self.args.disable_diskio
        elif self.pressedkey == ord('D'):
            # 'D' > Show/hide Docker stats
            self.args.disable_docker = not self.args.disable_docker
        elif self.pressedkey == ord('e'):
            # 'e' > Enable/Disable extended stats for top process
            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()
375 376 377 378
        elif self.pressedkey == ord('E'):
            # 'E' > Erase the process filter
            logger.info("Erase process filter")
            glances_processes.process_filter = None
379 380 381 382 383 384 385
        elif self.pressedkey == ord('F'):
            # 'F' > Switch between FS available and free space
            self.args.fs_free_space = not self.args.fs_free_space
        elif self.pressedkey == ord('f'):
            # 'f' > Show/hide fs / folder stats
            self.args.disable_fs = not self.args.disable_fs
            self.args.disable_folder = not self.args.disable_folder
386 387 388
        elif self.pressedkey == ord('g'):
            # 'g' > History
            self.history_tag = not self.history_tag
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
        elif self.pressedkey == ord('h'):
            # 'h' > Show/hide help
            self.args.help_tag = not self.args.help_tag
        elif self.pressedkey == ord('i'):
            # 'i' > Sort processes by IO rate (not available on OS X)
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'io_counters'
        elif self.pressedkey == ord('I'):
            # 'I' > Show/hide IP module
            self.args.disable_ip = not self.args.disable_ip
        elif self.pressedkey == ord('l'):
            # 'l' > Show/hide log messages
            self.args.disable_log = not self.args.disable_log
        elif self.pressedkey == ord('m'):
            # 'm' > Sort processes by MEM usage
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'memory_percent'
        elif self.pressedkey == ord('M'):
            # 'M' > Reset processes summary min/max
            self.args.reset_minmax_tag = not self.args.reset_minmax_tag
        elif self.pressedkey == ord('n'):
            # 'n' > Show/hide network stats
            self.args.disable_network = not self.args.disable_network
        elif self.pressedkey == ord('p'):
            # 'p' > Sort processes by name
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'name'
416 417
        elif self.pressedkey == ord('r'):
            # 'r' > Reset history
418
            self.reset_history_tag = not self.reset_history_tag
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
        elif self.pressedkey == ord('R'):
            # 'R' > Hide RAID plugins
            self.args.disable_raid = not self.args.disable_raid
        elif self.pressedkey == ord('s'):
            # 's' > Show/hide sensors stats (Linux-only)
            self.args.disable_sensors = not self.args.disable_sensors
        elif self.pressedkey == ord('t'):
            # 't' > Sort processes by TIME usage
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'cpu_times'
        elif self.pressedkey == ord('T'):
            # 'T' > View network traffic as sum Rx+Tx
            self.args.network_sum = not self.args.network_sum
        elif self.pressedkey == ord('u'):
            # 'u' > Sort processes by USER
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'username'
        elif self.pressedkey == ord('U'):
            # 'U' > View cumulative network I/O (instead of bitrate)
            self.args.network_cumul = not self.args.network_cumul
439 440 441 442 443 444
        elif self.pressedkey == ord('w'):
            # 'w' > Delete finished warning logs
            glances_logs.clean()
        elif self.pressedkey == ord('x'):
            # 'x' > Delete finished warning and critical logs
            glances_logs.clean(critical=True)
445 446 447 448 449 450 451 452 453
        elif self.pressedkey == ord('z'):
            # 'z' > Enable/Disable processes stats (count + list + AMPs)
            # Enable/Disable display
            self.args.disable_process = not self.args.disable_process
            # Enable/Disable update
            if self.args.disable_process:
                glances_processes.disable()
            else:
                glances_processes.enable()
454 455 456 457
        # Return the key code
        return self.pressedkey

    def end(self):
458
        """Shutdown the curses window."""
N
Nicolas Hennion 已提交
459 460 461 462 463 464 465 466 467
        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
468
        curses.endwin()
469

470
    def init_line_column(self):
A
PEP 257  
Alessio Sergi 已提交
471
        """Init the line and column position for the curses inteface."""
472 473
        self.init_line()
        self.init_column()
474 475

    def init_line(self):
A
PEP 257  
Alessio Sergi 已提交
476
        """Init the line position for the curses inteface."""
477 478 479 480
        self.line = 0
        self.next_line = 0

    def init_column(self):
A
PEP 257  
Alessio Sergi 已提交
481
        """Init the column position for the curses inteface."""
482 483 484 485
        self.column = 0
        self.next_column = 0

    def new_line(self):
A
PEP 257  
Alessio Sergi 已提交
486
        """New line in the curses interface."""
487 488 489
        self.line = self.next_line

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

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

496 497 498
        stats: Stats database to display
        cs_status:
            "None": standalone or server mode
499 500
            "Connected": Client is connected to a Glances server
            "SNMP": Client is connected to a SNMP server
501
            "Disconnected": Client is disconnected from the server
502 503 504 505

        Return:
            True if the stats have been displayed
            False if the help have been displayed
506
        """
507 508
        # Init the internal line/column for Glances Curses
        self.init_line_column()
509 510 511 512 513

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

514 515 516 517 518 519 520 521 522 523 524 525
        # No processes list in SNMP mode
        if cs_status == 'SNMP':
            # so... more space for others plugins
            plugin_max_width = 43
        else:
            plugin_max_width = None

        # Update the stats messages
        ###########################

        # Update the client server status
        self.args.cs_status = cs_status
526 527
        stats_system = stats.get_plugin(
            'system').get_stats_display(args=self.args)
528 529
        stats_uptime = stats.get_plugin('uptime').get_stats_display()
        if self.args.percpu:
530
            stats_cpu = stats.get_plugin('percpu').get_stats_display(args=self.args)
531
        else:
532 533 534 535
            stats_cpu = stats.get_plugin('cpu').get_stats_display(args=self.args)
        stats_load = stats.get_plugin('load').get_stats_display(args=self.args)
        stats_mem = stats.get_plugin('mem').get_stats_display(args=self.args)
        stats_memswap = stats.get_plugin('memswap').get_stats_display(args=self.args)
536 537
        stats_network = stats.get_plugin('network').get_stats_display(
            args=self.args, max_width=plugin_max_width)
538 539 540 541
        try:
            stats_ip = stats.get_plugin('ip').get_stats_display(args=self.args)
        except AttributeError:
            stats_ip = None
542 543 544 545
        stats_diskio = stats.get_plugin(
            'diskio').get_stats_display(args=self.args)
        stats_fs = stats.get_plugin('fs').get_stats_display(
            args=self.args, max_width=plugin_max_width)
546
        stats_folders = stats.get_plugin('folders').get_stats_display(
N
nicolargo 已提交
547
            args=self.args, max_width=plugin_max_width)
N
Nicolargo 已提交
548
        stats_raid = stats.get_plugin('raid').get_stats_display(
549
            args=self.args)
550 551
        stats_sensors = stats.get_plugin(
            'sensors').get_stats_display(args=self.args)
552
        stats_now = stats.get_plugin('now').get_stats_display()
N
Nicolargo 已提交
553 554
        stats_docker = stats.get_plugin('docker').get_stats_display(
            args=self.args)
555 556
        stats_processcount = stats.get_plugin(
            'processcount').get_stats_display(args=self.args)
N
nicolargo 已提交
557 558
        stats_amps = stats.get_plugin(
            'amps').get_stats_display(args=self.args)
559 560
        stats_alert = stats.get_plugin(
            'alert').get_stats_display(args=self.args)
561

562
        # Adapt number of processes to the available space
563
        max_processes_displayed = screen_y - 11 - \
N
Nicolargo 已提交
564 565
            self.get_stats_display_height(stats_alert) - \
            self.get_stats_display_height(stats_docker)
566 567 568 569 570
        try:
            if self.args.enable_process_extended and not self.args.process_tree:
                max_processes_displayed -= 4
        except AttributeError:
            pass
571
        if max_processes_displayed < 0:
572
            max_processes_displayed = 0
573 574 575 576
        if (glances_processes.max_processes is None or
                glances_processes.max_processes != max_processes_displayed):
            logger.debug("Set number of displayed processes to {0}".format(max_processes_displayed))
            glances_processes.max_processes = max_processes_displayed
577

578 579
        stats_processlist = stats.get_plugin(
            'processlist').get_stats_display(args=self.args)
580

581 582 583 584
        # Display the stats on the curses interface
        ###########################################

        # Help screen (on top of the other stats)
585
        if self.args.help_tag:
586
            # Display the stats...
587 588
            self.display_plugin(
                stats.get_plugin('help').get_stats_display(args=self.args))
589 590 591
            # ... and exit
            return False

592 593 594 595 596 597
        # ==================================
        # Display first line (system+uptime)
        # ==================================
        # Space between column
        self.space_between_column = 0
        self.new_line()
598
        l_uptime = self.get_stats_display_width(
N
Nicolargo 已提交
599
            stats_system) + self.space_between_column + self.get_stats_display_width(stats_ip) + 3 + self.get_stats_display_width(stats_uptime)
600 601 602 603 604 605 606 607
        self.display_plugin(
            stats_system, display_optional=(screen_x >= l_uptime))
        self.new_column()
        self.display_plugin(stats_ip)
        # Space between column
        self.space_between_column = 3
        self.new_column()
        self.display_plugin(stats_uptime)
A
Alessio Sergi 已提交
608

N
Nicolargo 已提交
609
        # ========================================================
N
Nicolargo 已提交
610
        # Display second line (<SUMMARY>+CPU|PERCPU+LOAD+MEM+SWAP)
N
Nicolargo 已提交
611
        # ========================================================
612
        self.init_column()
613
        self.new_line()
614

N
Nicolargo 已提交
615 616
        # Init quicklook
        stats_quicklook = {'msgdict': []}
617 618 619 620 621
        quicklook_width = 0

        # Get stats for CPU, MEM, SWAP and LOAD (if needed)
        if self.args.disable_cpu:
            cpu_width = 0
622
        else:
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
            cpu_width = self.get_stats_display_width(stats_cpu)
        if self.args.disable_mem:
            mem_width = 0
        else:
            mem_width = self.get_stats_display_width(stats_mem)
        if self.args.disable_swap:
            swap_width = 0
        else:
            swap_width = self.get_stats_display_width(stats_memswap)
        if self.args.disable_load:
            load_width = 0
        else:
            load_width = self.get_stats_display_width(stats_load)

        # Size of plugins but quicklook
        stats_width = cpu_width + mem_width + swap_width + load_width

        # Number of plugin but quicklook
641 642 643 644 645
        stats_number = (
            int(not self.args.disable_cpu and stats_cpu['msgdict'] != []) +
            int(not self.args.disable_mem and stats_mem['msgdict'] != []) +
            int(not self.args.disable_swap and stats_memswap['msgdict'] != []) +
            int(not self.args.disable_load and stats_load['msgdict'] != []))
646 647 648

        if not self.args.disable_quicklook:
            # Quick look is in the place !
649 650 651 652
            if self.args.full_quicklook:
                quicklook_width = screen_x - (stats_width + 8 + stats_number * self.space_between_column)
            else:
                quicklook_width = min(screen_x - (stats_width + 8 + stats_number * self.space_between_column), 79)
N
Nicolargo 已提交
653 654 655 656 657 658
            try:
                stats_quicklook = stats.get_plugin(
                    'quicklook').get_stats_display(max_width=quicklook_width, args=self.args)
            except AttributeError as e:
                logger.debug("Quicklook plugin not available (%s)" % e)
            else:
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692
                quicklook_width = self.get_stats_display_width(stats_quicklook)
                stats_width += quicklook_width + 1
            self.space_between_column = 1
            self.display_plugin(stats_quicklook)
            self.new_column()

        # Compute spaces between plugins
        # Note: Only one space between Quicklook and others
        display_optional_cpu = True
        display_optional_mem = True
        if stats_number > 1:
            self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
            # No space ? Remove optionnal MEM stats
            if self.space_between_column < 3:
                display_optional_mem = False
                if self.args.disable_mem:
                    mem_width = 0
                else:
                    mem_width = self.get_stats_display_width(stats_mem, without_option=True)
                stats_width = quicklook_width + 1 + cpu_width + mem_width + swap_width + load_width
                self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
            # No space again ? Remove optionnal CPU stats
            if self.space_between_column < 3:
                display_optional_cpu = False
                if self.args.disable_cpu:
                    cpu_width = 0
                else:
                    cpu_width = self.get_stats_display_width(stats_cpu, without_option=True)
                stats_width = quicklook_width + 1 + cpu_width + mem_width + swap_width + load_width
                self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1)))
        else:
            self.space_between_column = 0

        # Display CPU, MEM, SWAP and LOAD
693 694 695 696 697 698 699
        self.display_plugin(stats_cpu, display_optional=display_optional_cpu)
        self.new_column()
        self.display_plugin(stats_mem, display_optional=display_optional_mem)
        self.new_column()
        self.display_plugin(stats_memswap)
        self.new_column()
        self.display_plugin(stats_load)
700

701 702 703
        # Space between column
        self.space_between_column = 3

704 705 706
        # Backup line position
        self.saved_line = self.next_line

707
        # ==================================================================
708
        # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time)
709
        # ==================================================================
710
        self.init_column()
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
        if not (self.args.disable_network and
                self.args.disable_diskio and
                self.args.disable_fs and
                self.args.disable_folder and
                self.args.disable_raid and
                self.args.disable_sensors) and not self.args.disable_left_sidebar:
            self.new_line()
            self.display_plugin(stats_network)
            self.new_line()
            self.display_plugin(stats_diskio)
            self.new_line()
            self.display_plugin(stats_fs)
            self.new_line()
            self.display_plugin(stats_folders)
            self.new_line()
            self.display_plugin(stats_raid)
            self.new_line()
            self.display_plugin(stats_sensors)
            self.new_line()
            self.display_plugin(stats_now)
731

732 733 734
        # ====================================
        # Display right stats (process and co)
        # ====================================
735
        # If space available...
736
        if screen_x > 52:
737 738
            # Restore line position
            self.next_line = self.saved_line
739 740 741

            # Display right sidebar
            # DOCKER+PROCESS_COUNT+AMPS+PROCESS_LIST+ALERT
742
            self.new_column()
743 744 745 746 747 748 749 750 751 752 753 754 755
            self.new_line()
            self.display_plugin(stats_docker)
            self.new_line()
            self.display_plugin(stats_processcount)
            self.new_line()
            self.display_plugin(stats_amps)
            self.new_line()
            self.display_plugin(stats_processlist,
                                display_optional=(screen_x > 102),
                                display_additional=(not OSX),
                                max_y=(screen_y - self.get_stats_display_height(stats_alert) - 2))
            self.new_line()
            self.display_plugin(stats_alert)
756

757 758 759
        # History option
        # Generate history graph
        if self.history_tag and self.args.enable_history:
760
            self.display_popup(
A
Alessio Sergi 已提交
761 762
                'Generate graphs history in {0}\nPlease wait...'.format(
                    self.glances_history.get_output_folder()))
N
Nicolargo 已提交
763
            self.display_popup(
A
Alessio Sergi 已提交
764 765 766
                'Generate graphs history in {0}\nDone: {1} graphs generated'.format(
                    self.glances_history.get_output_folder(),
                    self.glances_history.generate_graph(stats)))
767
        elif self.reset_history_tag and self.args.enable_history:
A
Alessio Sergi 已提交
768
            self.display_popup('Reset history')
769
            self.glances_history.reset(stats)
770
        elif (self.history_tag or self.reset_history_tag) and not self.args.enable_history:
771 772
            try:
                self.glances_history.graph_enabled()
A
Alessio Sergi 已提交
773
            except Exception:
A
Alessio Sergi 已提交
774
                self.display_popup('History disabled\nEnable it using --enable-history')
775
            else:
A
Alessio Sergi 已提交
776
                self.display_popup('History disabled\nPlease install matplotlib')
777 778 779
        self.history_tag = False
        self.reset_history_tag = False

N
Nicolargo 已提交
780
        # Display edit filter popup
781 782
        # Only in standalone mode (cs_status is None)
        if self.edit_filter and cs_status is None:
A
Alessio Sergi 已提交
783
            new_filter = self.display_popup(
784 785 786 787 788 789 790 791 792 793 794 795
                'Process filter pattern: \n' +
                '\n' +
                'Examples:\n' +
                '- python\n' +
                '- .*python.*\n' +
                '- \/usr\/lib.*' +
                '- name:.*nautilus.*\n' +
                '- cmdline:.*glances.*\n' +
                '- username:nicolargo\n' +
                '- username:^root        ',
                is_input=True,
                input_value=glances_processes.process_filter_input)
796
            glances_processes.process_filter = new_filter
797
        elif self.edit_filter and cs_status != 'None':
A
Alessio Sergi 已提交
798
            self.display_popup('Process filter only available in standalone mode')
N
Nicolargo 已提交
799 800
        self.edit_filter = False

801 802
        return True

803 804
    def display_popup(self, message,
                      size_x=None, size_y=None,
N
Nicolargo 已提交
805 806
                      duration=3,
                      is_input=False,
N
Nicolargo 已提交
807
                      input_size=30,
N
Nicolargo 已提交
808
                      input_value=None):
809
        """
A
PEP 257  
Alessio Sergi 已提交
810 811
        Display a centered popup.

N
Nicolargo 已提交
812 813 814 815 816
        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 已提交
817

N
Nicolargo 已提交
818 819 820 821
        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
822
         Return the input string or None if the field is empty
823 824
        """
        # Center the popup
N
Nicolargo 已提交
825
        sentence_list = message.split('\n')
826
        if size_x is None:
N
Nicolargo 已提交
827
            size_x = len(max(sentence_list, key=len)) + 4
N
Nicolargo 已提交
828 829 830
            # Add space for the input field
            if is_input:
                size_x += input_size
831
        if size_y is None:
N
Nicolargo 已提交
832
            size_y = len(sentence_list) + 4
833 834 835 836
        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
837
            return False
838 839
        pos_x = int((screen_x - size_x) / 2)
        pos_y = int((screen_y - size_y) / 2)
840 841 842

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

844 845 846 847
        # Fill the popup
        popup.border()

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

A
Alessio Sergi 已提交
851
        if is_input and not WINDOWS:
N
Nicolargo 已提交
852 853
            # Create a subwindow for the text field
            subpop = popup.derwin(1, input_size, 2, 2 + len(m))
854
            subpop.attron(self.colors_list['FILTER'])
N
Nicolargo 已提交
855 856 857 858 859 860 861 862
            # 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)
863
            self.flash_cursor()
A
Alessio Sergi 已提交
864
            textbox = GlancesTextbox(subpop, insert_mode=False)
N
Nicolargo 已提交
865 866
            textbox.edit()
            self.set_cursor(0)
867
            self.no_flash_cursor()
N
Nicolargo 已提交
868
            if textbox.gather() != '':
N
Nicolargo 已提交
869
                logger.debug(
870
                    "User enters the following string: %s" % textbox.gather())
N
Nicolargo 已提交
871 872
                return textbox.gather()[:-1]
            else:
873
                logger.debug("User centers an empty string")
N
Nicolargo 已提交
874 875 876 877
                return None
        else:
            # Display the popup
            popup.refresh()
878
            self.wait(duration * 1000)
N
Nicolargo 已提交
879
            return True
880

881
    def display_plugin(self, plugin_stats,
882
                       display_optional=True,
883
                       display_additional=True,
884
                       max_y=65535):
A
PEP 257  
Alessio Sergi 已提交
885 886
        """Display the plugin_stats on the screen.

887 888
        If display_optional=True display the optional stats
        If display_additional=True display additionnal stats
889 890
        max_y do not display line > max_y
        """
891 892 893
        # Exit if:
        # - the plugin_stats message is empty
        # - the display tag = False
894
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
895
            # Exit
896 897 898 899 900 901 902
            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
903
        if plugin_stats['align'] == 'right':
904
            # Right align (last column)
905
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
906
        else:
907
            display_x = self.column
908
        if plugin_stats['align'] == 'bottom':
909
            # Bottom (last line)
910
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
911
        else:
912
            display_y = self.line
913

914 915
        # Display
        x = display_x
916
        x_max = x
917 918 919
        y = display_y
        for m in plugin_stats['msgdict']:
            # New line
920
            if m['msg'].startswith('\n'):
921
                # Go to the next line
922
                y += 1
923 924 925 926
                # Return to the first column
                x = display_x
                continue
            # Do not display outside the screen
927
            if x < 0:
928
                continue
929
            if not m['splittable'] and (x + len(m['msg']) > screen_x):
930
                continue
931
            if y < 0 or (y + 1 > screen_y) or (y > max_y):
932 933
                break
            # If display_optional = False do not display optional stats
934
            if not display_optional and m['optional']:
935
                continue
936 937 938
            # If display_additional = False do not display additional stats
            if not display_additional and m['additional']:
                continue
939 940 941
            # Is it possible to display the stat with the current screen size
            # !!! Crach if not try/except... Why ???
            try:
A
Alessio Sergi 已提交
942 943
                self.term_window.addnstr(y, x,
                                         m['msg'],
944 945
                                         # Do not disply outside the screen
                                         screen_x - x,
946
                                         self.colors_list[m['decoration']])
A
Alessio Sergi 已提交
947
            except Exception:
948 949 950
                pass
            else:
                # New column
A
Alessio Sergi 已提交
951 952 953 954
                # 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 已提交
955 956 957 958 959
                try:
                    x += len(u(m['msg']))
                except UnicodeDecodeError:
                    # Quick and dirty hack for issue #745
                    pass
960 961
                if x > x_max:
                    x_max = x
962 963

        # Compute the next Glances column/line position
N
Nicolargo 已提交
964 965
        self.next_column = max(
            self.next_column, x_max + self.space_between_column)
966
        self.next_line = max(self.next_line, y + self.space_between_line)
967 968

    def erase(self):
A
PEP 257  
Alessio Sergi 已提交
969
        """Erase the content of the screen."""
970 971
        self.term_window.erase()

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

975 976 977 978 979 980 981
        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()
982
        self.display(stats, cs_status=cs_status)
983

984
    def update(self, stats, cs_status=None, return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
985 986 987 988
        """Update the screen.

        Wait for __refresh_time sec / catch key every 100 ms.

N
Nicolargo 已提交
989
        INPUT
990 991 992 993 994
        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
N
Nicolargo 已提交
995 996 997 998 999 1000 1001
        return_to_browser:
            True: Do not exist, return to the browser list
            False: Exit and return to the shell

        OUPUT
        True: Exit key has been pressed
        False: Others cases...
1002 1003
        """
        # Flush display
1004
        self.flush(stats, cs_status=cs_status)
1005 1006

        # Wait
N
Nicolargo 已提交
1007
        exitkey = False
1008
        countdown = Timer(self.__refresh_time)
N
Nicolargo 已提交
1009
        while not countdown.finished() and not exitkey:
1010
            # Getkey
N
Nicolargo 已提交
1011 1012 1013 1014
            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:
1015
                # Redraw display
1016
                self.flush(stats, cs_status=cs_status)
1017
            # Wait 100ms...
1018
            self.wait()
1019

N
Nicolargo 已提交
1020 1021
        return exitkey

1022 1023 1024 1025
    def wait(self, delay=100):
        """Wait delay in ms"""
        curses.napms(100)

1026
    def get_stats_display_width(self, curse_msg, without_option=False):
A
PEP 257  
Alessio Sergi 已提交
1027
        """Return the width of the formatted curses message.
1028

A
PEP 257  
Alessio Sergi 已提交
1029 1030
        The height is defined by the maximum line.
        """
1031
        try:
1032
            if without_option:
1033
                # Size without options
A
flake8  
Alessio Sergi 已提交
1034
                c = len(max(''.join([(re.sub(r'[^\x00-\x7F]+', ' ', i['msg']) if not i['optional'] else "")
1035
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
1036 1037
            else:
                # Size with all options
A
flake8  
Alessio Sergi 已提交
1038
                c = len(max(''.join([re.sub(r'[^\x00-\x7F]+', ' ', i['msg'])
1039
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
A
Alessio Sergi 已提交
1040
        except Exception:
1041 1042 1043 1044
            return 0
        else:
            return c

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

A
PEP 257  
Alessio Sergi 已提交
1048 1049
        The height is defined by the number of '\n' (new line).
        """
1050
        try:
A
Alessio Sergi 已提交
1051
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
A
Alessio Sergi 已提交
1052
        except Exception:
1053 1054 1055
            return 0
        else:
            return c + 1
N
Nicolargo 已提交
1056

1057

1058 1059
class GlancesCursesStandalone(_GlancesCurses):

A
PEP 257  
Alessio Sergi 已提交
1060
    """Class for the Glances curse standalone."""
1061 1062 1063 1064

    pass


1065 1066
class GlancesCursesClient(_GlancesCurses):

A
PEP 257  
Alessio Sergi 已提交
1067
    """Class for the Glances curse client."""
1068 1069 1070 1071

    pass


A
Alessio Sergi 已提交
1072
if not WINDOWS:
1073
    class GlancesTextbox(Textbox, object):
1074

1075 1076
        def __init__(self, *args, **kwargs):
            super(GlancesTextbox, self).__init__(*args, **kwargs)
1077

N
Nicolargo 已提交
1078
        def do_command(self, ch):
1079
            if ch == 10:  # Enter
N
Nicolargo 已提交
1080
                return 0
1081
            if ch == 127:  # Back
N
Nicolargo 已提交
1082
                return 8
1083
            return super(GlancesTextbox, self).do_command(ch)