glances_curses.py 43.3 KB
Newer Older
1 2
# -*- coding: utf-8 -*-
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2015 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."""

22 23
# Import system lib
import sys
N
Nicolargo 已提交
24
import re
25 26

# Import Glances lib
A
Alessio Sergi 已提交
27 28
from glances.core.glances_globals import is_mac, is_windows
from glances.core.glances_logging import logger
A
Alessio Sergi 已提交
29 30
from glances.core.glances_logs import glances_logs
from glances.core.glances_processes import glances_processes
A
Alessio Sergi 已提交
31
from glances.core.glances_timer import Timer
32 33

# Import curses lib for "normal" operating system and consolelog for Windows
34
if not is_windows:
35 36 37
    try:
        import curses
        import curses.panel
38
        from curses.textpad import Textbox
39
    except ImportError:
N
Nicolargo 已提交
40 41
        logger.critical(
            "Curses module not found. Glances cannot start in standalone mode.")
42 43
        sys.exit(1)
else:
44 45
    from glances.outputs.glances_colorconsole import WCurseLight
    curses = WCurseLight()
46 47


48
class _GlancesCurses(object):
49

50 51 52 53
    """
    This class manages the curses display (and key pressed).
    Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser
    """
54

A
PEP 257  
Alessio Sergi 已提交
55
    def __init__(self, args=None):
56 57
        # Init args
        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 78 79 80 81

        # Set curses options
        if hasattr(curses, 'start_color'):
            curses.start_color()
        if hasattr(curses, 'use_default_colors'):
            curses.use_default_colors()
        if hasattr(curses, 'noecho'):
            curses.noecho()
        if hasattr(curses, 'cbreak'):
            curses.cbreak()
N
Nicolargo 已提交
82
        self.set_cursor(0)
83 84 85 86 87 88

        # Init colors
        self.hascolors = False
        if curses.has_colors() and curses.COLOR_PAIRS > 8:
            self.hascolors = True
            # FG color, BG color
N
Nicolargo 已提交
89 90 91 92
            if args.theme_white:
                curses.init_pair(1, curses.COLOR_BLACK, -1)
            else:
                curses.init_pair(1, curses.COLOR_WHITE, -1)
93 94 95 96 97 98 99
            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)
            curses.init_pair(6, curses.COLOR_RED, -1)
            curses.init_pair(7, curses.COLOR_GREEN, -1)
            curses.init_pair(8, curses.COLOR_BLUE, -1)
N
Nicolargo 已提交
100 101
            try:
                curses.init_pair(9, curses.COLOR_MAGENTA, -1)
A
Alessio Sergi 已提交
102
            except Exception:
N
Nicolargo 已提交
103 104 105 106 107 108
                if args.theme_white:
                    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)
A
Alessio Sergi 已提交
109
            except Exception:
N
Nicolargo 已提交
110 111 112 113 114
                if args.theme_white:
                    curses.init_pair(10, curses.COLOR_BLACK, -1)
                else:
                    curses.init_pair(10, curses.COLOR_WHITE, -1)

115 116 117
        else:
            self.hascolors = False

A
Alessio Sergi 已提交
118
        if args.disable_bold:
119 120 121 122 123 124 125 126 127 128
            A_BOLD = curses.A_BOLD
        else:
            A_BOLD = 0

        self.title_color = A_BOLD
        self.title_underline_color = A_BOLD | curses.A_UNDERLINE
        self.help_color = A_BOLD
        if self.hascolors:
            # Colors text styles
            self.no_color = curses.color_pair(1)
A
Alessio Sergi 已提交
129
            self.default_color = curses.color_pair(3) | A_BOLD
130
            self.nice_color = curses.color_pair(9) | A_BOLD
131 132 133 134 135 136 137
            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
            self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
            self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
N
Nicolargo 已提交
138
            self.filter_color = curses.color_pair(10) | A_BOLD
139 140 141 142
        else:
            # B&W text styles
            self.no_color = curses.A_NORMAL
            self.default_color = curses.A_NORMAL
143
            self.nice_color = A_BOLD
144 145 146 147 148 149 150
            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 已提交
151
            self.filter_color = A_BOLD
152 153

        # Define the colors list (hash table) for stats
154
        self.colors_list = {
155 156 157
            'DEFAULT': self.no_color,
            'UNDERLINE': curses.A_UNDERLINE,
            'BOLD': A_BOLD,
158
            'SORT': A_BOLD,
159
            'OK': self.default_color2,
N
Nicolargo 已提交
160
            'FILTER': self.filter_color,
161
            'TITLE': self.title_color,
162 163
            'PROCESS': self.default_color2,
            'STATUS': self.default_color2,
164
            'NICE': self.nice_color,
165 166 167 168 169 170 171 172 173 174 175 176 177
            '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,
            'CRITICAL_LOG': self.ifCRITICAL_color
        }

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

        # Init refresh time
N
Nicolargo 已提交
178
        self.__refresh_time = args.time
179

N
Nicolargo 已提交
180 181 182
        # Init edit filter tag
        self.edit_filter = False

183 184 185 186 187
        # Catch key pressed with non blocking mode
        self.term_window.keypad(1)
        self.term_window.nodelay(1)
        self.pressedkey = -1

188 189 190 191
        # History tag
        self.reset_history_tag = False
        self.history_tag = False
        if args.enable_history:
192 193
            logger.info('Stats history enabled with output path %s' %
                        args.path_history)
194
            from glances.exports.glances_history import GlancesHistory
195
            self.glances_history = GlancesHistory(args.path_history)
196 197
            if not self.glances_history.graph_enabled():
                args.enable_history = False
198 199
                logger.error(
                    'Stats history disabled because MatPlotLib is not installed')
200

N
Nicolargo 已提交
201
    def set_cursor(self, value):
202
        """Configure the curse cursor apparence
