glances_curses.py 47.0 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
# Import system lib
N
Nicolargo 已提交
23
import re
A
flake8  
Alessio Sergi 已提交
24
import sys
25 26

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

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


49
class _GlancesCurses(object):
50

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

    Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser.
54
    """
55

A
PEP 257  
Alessio Sergi 已提交
56
    def __init__(self, args=None):
57 58
        # Init args
        self.args = args
N
Nicolas Hennion 已提交
59

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

74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
        # 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

89 90 91
        # Init the process min/max reset
        self.args.reset_minmax_tag = False

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
        # Catch key pressed with non blocking mode
        self.no_flash_cursor()
        self.term_window.nodelay(1)
        self.pressedkey = -1

        # History tag
        self._init_history()

    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'''

118 119 120 121
        if hasattr(curses, 'noecho'):
            curses.noecho()
        if hasattr(curses, 'cbreak'):
            curses.cbreak()
N
Nicolargo 已提交
122
        self.set_cursor(0)
123

124 125 126 127 128 129 130 131 132
    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()

133
        # Init colors
134 135 136 137 138 139 140 141 142 143 144 145 146
        if self.args.disable_bold:
            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 curses.has_colors():
            # The screen is compatible with a colored design
            if self.args.theme_white:
                # White theme: black ==> white
N
Nicolargo 已提交
147 148 149
                curses.init_pair(1, curses.COLOR_BLACK, -1)
            else:
                curses.init_pair(1, curses.COLOR_WHITE, -1)
150 151 152 153 154 155 156 157 158
            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)

            # Colors text styles
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
            if curses.COLOR_PAIRS > 8:
                try:
                    curses.init_pair(9, curses.COLOR_MAGENTA, -1)
                except Exception:
                    if self.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)
                except Exception:
                    if self.args.theme_white:
                        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

179
            self.no_color = curses.color_pair(1)
A
Alessio Sergi 已提交
180
            self.default_color = curses.color_pair(3) | A_BOLD
181
            self.nice_color = curses.color_pair(9) | A_BOLD
182
            self.cpu_time_color = curses.color_pair(9) | A_BOLD
183 184 185 186 187
            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
188

189
        else:
190 191
            # The screen is NOT compatible with a colored design
            # switch to B&W text styles
192 193
            self.no_color = curses.A_NORMAL
            self.default_color = curses.A_NORMAL
194
            self.nice_color = A_BOLD
195
            self.cpu_time_color = A_BOLD
196 197 198 199 200 201 202
            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 已提交
203
            self.filter_color = A_BOLD
204 205

        # Define the colors list (hash table) for stats
206
        self.colors_list = {
207 208 209
            'DEFAULT': self.no_color,
            'UNDERLINE': curses.A_UNDERLINE,
            'BOLD': A_BOLD,
210
            'SORT': A_BOLD,
211
            'OK': self.default_color2,
N
Nicolargo 已提交
212
            'FILTER': self.filter_color,
213
            'TITLE': self.title_color,
214 215
            'PROCESS': self.default_color2,
            'STATUS': self.default_color2,
216
            'NICE': self.nice_color,
217
            'CPU_TIME': self.cpu_time_color,
218 219 220 221 222 223
            '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,
224 225
            'CRITICAL_LOG': self.ifCRITICAL_color,
            'PASSWORD': curses.A_PROTECT
226 227
        }

228 229 230 231 232 233
    def flash_cursor(self):
        self.term_window.keypad(1)

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

N
Nicolargo 已提交
234
    def set_cursor(self, value):
A
PEP 257  
Alessio Sergi 已提交
235 236 237 238 239 240
        """Configure the curse cursor apparence.

        0: invisible
        1: visible
        2: very visible
        """
N
Nicolargo 已提交
241 242 243 244 245 246
        if hasattr(curses, 'curs_set'):
            try:
                curses.curs_set(value)
            except Exception:
                pass

247
    def get_key(self, window):
A
PEP 257  
Alessio Sergi 已提交
248
        # Catch ESC key AND numlock key (issue #163)
249 250 251 252
        keycode = [0, 0]
        keycode[0] = window.getch()
        keycode[1] = window.getch()

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

256 257 258 259 260 261
        if keycode[0] == 27 and keycode[1] != -1:
            # Do not escape on specials keys
            return -1
        else:
            return keycode[0]

N
Nicolargo 已提交
262
    def __catch_key(self, return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
263
        # Catch the pressed key
264
        self.pressedkey = self.get_key(self.term_window)
265 266 267 268

        # Actions...
        if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
            # 'ESC'|'q' > Quit
N
Nicolargo 已提交
269 270 271 272 273 274
            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 已提交
275 276 277
        elif self.pressedkey == 10:
            # 'ENTER' > Edit the process filter
            self.edit_filter = not self.edit_filter
278 279 280
        elif self.pressedkey == ord('0'):
            # '0' > Switch between IRIX and Solaris mode
            self.args.disable_irix = not self.args.disable_irix
281 282 283
        elif self.pressedkey == ord('1'):
            # '1' > Switch between CPU and PerCPU information
            self.args.percpu = not self.args.percpu
284 285
        elif self.pressedkey == ord('2'):
            # '2' > Enable/disable left sidebar
286
            self.args.disable_left_sidebar = not self.args.disable_left_sidebar
N
nicolargo 已提交
287 288 289
        elif self.pressedkey == ord('3'):
            # '3' > Enable/disable quicklook
            self.args.disable_quicklook = not self.args.disable_quicklook
N
Nicolargo 已提交
290 291 292 293 294 295 296 297 298 299 300 301 302
        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
303 304 305
        elif self.pressedkey == ord('/'):
            # '/' > Switch between short/long name for processes
            self.args.process_short_name = not self.args.process_short_name
306
        elif self.pressedkey == ord('a'):
307 308 309
            # 'a' > Sort processes automatically and reset to 'cpu_percent'
            glances_processes.auto_sort = True
            glances_processes.sort_key = 'cpu_percent'
310 311 312 313 314 315
        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
316 317
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'cpu_percent'
318
        elif self.pressedkey == ord('d'):
319
            # 'd' > Show/hide disk I/O stats
320
            self.args.disable_diskio = not self.args.disable_diskio
N
Nicolargo 已提交
321 322 323
        elif self.pressedkey == ord('D'):
            # 'D' > Show/hide Docker stats
            self.args.disable_docker = not self.args.disable_docker
N
Nicolargo 已提交
324 325
        elif self.pressedkey == ord('e'):
            # 'e' > Enable/Disable extended stats for top process
326 327
            self.args.enable_process_extended = not self.args.enable_process_extended
            if not self.args.enable_process_extended:
N
Nicolargo 已提交
328 329 330
                glances_processes.disable_extended()
            else:
                glances_processes.enable_extended()
331 332 333 334
        elif self.pressedkey == ord('E'):
            # 'E' > Erase the process filter
            logger.info("Erase process filter")
            glances_processes.process_filter = None
335 336 337
        elif self.pressedkey == ord('F'):
            # 'F' > Switch between FS available and free space
            self.args.fs_free_space = not self.args.fs_free_space
338
        elif self.pressedkey == ord('f'):
339
            # 'f' > Show/hide fs stats
340
            self.args.disable_fs = not self.args.disable_fs
341 342 343
        elif self.pressedkey == ord('g'):
            # 'g' > History
            self.history_tag = not self.history_tag
344 345
        elif self.pressedkey == ord('h'):
            # 'h' > Show/hide help
346
            self.args.help_tag = not self.args.help_tag
347
        elif self.pressedkey == ord('i'):
348
            # 'i' > Sort processes by IO rate (not available on OS X)
349 350
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'io_counters'
351 352 353
        elif self.pressedkey == ord('I'):
            # 'I' > Show/hide IP module
            self.args.disable_ip = not self.args.disable_ip
354 355
        elif self.pressedkey == ord('l'):
            # 'l' > Show/hide log messages
356
            self.args.disable_log = not self.args.disable_log
357 358
        elif self.pressedkey == ord('m'):
            # 'm' > Sort processes by MEM usage
359 360
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'memory_percent'
361 362 363
        elif self.pressedkey == ord('M'):
            # 'M' > Reset processes summary min/max
            self.args.reset_minmax_tag = not self.args.reset_minmax_tag
364
        elif self.pressedkey == ord('n'):
365
            # 'n' > Show/hide network stats
366
            self.args.disable_network = not self.args.disable_network
367 368
        elif self.pressedkey == ord('p'):
            # 'p' > Sort processes by name
369 370
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'name'
371 372
        elif self.pressedkey == ord('r'):
            # 'r' > Reset history
373
            self.reset_history_tag = not self.reset_history_tag
N
Nicolargo 已提交
374 375 376
        elif self.pressedkey == ord('R'):
            # 'R' > Hide RAID plugins
            self.args.disable_raid = not self.args.disable_raid
377 378
        elif self.pressedkey == ord('s'):
            # 's' > Show/hide sensors stats (Linux-only)
379
            self.args.disable_sensors = not self.args.disable_sensors
380
        elif self.pressedkey == ord('t'):
381
            # 't' > Sort processes by TIME usage
382 383
            glances_processes.auto_sort = False
            glances_processes.sort_key = 'cpu_times'
384 385
        elif self.pressedkey == ord('T'):
            # 'T' > View network traffic as sum Rx+Tx
386
            self.args.network_sum = not self.args.network_sum
387
        elif self.pressedkey == ord('u'):
A
Alessio Sergi 已提交
388 389 390 391 392
            # '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)
393
            self.args.network_cumul = not self.args.network_cumul
394 395 396 397 398 399
        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)
400 401 402 403 404
        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
405
            if self.args.disable_process:
406 407 408
                glances_processes.disable()
            else:
                glances_processes.enable()
409 410 411 412
        # Return the key code
        return self.pressedkey

    def end(self):
413
        """Shutdown the curses window."""
N
Nicolas Hennion 已提交
414 415 416 417 418 419 420 421 422
        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
423
        curses.endwin()
424

425
    def init_line_column(self):
A
PEP 257  
Alessio Sergi 已提交
426
        """Init the line and column position for the curses inteface."""
427 428
        self.init_line()
        self.init_column()
429 430

    def init_line(self):
A
PEP 257  
Alessio Sergi 已提交
431
        """Init the line position for the curses inteface."""
432 433 434 435
        self.line = 0
        self.next_line = 0

    def init_column(self):
A
PEP 257  
Alessio Sergi 已提交
436
        """Init the column position for the curses inteface."""
437 438 439 440
        self.column = 0
        self.next_column = 0

    def new_line(self):
A
PEP 257  
Alessio Sergi 已提交
441
        """New line in the curses interface."""
442 443 444
        self.line = self.next_line

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

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

451 452 453
        stats: Stats database to display
        cs_status:
            "None": standalone or server mode
454 455
            "Connected": Client is connected to a Glances server
            "SNMP": Client is connected to a SNMP server
456
            "Disconnected": Client is disconnected from the server
457 458 459 460

        Return:
            True if the stats have been displayed
            False if the help have been displayed
461
        """