N
Nicolargo 已提交
203 204 205 206 207 208 209 210 211 212
           0: invisible
           1: visible
           2: very visible
           """
        if hasattr(curses, 'curs_set'):
            try:
                curses.curs_set(value)
            except Exception:
                pass

213
    def get_key(self, window):
A
PEP 257  
Alessio Sergi 已提交
214
        # Catch ESC key AND numlock key (issue #163)
215 216 217 218
        keycode = [0, 0]
        keycode[0] = window.getch()
        keycode[1] = window.getch()

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

222 223 224 225 226 227
        if keycode[0] == 27 and keycode[1] != -1:
            # Do not escape on specials keys
            return -1
        else:
            return keycode[0]

N
Nicolargo 已提交
228
    def __catch_key(self, return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
229
        # Catch the pressed key
230
        self.pressedkey = self.get_key(self.term_window)
231 232 233 234

        # Actions...
        if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
            # 'ESC'|'q' > Quit
N
Nicolargo 已提交
235 236 237 238 239 240
            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 已提交
241 242 243
        elif self.pressedkey == 10:
            # 'ENTER' > Edit the process filter
            self.edit_filter = not self.edit_filter
244 245 246
        elif self.pressedkey == ord('1'):
            # '1' > Switch between CPU and PerCPU information
            self.args.percpu = not self.args.percpu
247 248
        elif self.pressedkey == ord('2'):
            # '2' > Enable/disable left sidebar
249
            self.args.disable_left_sidebar = not self.args.disable_left_sidebar
N
nicolargo 已提交
250 251 252
        elif self.pressedkey == ord('3'):
            # '3' > Enable/disable quicklook
            self.args.disable_quicklook = not self.args.disable_quicklook
253 254 255
        elif self.pressedkey == ord('/'):
            # '/' > Switch between short/long name for processes
            self.args.process_short_name = not self.args.process_short_name
256
        elif self.pressedkey == ord('a'):
257 258 259
            # 'a' > Sort processes automatically and reset to 'cpu_percent'
            glances_processes.auto_sort = True
            glances_processes.sort_key = 'cpu_percent'
260 261 262 263 264 265
        elif self.pressedkey == ord('b'):
            # 'b' > Switch between bit/s and Byte/s for network IO
            # self.net_byteps_tag = not self.net_byteps_tag
            self.args.byte = not self.args.byte
        elif self.pressedkey == ord('c'):
            # 'c' > Sort processes by CPU usage
266 267
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'cpu_percent'
268
        elif self.pressedkey == ord('d'):
269
            # 'd' > Show/hide disk I/O stats
270
            self.args.disable_diskio = not self.args.disable_diskio
N
Nicolargo 已提交
271 272 273
        elif self.pressedkey == ord('D'):
            # 'D' > Show/hide Docker stats
            self.args.disable_docker = not self.args.disable_docker
N
Nicolargo 已提交
274 275
        elif self.pressedkey == ord('e'):
            # 'e' > Enable/Disable extended stats for top process
276 277
            self.args.enable_process_extended = not self.args.enable_process_extended
            if not self.args.enable_process_extended:
N
Nicolargo 已提交
278 279 280
                glances_processes.disable_extended()
            else:
                glances_processes.enable_extended()
281 282 283
        elif self.pressedkey == ord('F'):
            # 'F' > Switch between FS available and free space
            self.args.fs_free_space = not self.args.fs_free_space
284
        elif self.pressedkey == ord('f'):
285
            # 'f' > Show/hide fs stats
286
            self.args.disable_fs = not self.args.disable_fs
287 288 289
        elif self.pressedkey == ord('g'):
            # 'g' > History
            self.history_tag = not self.history_tag
290 291
        elif self.pressedkey == ord('h'):
            # 'h' > Show/hide help
292
            self.args.help_tag = not self.args.help_tag
293
        elif self.pressedkey == ord('i'):
294
            # 'i' > Sort processes by IO rate (not available on OS X)
295 296
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'io_counters'
297 298 299
        elif self.pressedkey == ord('I'):
            # 'I' > Show/hide IP module
            self.args.disable_ip = not self.args.disable_ip
300 301
        elif self.pressedkey == ord('l'):
            # 'l' > Show/hide log messages
302
            self.args.disable_log = not self.args.disable_log
303 304
        elif self.pressedkey == ord('m'):
            # 'm' > Sort processes by MEM usage
305 306
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'memory_percent'
307
        elif self.pressedkey == ord('n'):
308
            # 'n' > Show/hide network stats
309
            self.args.disable_network = not self.args.disable_network
310 311
        elif self.pressedkey == ord('p'):
            # 'p' > Sort processes by name
312 313
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'name'
314 315
        elif self.pressedkey == ord('r'):
            # 'r' > Reset history
316
            self.reset_history_tag = not self.reset_history_tag
N
Nicolargo 已提交
317 318 319
        elif self.pressedkey == ord('R'):
            # 'R' > Hide RAID plugins
            self.args.disable_raid = not self.args.disable_raid
320 321
        elif self.pressedkey == ord('s'):
            # 's' > Show/hide sensors stats (Linux-only)
322
            self.args.disable_sensors = not self.args.disable_sensors
323
        elif self.pressedkey == ord('t'):
324
            # 't' > Sort processes by TIME usage
325 326
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'cpu_times'
327 328
        elif self.pressedkey == ord('T'):
            # 'T' > View network traffic as sum Rx+Tx
329
            self.args.network_sum = not self.args.network_sum
330
        elif self.pressedkey == ord('u'):
331 332
            # 'u' > View cumulative network IO (instead of bitrate)
            self.args.network_cumul = not self.args.network_cumul
333 334 335 336 337 338
        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)
339 340 341 342 343
        elif self.pressedkey == ord('z'):
            # 'z' > Enable/Disable processes stats (count + list + monitor)
            # Enable/Disable display
            self.args.disable_process = not self.args.disable_process
            # Enable/Disable update
344
            if self.args.disable_process:
345 346 347
                glances_processes.disable()
            else:
                glances_processes.enable()
348 349 350 351
        # Return the key code
        return self.pressedkey

    def end(self):
352
        """Shutdown the curses window."""
N
Nicolas Hennion 已提交
353 354 355 356 357 358 359 360 361
        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
362
        curses.endwin()
363

364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
    def init_line_column(self):
        """Init the line and column position for the curses inteface"""
        self.line = 0
        self.column = 0
        self.next_line = 0
        self.next_column = 0

    def init_line(self):
        """Init the line position for the curses inteface"""
        self.line = 0
        self.next_line = 0

    def init_column(self):
        """Init the column position for the curses inteface"""
        self.column = 0
        self.next_column = 0

    def new_line(self):
        """New line in the curses interface"""
        self.line = self.next_line

    def new_column(self):
        """New column in the curses interface"""
        self.column = self.next_column

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

392 393 394
        stats: Stats database to display
        cs_status:
            "None": standalone or server mode
395 396
            "Connected": Client is connected to a Glances server
            "SNMP": Client is connected to a SNMP server
397
            "Disconnected": Client is disconnected from the server
398 399 400 401

        Return:
            True if the stats have been displayed
            False if the help have been displayed
402
        """
403 404
        # Init the internal line/column for Glances Curses
        self.init_line_column()
405 406 407 408 409

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

410 411 412 413 414 415 416 417 418 419 420 421
        # 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
422 423
        stats_system = stats.get_plugin(
            'system').get_stats_display(args=self.args)
424 425 426 427 428 429 430 431
        stats_uptime = stats.get_plugin('uptime').get_stats_display()
        if self.args.percpu:
            stats_percpu = stats.get_plugin('percpu').get_stats_display()
        else:
            stats_cpu = stats.get_plugin('cpu').get_stats_display()
        stats_load = stats.get_plugin('load').get_stats_display()
        stats_mem = stats.get_plugin('mem').get_stats_display()
        stats_memswap = stats.get_plugin('memswap').get_stats_display()
432 433
        stats_network = stats.get_plugin('network').get_stats_display(
            args=self.args, max_width=plugin_max_width)
434 435 436 437
        try:
            stats_ip = stats.get_plugin('ip').get_stats_display(args=self.args)
        except AttributeError:
            stats_ip = None
438 439 440 441
        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)
N
Nicolargo 已提交
442
        stats_raid = stats.get_plugin('raid').get_stats_display(
443
            args=self.args)
444 445
        stats_sensors = stats.get_plugin(
            'sensors').get_stats_display(args=self.args)
446
        stats_now = stats.get_plugin('now').get_stats_display()
N
Nicolargo 已提交
447 448
        stats_docker = stats.get_plugin('docker').get_stats_display(
            args=self.args)
449 450 451 452 453 454
        stats_processcount = stats.get_plugin(
            'processcount').get_stats_display(args=self.args)
        stats_monitor = stats.get_plugin(
            'monitor').get_stats_display(args=self.args)
        stats_alert = stats.get_plugin(
            'alert').get_stats_display(args=self.args)
455

456
        # Adapt number of processes to the available space
457
        max_processes_displayed = screen_y - 11 - \