462 463
        # Init the internal line/column for Glances Curses
        self.init_line_column()
464 465 466 467 468

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

469 470 471 472 473 474 475 476 477 478 479 480
        # 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
481 482
        stats_system = stats.get_plugin(
            'system').get_stats_display(args=self.args)
483 484
        stats_uptime = stats.get_plugin('uptime').get_stats_display()
        if self.args.percpu:
485
            stats_cpu = stats.get_plugin('percpu').get_stats_display(args=self.args)
486
        else:
487 488 489 490
            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)
491 492
        stats_network = stats.get_plugin('network').get_stats_display(
            args=self.args, max_width=plugin_max_width)
493 494 495 496
        try:
            stats_ip = stats.get_plugin('ip').get_stats_display(args=self.args)
        except AttributeError:
            stats_ip = None
497 498 499 500
        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 已提交
501
        stats_raid = stats.get_plugin('raid').get_stats_display(
502
            args=self.args)
503 504
        stats_sensors = stats.get_plugin(
            'sensors').get_stats_display(args=self.args)
505
        stats_now = stats.get_plugin('now').get_stats_display()
N
Nicolargo 已提交
506 507
        stats_docker = stats.get_plugin('docker').get_stats_display(
            args=self.args)
508 509 510 511 512 513
        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)
514

515
        # Adapt number of processes to the available space
516
        max_processes_displayed = screen_y - 11 - \
N
Nicolargo 已提交
517 518
            self.get_stats_display_height(stats_alert) - \
            self.get_stats_display_height(stats_docker)
519 520 521 522 523
        try:
            if self.args.enable_process_extended and not self.args.process_tree:
                max_processes_displayed -= 4
        except AttributeError:
            pass
524
        if max_processes_displayed < 0:
525
            max_processes_displayed = 0
526 527 528 529
        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
530

531 532
        stats_processlist = stats.get_plugin(
            'processlist').get_stats_display(args=self.args)
533

534 535 536 537
        # Display the stats on the curses interface
        ###########################################

        # Help screen (on top of the other stats)
538
        if self.args.help_tag:
539
            # Display the stats...
540 541
            self.display_plugin(
                stats.get_plugin('help').get_stats_display(args=self.args))
542 543 544
            # ... and exit
            return False

N
Nicolargo 已提交
545
        # ==================================
546
        # Display first line (system+uptime)
N
Nicolargo 已提交
547
        # ==================================
548 549
        # Space between column
        self.space_between_column = 0
550
        self.new_line()
551
        l_uptime = self.get_stats_display_width(
N
Nicolargo 已提交
552
            stats_system) + self.space_between_column + self.get_stats_display_width(stats_ip) + 3 + self.get_stats_display_width(stats_uptime)