N
Nicolargo 已提交
458 459
            self.get_stats_display_height(stats_alert) - \
            self.get_stats_display_height(stats_docker)
460
        if self.args.enable_process_extended and not self.args.process_tree:
461 462
            max_processes_displayed -= 4
        if max_processes_displayed < 0:
463
            max_processes_displayed = 0
464 465 466 467
        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
468

469 470
        stats_processlist = stats.get_plugin(
            'processlist').get_stats_display(args=self.args)
471

472 473 474 475
        # Display the stats on the curses interface
        ###########################################

        # Help screen (on top of the other stats)
476
        if self.args.help_tag:
477
            # Display the stats...
478 479
            self.display_plugin(
                stats.get_plugin('help').get_stats_display(args=self.args))
480 481 482
            # ... and exit
            return False

N
Nicolargo 已提交
483
        # ==================================
484
        # Display first line (system+uptime)
N
Nicolargo 已提交
485
        # ==================================
486 487
        # Space between column
        self.space_between_column = 0
488
        self.new_line()
489
        l_uptime = self.get_stats_display_width(
N
Nicolargo 已提交
490
            stats_system) + self.space_between_column + self.get_stats_display_width(stats_ip) + 3 + self.get_stats_display_width(stats_uptime)
491 492 493 494 495 496
        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
497
        self.new_column()
498
        self.display_plugin(stats_uptime)
A
Alessio Sergi 已提交
499

N
Nicolargo 已提交
500
        # ========================================================
N
Nicolargo 已提交
501
        # Display second line (<SUMMARY>+CPU|PERCPU+LOAD+MEM+SWAP)
N
Nicolargo 已提交
502
        # ========================================================
503
        self.init_column()
504
        self.new_line()
N
Nicolargo 已提交
505 506 507 508
        # Init quicklook
        stats_quicklook = {'msgdict': []}
        # Start with the mandatory stats:
        # CPU | PERCPU
509
        if self.args.percpu:
N
Nicolargo 已提交
510
            cpu_width = self.get_stats_display_width(stats_percpu)
N
nicolargo 已提交
511
            quicklook_adapt = 114
512
        else:
513 514
            cpu_width = self.get_stats_display_width(
                stats_cpu, without_option=(screen_x < 80))
515
            quicklook_adapt = 108
N
Nicolargo 已提交
516 517
        l = cpu_width
        # MEM & SWAP & LOAD
518 519
        l += self.get_stats_display_width(stats_mem,
                                          without_option=(screen_x < 100))
N
Nicolargo 已提交
520 521 522
        l += self.get_stats_display_width(stats_memswap)
        l += self.get_stats_display_width(stats_load)
        # Quicklook plugin size is dynamic
N
Nicolargo 已提交
523
        l_ql = 0
N
nicolargo 已提交
524
        if screen_x > 126 and not self.args.disable_quicklook:
525 526
            # Limit the size to be align with the process
            quicklook_width = min(screen_x - quicklook_adapt, 87)
N
Nicolargo 已提交
527 528 529 530 531 532
            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:
N
Nicolargo 已提交
533 534 535 536
                l_ql = self.get_stats_display_width(stats_quicklook)
        # Display Quicklook
        self.display_plugin(stats_quicklook)
        self.new_column()
N
Nicolargo 已提交
537 538 539 540 541
        # Compute space between column
        space_number = int(stats_quicklook['msgdict'] != [])
        space_number += int(stats_mem['msgdict'] != [])
        space_number += int(stats_memswap['msgdict'] != [])
        space_number += int(stats_load['msgdict'] != [])
N
Nicolargo 已提交
542
        if space_number < 1:
543
            space_number = 1
N
Nicolargo 已提交
544
        if screen_x > (space_number * self.space_between_column + l):
N
Nicolargo 已提交
545 546
            self.space_between_column = int((screen_x - l_ql - l) / space_number)
        # Display others stats
547
        if self.args.percpu:
548 549
            self.display_plugin(stats_percpu)
        else:
550
            self.display_plugin(stats_cpu, display_optional=(screen_x >= 80))
551
        self.new_column()
N
Nicolargo 已提交
552
        self.display_plugin(stats_mem, display_optional=(screen_x >= 100))
553
        self.new_column()
554
        self.display_plugin(stats_memswap)
N
Nicolargo 已提交
555 556
        self.new_column()
        self.display_plugin(stats_load)
557 558 559
        # Space between column
        self.space_between_column = 3

560 561 562
        # Backup line position
        self.saved_line = self.next_line

563
        # ==================================================================
564
        # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time)
565
        # ==================================================================
566
        self.init_column()
567
        if (not (self.args.disable_network and self.args.disable_diskio
N
Nicolargo 已提交
568 569
                 and self.args.disable_fs and self.args.disable_raid
                 and self.args.disable_sensors)) \
570
                and not self.args.disable_left_sidebar:
571 572 573 574 575 576 577
            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()
N
Nicolargo 已提交
578 579
            self.display_plugin(stats_raid)
            self.new_line()