553 554 555 556 557 558
        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
559
        self.new_column()
560
        self.display_plugin(stats_uptime)
A
Alessio Sergi 已提交
561

N
Nicolargo 已提交
562
        # ========================================================
N
Nicolargo 已提交
563
        # Display second line (<SUMMARY>+CPU|PERCPU+LOAD+MEM+SWAP)
N
Nicolargo 已提交
564
        # ========================================================
565
        self.init_column()
566
        self.new_line()
567

N
Nicolargo 已提交
568 569
        # Init quicklook
        stats_quicklook = {'msgdict': []}
570 571 572 573 574
        quicklook_width = 0

        # Get stats for CPU, MEM, SWAP and LOAD (if needed)
        if self.args.disable_cpu:
            cpu_width = 0
575
        else:
576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
            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
594 595 596 597 598
        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'] != []))
599 600 601

        if not self.args.disable_quicklook:
            # Quick look is in the place !
602 603 604 605
            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 已提交
606 607 608 609 610 611
            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:
612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
                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
646
        self.display_plugin(stats_cpu, display_optional=display_optional_cpu)
647
        self.new_column()
648
        self.display_plugin(stats_mem, display_optional=display_optional_mem)
649
        self.new_column()
650
        self.display_plugin(stats_memswap)
N
Nicolargo 已提交
651 652
        self.new_column()
        self.display_plugin(stats_load)
653

654 655 656
        # Space between column
        self.space_between_column = 3

657 658 659
        # Backup line position
        self.saved_line = self.next_line

660
        # ==================================================================
661
        # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time)
662
        # ==================================================================
663
        self.init_column()
A
flake8  
Alessio Sergi 已提交
664 665 666
        if not (self.args.disable_network and self.args.disable_diskio and
                self.args.disable_fs and self.args.disable_raid and
                self.args.disable_sensors) and not self.args.disable_left_sidebar:
667 668 669 670 671 672 673
            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 已提交
674 675
            self.display_plugin(stats_raid)
            self.new_line()
676 677 678
            self.display_plugin(stats_sensors)
            self.new_line()
            self.display_plugin(stats_now)
679

N
Nicolargo 已提交
680 681 682
        # ====================================
        # Display right stats (process and co)
        # ====================================
683
        # If space available...
684
        if screen_x > 52:
685 686 687
            # Restore line position
            self.next_line = self.saved_line

688
            # Display right sidebar
N
Nicolargo 已提交
689
            # ((DOCKER)+PROCESS_COUNT+(MONITORED)+PROCESS_LIST+ALERT)
690 691
            self.new_column()
            self.new_line()
N
Nicolargo 已提交
692 693
            self.display_plugin(stats_docker)
            self.new_line()
694
            self.display_plugin(stats_processcount)
695
            if glances_processes.process_filter is None and cs_status is None:
N
Nicolargo 已提交
696 697 698
                # Do not display stats monitor list if a filter exist
                self.new_line()
                self.display_plugin(stats_monitor)
699
            self.new_line()
A
Alessio Sergi 已提交
700
            self.display_plugin(stats_processlist,
701
                                display_optional=(screen_x > 102),
702
                                display_additional=(not is_mac),
A
Alessio Sergi 已提交
703
                                max_y=(screen_y - self.get_stats_display_height(stats_alert) - 2))
704
            self.new_line()
705 706
            self.display_plugin(stats_alert)

707 708 709
        # History option
        # Generate history graph
        if self.history_tag and self.args.enable_history:
710
            self.display_popup(
A
Alessio Sergi 已提交
711 712
                'Generate graphs history in {0}\nPlease wait...'.format(
                    self.glances_history.get_output_folder()))
N
Nicolargo 已提交
713
            self.display_popup(
A
Alessio Sergi 已提交
714 715 716
                'Generate graphs history in {0}\nDone: {1} graphs generated'.format(
                    self.glances_history.get_output_folder(),
                    self.glances_history.generate_graph(stats)))