580 581 582
            self.display_plugin(stats_sensors)
            self.new_line()
            self.display_plugin(stats_now)
583

N
Nicolargo 已提交
584 585 586
        # ====================================
        # Display right stats (process and co)
        # ====================================
587
        # If space available...
588
        if screen_x > 52:
589 590 591
            # Restore line position
            self.next_line = self.saved_line

592
            # Display right sidebar
N
Nicolargo 已提交
593
            # ((DOCKER)+PROCESS_COUNT+(MONITORED)+PROCESS_LIST+ALERT)
594 595
            self.new_column()
            self.new_line()
N
Nicolargo 已提交
596 597
            self.display_plugin(stats_docker)
            self.new_line()
598
            self.display_plugin(stats_processcount)
599
            if glances_processes.process_filter is None and cs_status is None:
N
Nicolargo 已提交
600 601 602
                # Do not display stats monitor list if a filter exist
                self.new_line()
                self.display_plugin(stats_monitor)
603
            self.new_line()
A
Alessio Sergi 已提交
604
            self.display_plugin(stats_processlist,
605
                                display_optional=(screen_x > 102),
606
                                display_additional=(not is_mac),
A
Alessio Sergi 已提交
607
                                max_y=(screen_y - self.get_stats_display_height(stats_alert) - 2))
608
            self.new_line()
609 610
            self.display_plugin(stats_alert)

611 612 613
        # History option
        # Generate history graph
        if self.history_tag and self.args.enable_history:
614 615
            self.display_popup(
                _("Generate graphs history in %s\nPlease wait...") % self.glances_history.get_output_folder())
N
Nicolargo 已提交
616 617
            self.display_popup(
                _("Generate graphs history in %s\nDone: %s graphs generated") % (self.glances_history.get_output_folder(), self.glances_history.generate_graph(stats)))
618 619
        elif self.reset_history_tag and self.args.enable_history:
            self.display_popup(_("Reset history"))
620
            self.glances_history.reset(stats)
621
        elif (self.history_tag or self.reset_history_tag) and not self.args.enable_history:
622 623
            try:
                self.glances_history.graph_enabled()
A
Alessio Sergi 已提交
624
            except Exception:
625 626
                self.display_popup(
                    _("History disabled\nEnable it using --enable-history"))
627
            else:
628 629
                self.display_popup(
                    _("History disabled\nPlease install MatPlotLib"))
630 631 632
        self.history_tag = False
        self.reset_history_tag = False

N
Nicolargo 已提交
633
        # Display edit filter popup
634 635
        # Only in standalone mode (cs_status is None)
        if self.edit_filter and cs_status is None:
636
            new_filter = self.display_popup(_("Process filter pattern: "),
N
Nicolargo 已提交
637
                                            is_input=True,
638 639
                                            input_value=glances_processes.process_filter)
            glances_processes.process_filter = new_filter
640
        elif self.edit_filter and cs_status != 'None':
641 642
            self.display_popup(
                _("Process filter only available in standalone mode"))
N
Nicolargo 已提交
643 644
        self.edit_filter = False

645 646
        return True

647 648
    def display_popup(self, message,
                      size_x=None, size_y=None,
N
Nicolargo 已提交
649 650
                      duration=3,
                      is_input=False,
N
Nicolargo 已提交
651
                      input_size=30,
N
Nicolargo 已提交
652
                      input_value=None):
653
        """
N
Nicolargo 已提交
654 655 656 657 658 659 660 661 662
        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
        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
663
         Return the input string or None if the field is empty
664 665 666
        """

        # Center the popup
N
Nicolargo 已提交
667
        sentence_list = message.split('\n')
668
        if size_x is None:
N
Nicolargo 已提交
669
            size_x = len(max(sentence_list, key=len)) + 4
N
Nicolargo 已提交
670 671 672
            # Add space for the input field
            if is_input:
                size_x += input_size
673
        if size_y is None:
N
Nicolargo 已提交
674
            size_y = len(sentence_list) + 4
675 676 677 678
        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
679
            return False
680 681
        pos_x = int((screen_x - size_x) / 2)
        pos_y = int((screen_y - size_y) / 2)
682 683 684

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

686 687 688 689 690 691 692 693 694
        # Fill the popup
        popup.border()

        # Add the message
        y = 0
        for m in message.split('\n'):
            popup.addnstr(2 + y, 2, m, len(m))
            y += 1

N
Nicolargo 已提交
695
        if is_input and not is_windows:
N
Nicolargo 已提交
696 697
            # Create a subwindow for the text field
            subpop = popup.derwin(1, input_size, 2, 2 + len(m))
698
            subpop.attron(self.colors_list['FILTER'])
N
Nicolargo 已提交
699 700 701 702 703 704 705 706
            # 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)
A
Alessio Sergi 已提交
707
            textbox = GlancesTextbox(subpop, insert_mode=False)
N
Nicolargo 已提交
708 709 710
            textbox.edit()
            self.set_cursor(0)
            if textbox.gather() != '':