717
        elif self.reset_history_tag and self.args.enable_history:
A
Alessio Sergi 已提交
718
            self.display_popup('Reset history')
719
            self.glances_history.reset(stats)
720
        elif (self.history_tag or self.reset_history_tag) and not self.args.enable_history:
721 722
            try:
                self.glances_history.graph_enabled()
A
Alessio Sergi 已提交
723
            except Exception:
A
Alessio Sergi 已提交
724
                self.display_popup('History disabled\nEnable it using --enable-history')
725
            else:
A
Alessio Sergi 已提交
726
                self.display_popup('History disabled\nPlease install matplotlib')
727 728 729
        self.history_tag = False
        self.reset_history_tag = False

N
Nicolargo 已提交
730
        # Display edit filter popup
731 732
        # Only in standalone mode (cs_status is None)
        if self.edit_filter and cs_status is None:
A
Alessio Sergi 已提交
733 734 735
            new_filter = self.display_popup(
                'Process filter pattern: ', is_input=True,
                input_value=glances_processes.process_filter)
736
            glances_processes.process_filter = new_filter
737
        elif self.edit_filter and cs_status != 'None':
A
Alessio Sergi 已提交
738
            self.display_popup('Process filter only available in standalone mode')
N
Nicolargo 已提交
739 740
        self.edit_filter = False

741 742
        return True

743 744
    def display_popup(self, message,
                      size_x=None, size_y=None,
N
Nicolargo 已提交
745 746
                      duration=3,
                      is_input=False,
N
Nicolargo 已提交
747
                      input_size=30,
N
Nicolargo 已提交
748
                      input_value=None):
749
        """
A
PEP 257  
Alessio Sergi 已提交
750 751
        Display a centered popup.

N
Nicolargo 已提交
752 753 754 755 756
        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 已提交
757

N
Nicolargo 已提交
758 759 760 761
        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
762
         Return the input string or None if the field is empty
763 764
        """
        # Center the popup
N
Nicolargo 已提交
765
        sentence_list = message.split('\n')
766
        if size_x is None:
N
Nicolargo 已提交
767
            size_x = len(max(sentence_list, key=len)) + 4
N
Nicolargo 已提交
768 769 770
            # Add space for the input field
            if is_input:
                size_x += input_size
771
        if size_y is None:
N
Nicolargo 已提交
772
            size_y = len(sentence_list) + 4
773 774 775 776
        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
777
            return False
778 779
        pos_x = int((screen_x - size_x) / 2)
        pos_y = int((screen_y - size_y) / 2)
780 781 782

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

784 785 786 787
        # Fill the popup
        popup.border()

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

N
Nicolargo 已提交
791
        if is_input and not is_windows:
N
Nicolargo 已提交
792 793
            # Create a subwindow for the text field
            subpop = popup.derwin(1, input_size, 2, 2 + len(m))
794
            subpop.attron(self.colors_list['FILTER'])
N
Nicolargo 已提交
795 796 797 798 799 800 801 802
            # 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)
803
            self.flash_cursor()
A
Alessio Sergi 已提交
804
            textbox = GlancesTextbox(subpop, insert_mode=False)
N
Nicolargo 已提交
805 806
            textbox.edit()
            self.set_cursor(0)
807
            self.no_flash_cursor()
N
Nicolargo 已提交
808
            if textbox.gather() != '':
N
Nicolargo 已提交
809
                logger.debug(
810
                    "User enters the following string: %s" % textbox.gather())
N
Nicolargo 已提交
811 812
                return textbox.gather()[:-1]
            else:
813
                logger.debug("User centers an empty string")
N
Nicolargo 已提交
814 815 816 817 818 819
                return None
        else:
            # Display the popup
            popup.refresh()
            curses.napms(duration * 1000)
            return True
820

821
    def display_plugin(self, plugin_stats,
822
                       display_optional=True,
823
                       display_additional=True,
824
                       max_y=65535):
A
PEP 257  
Alessio Sergi 已提交
825 826
        """Display the plugin_stats on the screen.

827 828
        If display_optional=True display the optional stats
        If display_additional=True display additionnal stats
829 830
        max_y do not display line > max_y
        """
831 832 833
        # Exit if:
        # - the plugin_stats message is empty
        # - the display tag = False
834
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
835
            # Exit
836 837 838 839 840 841 842
            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
843
        if plugin_stats['align'] == 'right':
844
            # Right align (last column)
845
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
846
        else:
847
            display_x = self.column