N
Nicolargo 已提交
711 712
                logger.debug(
                    "User enters the following process filter patern: %s" % textbox.gather())
N
Nicolargo 已提交
713 714
                return textbox.gather()[:-1]
            else:
N
Nicolargo 已提交
715
                logger.debug("User clears the process filter patern")
N
Nicolargo 已提交
716 717 718 719 720 721
                return None
        else:
            # Display the popup
            popup.refresh()
            curses.napms(duration * 1000)
            return True
722

723
    def display_plugin(self, plugin_stats,
724
                       display_optional=True,
725
                       display_additional=True,
726
                       max_y=65535):
A
PEP 257  
Alessio Sergi 已提交
727 728
        """Display the plugin_stats on the screen.

729 730
        If display_optional=True display the optional stats
        If display_additional=True display additionnal stats
731 732
        max_y do not display line > max_y
        """
733 734 735
        # Exit if:
        # - the plugin_stats message is empty
        # - the display tag = False
736
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
737
            # Exit
738 739 740 741 742 743 744
            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
745
        if plugin_stats['align'] == 'right':
746
            # Right align (last column)
747
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
748
        else:
749
            display_x = self.column
750
        if plugin_stats['align'] == 'bottom':
751
            # Bottom (last line)
752
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
753
        else:
754
            display_y = self.line
755

756 757
        # Display
        x = display_x
758
        x_max = x
759 760 761
        y = display_y
        for m in plugin_stats['msgdict']:
            # New line
762
            if m['msg'].startswith('\n'):
763
                # Go to the next line
764
                y += 1
765 766 767 768
                # Return to the first column
                x = display_x
                continue
            # Do not display outside the screen
769
            if x < 0:
770
                continue
771
            if not m['splittable'] and (x + len(m['msg']) > screen_x):
772
                continue
773
            if y < 0 or (y + 1 > screen_y) or (y > max_y):
774 775
                break
            # If display_optional = False do not display optional stats
776
            if not display_optional and m['optional']:
777
                continue
778 779 780
            # If display_additional = False do not display additional stats
            if not display_additional and m['additional']:
                continue
781 782 783
            # Is it possible to display the stat with the current screen size
            # !!! Crach if not try/except... Why ???
            try:
A
Alessio Sergi 已提交
784 785
                self.term_window.addnstr(y, x,
                                         m['msg'],
786 787
                                         # Do not disply outside the screen
                                         screen_x - x,
788
                                         self.colors_list[m['decoration']])
A
Alessio Sergi 已提交
789
            except Exception:
790 791 792
                pass
            else:
                # New column
D
desbma 已提交
793 794 795 796 797
                try:
                    # Python 2: we need to decode to get real screen size because utf-8 special tree chars
                    # occupy several bytes
                    offset = len(m['msg'].decode("utf-8"))
                except AttributeError:
N
Nicolargo 已提交
798 799
                    # Python 3: strings are strings and bytes are bytes, all is
                    # good
D
desbma 已提交
800
                    offset = len(m['msg'])
801
                x += offset
802 803
                if x > x_max:
                    x_max = x
804 805

        # Compute the next Glances column/line position
N
Nicolargo 已提交
806 807
        self.next_column = max(
            self.next_column, x_max + self.space_between_column)
808
        self.next_line = max(self.next_line, y + self.space_between_line)
809 810

    def erase(self):
A
PEP 257  
Alessio Sergi 已提交
811
        """Erase the content of the screen."""
812 813
        self.term_window.erase()

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

817 818 819 820 821 822 823
        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()
824
        self.display(stats, cs_status=cs_status)
825

826
    def update(self, stats, cs_status=None, return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
827 828 829 830
        """Update the screen.

        Wait for __refresh_time sec / catch key every 100 ms.

N
Nicolargo 已提交
831
        INPUT
832 833 834 835 836
        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 已提交
837 838 839 840 841 842 843
        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...
844 845
        """
        # Flush display
846
        self.flush(stats, cs_status=cs_status)
847 848

        # Wait
N
Nicolargo 已提交
849
        exitkey = False
850
        countdown = Timer(self.__refresh_time)
N
Nicolargo 已提交
851
        while not countdown.finished() and not exitkey:
852
            # Getkey
N
Nicolargo 已提交
853 854 855 856
            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:
857
                # Redraw display
858
                self.flush(stats, cs_status=cs_status)
859 860 861
            # Wait 100ms...
            curses.napms(100)

N
Nicolargo 已提交
862 863
        return exitkey

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

A
PEP 257  
Alessio Sergi 已提交
867 868
        The height is defined by the maximum line.
        """
869
        try:
870
            if without_option:
871
                # Size without options
N
Nicolargo 已提交
872
                c = len(max(''.join([(re.sub(r'[^\x00-\x7F]+',' ', i['msg']) if not i['optional'] else "")
873
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
874 875
            else:
                # Size with all options
N
Nicolargo 已提交
876
                c = len(max(''.join([re.sub(r'[^\x00-\x7F]+',' ', i['msg'])
877
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
A
Alessio Sergi 已提交
878
        except Exception:
879 880 881 882
            return 0
        else:
            return c

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

A
PEP 257  
Alessio Sergi 已提交
886 887
        The height is defined by the number of '\n' (new line).
        """
888
        try:
A
Alessio Sergi 已提交
889
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
A
Alessio Sergi 已提交
890
        except Exception:
891 892 893
            return 0
        else:
            return c + 1
N
Nicolargo 已提交
894

895

896 897 898 899 900 901 902
class GlancesCursesStandalone(_GlancesCurses):

    """Class for the Glances' curse standalone"""

    pass


903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
class GlancesCursesClient(_GlancesCurses):

    """Class for the Glances' curse client"""

    pass


class GlancesCursesBrowser(_GlancesCurses):

    """Class for the Glances' curse client browser"""

    def __init__(self, args=None):
        # Init the father class
        _GlancesCurses.__init__(self, args=args)

N
Nicolargo 已提交
918 919
        _colors_list = {
            'UNKNOWN': self.no_color,
N
Nicolargo 已提交
920
            'SNMP': self.default_color2,
N
Nicolargo 已提交
921 922 923 924
            'ONLINE': self.default_color2,
            'OFFLINE': self.ifCRITICAL_color2,
            'PROTECTED': self.ifWARNING_color2,
        }
925
        self.colors_list.update(_colors_list)
N
Nicolargo 已提交
926

927 928 929 930
        # First time scan tag
        # Used to display a specific message when the browser is started
        self.first_scan = True

931 932 933 934
        # Init refresh time
        self.__refresh_time = args.time

        # Init the cursor position for the client browser
935
        self.cursor_position = 0
936

N
Nicolargo 已提交
937
        # Active Glances server number
938
        self._active_server = None
N
Nicolargo 已提交
939

940 941 942 943
    @property
    def active_server(self):
        """Return the active server or None if it's the browser list."""
        return self._active_server
N
Nicolargo 已提交
944

945 946 947 948
    @active_server.setter
    def active_server(self, index):
        """Set the active server or None if no server selected."""
        self._active_server = index
949

950 951 952
    @property
    def cursor(self):
        """Get the cursor position."""
953 954
        return self.cursor_position

955 956 957 958 959
    @cursor.setter
    def cursor(self, position):
        """Set the cursor position."""
        self.cursor_position = position

N
Nicolargo 已提交
960
    def cursor_up(self, servers_list):
961 962 963
        """Set the cursor to position N-1 in the list"""
        if self.cursor_position > 0:
            self.cursor_position -= 1
N
Nicolargo 已提交
964 965
        else:
            self.cursor_position = len(servers_list) - 1
966 967 968 969 970

    def cursor_down(self, servers_list):
        """Set the cursor to position N-1 in the list"""
        if self.cursor_position < len(servers_list) - 1:
            self.cursor_position += 1
N
Nicolargo 已提交
971 972
        else:
            self.cursor_position = 0
973 974 975 976 977 978 979 980 981 982 983 984 985

    def __catch_key(self, servers_list):
        # Catch the browser pressed key
        self.pressedkey = self.get_key(self.term_window)

        # Actions...
        if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
            # 'ESC'|'q' > Quit
            self.end()
            logger.info("Stop Glances client browser")
            sys.exit(0)
        elif self.pressedkey == 10:
            # 'ENTER' > Run Glances on the selected server
986 987
            logger.debug("Server number {0} selected".format(self.cursor + 1))
            self.active_server = self.cursor
988 989
        elif self.pressedkey == 259:
            # 'UP' > Up in the server list
N
Nicolargo 已提交
990
            self.cursor_up(servers_list)
991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
        elif self.pressedkey == 258:
            # 'DOWN' > Down in the server list
            self.cursor_down(servers_list)

        # Return the key code
        return self.pressedkey

    def update(self, servers_list):
        """Update the servers' list screen.

        Wait for __refresh_time sec / catch key every 100 ms.

        servers_list: Dict of dict with servers stats
        """
        # Flush display
        self.flush(servers_list)

        # Wait
N
Nicolargo 已提交
1009
        exitkey = False
1010
        countdown = Timer(self.__refresh_time)
N
Nicolargo 已提交
1011
        while not countdown.finished() and not exitkey:
1012
            # Getkey
N
Nicolargo 已提交
1013 1014
            pressedkey = self.__catch_key(servers_list)
            # Is it an exit or select server key ?
N
Nicolargo 已提交
1015 1016
            exitkey = (
                pressedkey == ord('\x1b') or pressedkey == ord('q') or pressedkey == 10)
N
Nicolargo 已提交
1017
            if not exitkey and pressedkey > -1:
1018 1019 1020 1021 1022
                # Redraw display
                self.flush(servers_list)
            # Wait 100ms...
            curses.napms(100)

1023
        return self.active_server
N
Nicolargo 已提交
1024

1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050
    def flush(self, servers_list):
        """Update the servers' list screen.
        servers_list: List of dict with servers stats
        """
        self.erase()
        self.display(servers_list)

    def display(self, servers_list):
        """Display the servers list
        Return:
            True if the stats have been displayed
            False if the stats have not been displayed (no server available)
        """
        # Init the internal line/column for Glances Curses
        self.init_line_column()

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

        # Init position
        x = 0
        y = 0

        # Display top header
        if len(servers_list) == 0:
N
Nicolargo 已提交
1051
            if self.first_scan and not self.args.disable_autodiscover:
1052
                msg = _("Glances is scanning your network (please wait)...")
N
Nicolargo 已提交
1053
                self.first_scan = False
1054
            else:
N
Nicolargo 已提交
1055
                msg = _("No Glances servers available")
1056
        elif len(servers_list) == 1:
N
Nicolargo 已提交
1057
            msg = _("One Glances server available")
1058
        else:
N
Nicolargo 已提交
1059
            msg = _("%d Glances servers available" %
1060
                    len(servers_list))
1061
        if self.args.disable_autodiscover:
N
Nicolargo 已提交
1062
            msg += ' ' + _("(auto discover is disabled)")
1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
        self.term_window.addnstr(y, x,
                                 msg,
                                 screen_x - x,
                                 self.colors_list['TITLE'])

        if len(servers_list) == 0:
            return False

        # Display the Glances server list
        #================================

        # Table of table
        # Item description: [stats_id, column name, column size]
        column_def = [
            ['name', _('Name'), 16],
1078
            ['alias', None, None],
1079 1080 1081
            ['load_min5', _('LOAD'), 6],
            ['cpu_percent', _('CPU%'), 5],
            ['mem_percent', _('MEM%'), 5],
N
Nicolargo 已提交
1082
            ['status', _('STATUS'), 8],
1083
            ['ip', _('IP'), 15],
1084
            # ['port', _('PORT'), 5],
1085 1086 1087 1088 1089 1090 1091 1092
            ['hr_name', _('OS'), 16],
        ]
        y = 2

        # Display table header
        cpt = 0
        xc = x + 2
        for c in column_def:
1093
            if xc < screen_x and y < screen_y and c[1] is not None:
N
Nicolargo 已提交
1094 1095 1096 1097 1098
                self.term_window.addnstr(y, xc,
                                         c[1],
                                         screen_x - x,
                                         self.colors_list['BOLD'])
                xc += c[2] + self.space_between_column
1099 1100 1101 1102 1103
            cpt += 1
        y += 1

        # If a servers has been deleted from the list...
        # ... and if the cursor is in the latest position
1104
        if self.cursor > len(servers_list) - 1:
1105
            # Set the cursor position to the latest item
1106
            self.cursor = len(servers_list) - 1
1107 1108 1109 1110 1111

        # Display table
        line = 0
        for v in servers_list:
            # Get server stats
1112 1113 1114
            server_stat = {}
            for c in column_def:
                try:
1115
                    server_stat[c[0]] = v[c[0]]
1116 1117
                except KeyError as e:
                    logger.debug(
A
Alessio Sergi 已提交
1118
                        "Cannot grab stats {0} from server (KeyError: {1})".format(c[0], e))
1119
                    server_stat[c[0]] = '?'
1120
                # Display alias instead of name
N
Nicolargo 已提交
1121 1122 1123 1124 1125
                try:
                    if c[0] == 'alias' and v[c[0]] is not None:
                        server_stat['name'] = v[c[0]]
                except KeyError as e:
                    pass
1126 1127 1128 1129 1130 1131

            # Display line for server stats
            cpt = 0
            xc = x

            # Is the line selected ?
1132
            if line == self.cursor:
1133
                # Display cursor
1134 1135
                self.term_window.addnstr(
                    y, xc, ">", screen_x - xc, self.colors_list['BOLD'])
1136

1137
            # Display the line
1138 1139
            xc += 2
            for c in column_def:
1140
                if xc < screen_x and y < screen_y and c[1] is not None:
N
Nicolargo 已提交
1141
                    # Display server stats
1142 1143
                    self.term_window.addnstr(
                        y, xc, format(server_stat[c[0]]), c[2], self.colors_list[v['status']])
N
Nicolargo 已提交
1144
                    xc += c[2] + self.space_between_column
1145 1146 1147 1148 1149 1150 1151 1152
                cpt += 1
            # Next line, next server...
            y += 1
            line += 1

        return True


N
Nicolargo 已提交
1153
if not is_windows:
A
Alessio Sergi 已提交
1154
    class GlancesTextbox(Textbox):
1155

N
Nicolargo 已提交
1156 1157
        def __init__(*args, **kwargs):
            Textbox.__init__(*args, **kwargs)
1158

N
Nicolargo 已提交
1159
        def do_command(self, ch):
1160
            if ch == 10:  # Enter
N
Nicolargo 已提交
1161
                return 0
1162
            if ch == 127:  # Enter
N
Nicolargo 已提交
1163
                return 8
1164
            return Textbox.do_command(self, ch)