848
        if plugin_stats['align'] == 'bottom':
849
            # Bottom (last line)
850
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
851
        else:
852
            display_y = self.line
853

854 855
        # Display
        x = display_x
856
        x_max = x
857 858 859
        y = display_y
        for m in plugin_stats['msgdict']:
            # New line
860
            if m['msg'].startswith('\n'):
861
                # Go to the next line
862
                y += 1
863 864 865 866
                # Return to the first column
                x = display_x
                continue
            # Do not display outside the screen
867
            if x < 0:
868
                continue
869
            if not m['splittable'] and (x + len(m['msg']) > screen_x):
870
                continue
871
            if y < 0 or (y + 1 > screen_y) or (y > max_y):
872 873
                break
            # If display_optional = False do not display optional stats
874
            if not display_optional and m['optional']:
875
                continue
876 877 878
            # If display_additional = False do not display additional stats
            if not display_additional and m['additional']:
                continue
879 880 881
            # Is it possible to display the stat with the current screen size
            # !!! Crach if not try/except... Why ???
            try:
A
Alessio Sergi 已提交
882 883
                self.term_window.addnstr(y, x,
                                         m['msg'],
884 885
                                         # Do not disply outside the screen
                                         screen_x - x,
886
                                         self.colors_list[m['decoration']])
A
Alessio Sergi 已提交
887
            except Exception:
888 889 890
                pass
            else:
                # New column
A
Alessio Sergi 已提交
891 892 893 894 895
                # 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.
                offset = len(u(m['msg']))
896
                x += offset
897 898
                if x > x_max:
                    x_max = x
899 900

        # Compute the next Glances column/line position
N
Nicolargo 已提交
901 902
        self.next_column = max(
            self.next_column, x_max + self.space_between_column)
903
        self.next_line = max(self.next_line, y + self.space_between_line)
904 905

    def erase(self):
A
PEP 257  
Alessio Sergi 已提交
906
        """Erase the content of the screen."""
907 908
        self.term_window.erase()

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

912 913 914 915 916 917 918
        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()
919
        self.display(stats, cs_status=cs_status)
920

921
    def update(self, stats, cs_status=None, return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
922 923 924 925
        """Update the screen.

        Wait for __refresh_time sec / catch key every 100 ms.

N
Nicolargo 已提交
926
        INPUT
927 928 929 930 931
        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 已提交
932 933 934 935 936 937 938
        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...
939 940
        """
        # Flush display
941
        self.flush(stats, cs_status=cs_status)
942 943

        # Wait
N
Nicolargo 已提交
944
        exitkey = False
945
        countdown = Timer(self.__refresh_time)
N
Nicolargo 已提交
946
        while not countdown.finished() and not exitkey:
947
            # Getkey
N
Nicolargo 已提交
948 949 950 951
            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:
952
                # Redraw display
953
                self.flush(stats, cs_status=cs_status)
954 955 956
            # Wait 100ms...
            curses.napms(100)

N
Nicolargo 已提交
957 958
        return exitkey

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

A
PEP 257  
Alessio Sergi 已提交
962 963
        The height is defined by the maximum line.
        """
964
        try:
965
            if without_option:
966
                # Size without options
A
flake8  
Alessio Sergi 已提交
967
                c = len(max(''.join([(re.sub(r'[^\x00-\x7F]+', ' ', i['msg']) if not i['optional'] else "")
968
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
969 970
            else:
                # Size with all options
A
flake8  
Alessio Sergi 已提交
971
                c = len(max(''.join([re.sub(r'[^\x00-\x7F]+', ' ', i['msg'])
972
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
A
Alessio Sergi 已提交
973
        except Exception:
974 975 976 977
            return 0
        else:
            return c

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

A
PEP 257  
Alessio Sergi 已提交
981 982
        The height is defined by the number of '\n' (new line).
        """
983
        try:
A
Alessio Sergi 已提交
984
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
A
Alessio Sergi 已提交
985
        except Exception:
986 987 988
            return 0
        else:
            return c + 1
N
Nicolargo 已提交
989

990

991 992
class GlancesCursesStandalone(_GlancesCurses):

A
PEP 257  
Alessio Sergi 已提交
993
    """Class for the Glances curse standalone."""
994 995 996 997

    pass


998 999
class GlancesCursesClient(_GlancesCurses):

A
PEP 257  
Alessio Sergi 已提交
1000
    """Class for the Glances curse client."""
1001 1002 1003 1004 1005 1006

    pass


class GlancesCursesBrowser(_GlancesCurses):

A
PEP 257  
Alessio Sergi 已提交
1007
    """Class for the Glances curse client browser."""
1008 1009 1010

    def __init__(self, args=None):
        # Init the father class
A
Alessio Sergi 已提交
1011
        super(GlancesCursesBrowser, self).__init__(args=args)
1012

N
Nicolargo 已提交
1013 1014
        _colors_list = {
            'UNKNOWN': self.no_color,
N
Nicolargo 已提交
1015
            'SNMP': self.default_color2,
N
Nicolargo 已提交
1016 1017 1018 1019
            'ONLINE': self.default_color2,
            'OFFLINE': self.ifCRITICAL_color2,
            'PROTECTED': self.ifWARNING_color2,
        }
1020
        self.colors_list.update(_colors_list)
N
Nicolargo 已提交
1021

1022 1023 1024 1025
        # First time scan tag
        # Used to display a specific message when the browser is started
        self.first_scan = True

1026 1027 1028 1029
        # Init refresh time
        self.__refresh_time = args.time

        # Init the cursor position for the client browser
1030
        self.cursor_position = 0
1031

N
Nicolargo 已提交
1032
        # Active Glances server number
1033
        self._active_server = None
N
Nicolargo 已提交
1034

1035 1036 1037 1038
    @property
    def active_server(self):
        """Return the active server or None if it's the browser list."""
        return self._active_server
N
Nicolargo 已提交
1039

1040 1041 1042 1043
    @active_server.setter
    def active_server(self, index):
        """Set the active server or None if no server selected."""
        self._active_server = index
1044

1045 1046 1047
    @property
    def cursor(self):
        """Get the cursor position."""
1048 1049
        return self.cursor_position

1050 1051 1052 1053 1054
    @cursor.setter
    def cursor(self, position):
        """Set the cursor position."""
        self.cursor_position = position

N
Nicolargo 已提交
1055
    def cursor_up(self, servers_list):
A
PEP 257  
Alessio Sergi 已提交
1056
        """Set the cursor to position N-1 in the list."""
1057 1058
        if self.cursor_position > 0:
            self.cursor_position -= 1
N
Nicolargo 已提交
1059 1060
        else:
            self.cursor_position = len(servers_list) - 1
1061 1062

    def cursor_down(self, servers_list):
A
PEP 257  
Alessio Sergi 已提交
1063
        """Set the cursor to position N-1 in the list."""
1064 1065
        if self.cursor_position < len(servers_list) - 1:
            self.cursor_position += 1
N
Nicolargo 已提交
1066 1067
        else:
            self.cursor_position = 0
1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080

    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
1081 1082
            logger.debug("Server number {0} selected".format(self.cursor + 1))
            self.active_server = self.cursor
1083 1084
        elif self.pressedkey == 259:
            # 'UP' > Up in the server list
N
Nicolargo 已提交
1085
            self.cursor_up(servers_list)
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100
        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
1101
        logger.debug('Servers list: {0}'.format(servers_list))
1102 1103 1104
        self.flush(servers_list)

        # Wait
N
Nicolargo 已提交
1105
        exitkey = False
1106
        countdown = Timer(self.__refresh_time)
N
Nicolargo 已提交
1107
        while not countdown.finished() and not exitkey:
1108
            # Getkey
N
Nicolargo 已提交
1109 1110
            pressedkey = self.__catch_key(servers_list)
            # Is it an exit or select server key ?
N
Nicolargo 已提交
1111 1112
            exitkey = (
                pressedkey == ord('\x1b') or pressedkey == ord('q') or pressedkey == 10)
N
Nicolargo 已提交
1113
            if not exitkey and pressedkey > -1:
1114 1115 1116 1117 1118
                # Redraw display
                self.flush(servers_list)
            # Wait 100ms...
            curses.napms(100)

1119
        return self.active_server
N
Nicolargo 已提交
1120

1121 1122
    def flush(self, servers_list):
        """Update the servers' list screen.
A
PEP 257  
Alessio Sergi 已提交
1123

1124 1125 1126 1127 1128 1129
        servers_list: List of dict with servers stats
        """
        self.erase()
        self.display(servers_list)

    def display(self, servers_list):
A
PEP 257  
Alessio Sergi 已提交
1130 1131
        """Display the servers list.

1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148
        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 已提交
1149
            if self.first_scan and not self.args.disable_autodiscover:
A
Alessio Sergi 已提交
1150
                msg = 'Glances is scanning your network. Please wait...'
N
Nicolargo 已提交
1151
                self.first_scan = False
1152
            else:
A
Alessio Sergi 已提交
1153
                msg = 'No Glances server available'
1154
        elif len(servers_list) == 1:
A
Alessio Sergi 已提交
1155
            msg = 'One Glances server available'
1156
        else:
A
Alessio Sergi 已提交
1157
            msg = '{0} Glances servers available'.format(len(servers_list))
1158
        if self.args.disable_autodiscover:
A
Alessio Sergi 已提交
1159
            msg += ' ' + '(auto discover is disabled)'
1160 1161 1162 1163 1164 1165 1166 1167 1168
        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
A
flake8  
Alessio Sergi 已提交
1169
        # ================================
1170 1171 1172 1173

        # Table of table
        # Item description: [stats_id, column name, column size]
        column_def = [
A
Alessio Sergi 已提交
1174
            ['name', 'Name', 16],
1175
            ['alias', None, None],
A
Alessio Sergi 已提交
1176 1177 1178
            ['load_min5', 'LOAD', 6],
            ['cpu_percent', 'CPU%', 5],
            ['mem_percent', 'MEM%', 5],
1179
            ['status', 'STATUS', 9],
A
Alessio Sergi 已提交
1180 1181 1182
            ['ip', 'IP', 15],
            # ['port', 'PORT', 5],
            ['hr_name', 'OS', 16],
1183 1184 1185 1186 1187
        ]
        y = 2

        # Display table header
        xc = x + 2
N
nicolargo 已提交
1188
        for cpt, c in enumerate(column_def):
1189
            if xc < screen_x and y < screen_y and c[1] is not None:
N
Nicolargo 已提交
1190 1191 1192 1193 1194
                self.term_window.addnstr(y, xc,
                                         c[1],
                                         screen_x - x,
                                         self.colors_list['BOLD'])
                xc += c[2] + self.space_between_column
1195 1196 1197 1198
        y += 1

        # If a servers has been deleted from the list...
        # ... and if the cursor is in the latest position
1199
        if self.cursor > len(servers_list) - 1:
1200
            # Set the cursor position to the latest item
1201
            self.cursor = len(servers_list) - 1
1202 1203 1204 1205 1206

        # Display table
        line = 0
        for v in servers_list:
            # Get server stats
1207 1208 1209
            server_stat = {}
            for c in column_def:
                try:
1210
                    server_stat[c[0]] = v[c[0]]
1211 1212
                except KeyError as e:
                    logger.debug(
A
Alessio Sergi 已提交
1213
                        "Cannot grab stats {0} from server (KeyError: {1})".format(c[0], e))
1214
                    server_stat[c[0]] = '?'
1215
                # Display alias instead of name
N
Nicolargo 已提交
1216 1217 1218
                try:
                    if c[0] == 'alias' and v[c[0]] is not None:
                        server_stat['name'] = v[c[0]]
A
Alessio Sergi 已提交
1219
                except KeyError:
N
Nicolargo 已提交
1220
                    pass
1221 1222 1223 1224 1225 1226

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

            # Is the line selected ?
1227
            if line == self.cursor:
1228
                # Display cursor
1229 1230
                self.term_window.addnstr(
                    y, xc, ">", screen_x - xc, self.colors_list['BOLD'])
1231

1232
            # Display the line
1233 1234
            xc += 2
            for c in column_def:
1235
                if xc < screen_x and y < screen_y and c[1] is not None:
N
Nicolargo 已提交
1236
                    # Display server stats
1237 1238
                    self.term_window.addnstr(
                        y, xc, format(server_stat[c[0]]), c[2], self.colors_list[v['status']])
N
Nicolargo 已提交
1239
                    xc += c[2] + self.space_between_column
1240 1241 1242 1243 1244 1245 1246
                cpt += 1
            # Next line, next server...
            y += 1
            line += 1

        return True

N
Nicolargo 已提交
1247
if not is_windows:
1248
    class GlancesTextbox(Textbox, object):
1249

1250 1251
        def __init__(self, *args, **kwargs):
            super(GlancesTextbox, self).__init__(*args, **kwargs)
1252

N
Nicolargo 已提交
1253
        def do_command(self, ch):
1254
            if ch == 10:  # Enter
N
Nicolargo 已提交
1255
                return 0
1256
            if ch == 127:  # Back
N
Nicolargo 已提交
1257
                return 8
1258
            return super(GlancesTextbox, self).do_command(ch)