glances_curses.py 41.6 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 24 25
# Import system lib
import sys

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

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


46
class _GlancesCurses(object):
47

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

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

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

        # 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 已提交
80
        self.set_cursor(0)
81 82 83 84 85 86

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

113 114 115
        else:
            self.hascolors = False

A
Alessio Sergi 已提交
116
        if args.disable_bold:
117 118 119 120 121 122 123 124 125 126
            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 已提交
127
            self.default_color = curses.color_pair(3) | A_BOLD
128
            self.nice_color = curses.color_pair(9) | A_BOLD
129 130 131 132 133 134 135
            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 已提交
136
            self.filter_color = curses.color_pair(10) | A_BOLD
137 138 139 140
        else:
            # B&W text styles
            self.no_color = curses.A_NORMAL
            self.default_color = curses.A_NORMAL
141
            self.nice_color = A_BOLD
142 143 144 145 146 147 148
            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 已提交
149
            self.filter_color = A_BOLD
150 151

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

        # Init process sort method
        self.args.process_sorted_by = 'auto'

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

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

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

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

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

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

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

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

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

    def end(self):
347
        """Shutdown the curses window."""
N
Nicolas Hennion 已提交
348 349 350 351 352 353 354 355 356
        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
357
        curses.endwin()
358

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
    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

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

387 388 389
        stats: Stats database to display
        cs_status:
            "None": standalone or server mode
390 391
            "Connected": Client is connected to a Glances server
            "SNMP": Client is connected to a SNMP server
392
            "Disconnected": Client is disconnected from the server
393 394 395 396

        Return:
            True if the stats have been displayed
            False if the help have been displayed
397
        """
398 399
        # Init the internal line/column for Glances Curses
        self.init_line_column()
400 401 402 403 404

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

405 406 407 408 409 410 411 412 413 414 415 416
        # 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
417 418
        stats_system = stats.get_plugin(
            'system').get_stats_display(args=self.args)
419 420 421 422 423 424 425 426
        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()
427 428 429 430 431 432
        stats_network = stats.get_plugin('network').get_stats_display(
            args=self.args, max_width=plugin_max_width)
        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 已提交
433 434
        stats_raid = stats.get_plugin('raid').get_stats_display(
            args=self.args, max_width=plugin_max_width)
435 436
        stats_sensors = stats.get_plugin(
            'sensors').get_stats_display(args=self.args)
437
        stats_now = stats.get_plugin('now').get_stats_display()
N
Nicolargo 已提交
438 439
        stats_docker = stats.get_plugin('docker').get_stats_display(
            args=self.args)
440 441 442 443 444 445
        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)
446

447
        # Adapt number of processes to the available space
448
        max_processes_displayed = screen_y - 11 - \
N
Nicolargo 已提交
449 450
            self.get_stats_display_height(stats_alert) - \
            self.get_stats_display_height(stats_docker)
451
        if self.args.enable_process_extended and not self.args.process_tree:
452 453
            max_processes_displayed -= 4
        if max_processes_displayed < 0:
454
            max_processes_displayed = 0
455 456
        if glances_processes.get_max_processes() is None or \
           glances_processes.get_max_processes() != max_processes_displayed:
N
Nicolargo 已提交
457 458
            logger.debug("Set number of displayed processes to %s" %
                         max_processes_displayed)
459 460
            glances_processes.set_max_processes(max_processes_displayed)

461 462
        stats_processlist = stats.get_plugin(
            'processlist').get_stats_display(args=self.args)
463

464 465 466 467
        # Display the stats on the curses interface
        ###########################################

        # Help screen (on top of the other stats)
468
        if self.args.help_tag:
469
            # Display the stats...
470 471
            self.display_plugin(
                stats.get_plugin('help').get_stats_display(args=self.args))
472 473 474
            # ... and exit
            return False

475
        # Display first line (system+uptime)
476
        self.new_line()
477 478
        l = self.get_stats_display_width(
            stats_system) + self.get_stats_display_width(stats_uptime) + self.space_between_column
479
        self.display_plugin(stats_system, display_optional=(screen_x >= l))
480
        self.new_column()
481
        self.display_plugin(stats_uptime)
A
Alessio Sergi 已提交
482

483 484
        # Display second line (CPU|PERCPU+LOAD+MEM+SWAP+<SUMMARY>)
        # CPU|PERCPU
485
        self.init_column()
486
        self.new_line()
487
        if self.args.percpu:
488
            l = self.get_stats_display_width(stats_percpu)
489
        else:
490
            l = self.get_stats_display_width(stats_cpu)
491 492
        l += self.get_stats_display_width(stats_load) + self.get_stats_display_width(
            stats_mem) + self.get_stats_display_width(stats_memswap)
493
        # Space between column
494 495
        space_number = int(stats_load['msgdict'] != [
        ]) + int(stats_mem['msgdict'] != []) + int(stats_memswap['msgdict'] != [])
496 497
        if space_number == 0:
            space_number = 1
N
Nicolargo 已提交
498 499
        if screen_x > (space_number * self.space_between_column + l):
            self.space_between_column = int((screen_x - l) / space_number)
500
        # Display
501
        if self.args.percpu:
502 503
            self.display_plugin(stats_percpu)
        else:
504
            self.display_plugin(stats_cpu, display_optional=(screen_x >= 80))
505
        self.new_column()
506
        self.display_plugin(stats_load)
507
        self.new_column()
508 509
        self.display_plugin(stats_mem, display_optional=(
            screen_x >= (space_number * self.space_between_column + l)))
510
        self.new_column()
511 512 513 514
        self.display_plugin(stats_memswap)
        # Space between column
        self.space_between_column = 3

515 516 517 518 519
        # Backup line position
        self.saved_line = self.next_line

        # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time)
        self.init_column()
520
        if (not (self.args.disable_network and self.args.disable_diskio
N
Nicolargo 已提交
521 522
                 and self.args.disable_fs and self.args.disable_raid
                 and self.args.disable_sensors)) \
523
                and not self.args.disable_left_sidebar:
524 525 526 527 528 529 530
            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 已提交
531 532
            self.display_plugin(stats_raid)
            self.new_line()
533 534 535
            self.display_plugin(stats_sensors)
            self.new_line()
            self.display_plugin(stats_now)
536 537

        # If space available...
538
        if screen_x > 52:
539 540 541
            # Restore line position
            self.next_line = self.saved_line

542
            # Display right sidebar
N
Nicolargo 已提交
543
            # ((DOCKER)+PROCESS_COUNT+(MONITORED)+PROCESS_LIST+ALERT)
544 545
            self.new_column()
            self.new_line()
N
Nicolargo 已提交
546 547
            self.display_plugin(stats_docker)
            self.new_line()
548
            self.display_plugin(stats_processcount)
549
            if glances_processes.get_process_filter() is None and cs_status == 'None':
N
Nicolargo 已提交
550 551 552
                # Do not display stats monitor list if a filter exist
                self.new_line()
                self.display_plugin(stats_monitor)
553
            self.new_line()
A
Alessio Sergi 已提交
554
            self.display_plugin(stats_processlist,
555
                                display_optional=(screen_x > 102),
556
                                display_additional=(not is_mac),
A
Alessio Sergi 已提交
557
                                max_y=(screen_y - self.get_stats_display_height(stats_alert) - 2))
558
            self.new_line()
559 560
            self.display_plugin(stats_alert)

561 562 563
        # History option
        # Generate history graph
        if self.history_tag and self.args.enable_history:
564 565
            self.display_popup(
                _("Generate graphs history in %s\nPlease wait...") % self.glances_history.get_output_folder())
N
Nicolargo 已提交
566 567
            self.display_popup(
                _("Generate graphs history in %s\nDone: %s graphs generated") % (self.glances_history.get_output_folder(), self.glances_history.generate_graph(stats)))
568 569
        elif self.reset_history_tag and self.args.enable_history:
            self.display_popup(_("Reset history"))
570
            self.glances_history.reset(stats)
571
        elif (self.history_tag or self.reset_history_tag) and not self.args.enable_history:
572 573
            try:
                self.glances_history.graph_enabled()
A
Alessio Sergi 已提交
574
            except Exception:
575 576
                self.display_popup(
                    _("History disabled\nEnable it using --enable-history"))
577
            else:
578 579
                self.display_popup(
                    _("History disabled\nPlease install MatPlotLib"))
580 581 582
        self.history_tag = False
        self.reset_history_tag = False

N
Nicolargo 已提交
583
        # Display edit filter popup
584 585
        # Only in standalone mode (cs_status == 'None')
        if self.edit_filter and cs_status == 'None':
586
            new_filter = self.display_popup(_("Process filter pattern: "),
N
Nicolargo 已提交
587 588 589
                                            is_input=True,
                                            input_value=glances_processes.get_process_filter())
            glances_processes.set_process_filter(new_filter)
590
        elif self.edit_filter and cs_status != 'None':
591 592
            self.display_popup(
                _("Process filter only available in standalone mode"))
N
Nicolargo 已提交
593 594
        self.edit_filter = False

595 596
        return True

597 598
    def display_popup(self, message,
                      size_x=None, size_y=None,
N
Nicolargo 已提交
599 600
                      duration=3,
                      is_input=False,
N
Nicolargo 已提交
601
                      input_size=30,
N
Nicolargo 已提交
602
                      input_value=None):
603
        """
N
Nicolargo 已提交
604 605 606 607 608 609 610 611 612
        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
613
         Return the input string or None if the field is empty
614 615 616
        """

        # Center the popup
N
Nicolargo 已提交
617
        sentence_list = message.split('\n')
618
        if size_x is None:
N
Nicolargo 已提交
619
            size_x = len(max(sentence_list, key=len)) + 4
N
Nicolargo 已提交
620 621 622
            # Add space for the input field
            if is_input:
                size_x += input_size
623
        if size_y is None:
N
Nicolargo 已提交
624
            size_y = len(sentence_list) + 4
625 626 627 628
        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
629
            return False
630 631
        pos_x = int((screen_x - size_x) / 2)
        pos_y = int((screen_y - size_y) / 2)
632 633 634

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

636 637 638 639 640 641 642 643 644
        # 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 已提交
645
        if is_input and not is_windows:
N
Nicolargo 已提交
646 647
            # Create a subwindow for the text field
            subpop = popup.derwin(1, input_size, 2, 2 + len(m))
648
            subpop.attron(self.colors_list['FILTER'])
N
Nicolargo 已提交
649 650 651 652 653 654 655 656
            # 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 已提交
657
            textbox = GlancesTextbox(subpop, insert_mode=False)
N
Nicolargo 已提交
658 659 660
            textbox.edit()
            self.set_cursor(0)
            if textbox.gather() != '':
N
Nicolargo 已提交
661 662
                logger.debug(
                    "User enters the following process filter patern: %s" % textbox.gather())
N
Nicolargo 已提交
663 664
                return textbox.gather()[:-1]
            else:
N
Nicolargo 已提交
665
                logger.debug("User clears the process filter patern")
N
Nicolargo 已提交
666 667 668 669 670 671
                return None
        else:
            # Display the popup
            popup.refresh()
            curses.napms(duration * 1000)
            return True
672

673
    def display_plugin(self, plugin_stats,
674
                       display_optional=True,
675
                       display_additional=True,
676
                       max_y=65535):
A
PEP 257  
Alessio Sergi 已提交
677 678
        """Display the plugin_stats on the screen.

679 680
        If display_optional=True display the optional stats
        If display_additional=True display additionnal stats
681 682
        max_y do not display line > max_y
        """
683 684 685
        # Exit if:
        # - the plugin_stats message is empty
        # - the display tag = False
D
desbma 已提交
686
        if not plugin_stats['msgdict'] or not plugin_stats['display']:
687
            # Exit
688 689 690 691 692 693 694
            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
695
        if plugin_stats['align'] == 'right':
696
            # Right align (last column)
697
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
698
        else:
699
            display_x = self.column
700
        if plugin_stats['align'] == 'bottom':
701
            # Bottom (last line)
702
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
703
        else:
704
            display_y = self.line
705

706 707
        # Display
        x = display_x
708
        x_max = x
709 710 711
        y = display_y
        for m in plugin_stats['msgdict']:
            # New line
712
            if m['msg'].startswith('\n'):
713 714 715 716 717 718
                # Go to the next line
                y = y + 1
                # Return to the first column
                x = display_x
                continue
            # Do not display outside the screen
719
            if x < 0:
720
                continue
721
            if not m['splittable'] and (x + len(m['msg']) > screen_x):
722
                continue
723
            if y < 0 or (y + 1 > screen_y) or (y > max_y):
724 725
                break
            # If display_optional = False do not display optional stats
726
            if not display_optional and m['optional']:
727
                continue
728 729 730
            # If display_additional = False do not display additional stats
            if not display_additional and m['additional']:
                continue
731 732 733
            # Is it possible to display the stat with the current screen size
            # !!! Crach if not try/except... Why ???
            try:
A
Alessio Sergi 已提交
734 735
                self.term_window.addnstr(y, x,
                                         m['msg'],
736 737
                                         # Do not disply outside the screen
                                         screen_x - x,
738
                                         self.colors_list[m['decoration']])
A
Alessio Sergi 已提交
739
            except Exception:
740 741 742
                pass
            else:
                # New column
D
desbma 已提交
743 744 745 746 747
                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 已提交
748 749
                    # Python 3: strings are strings and bytes are bytes, all is
                    # good
D
desbma 已提交
750 751
                    offset = len(m['msg'])
                x = x + offset
752 753
                if x > x_max:
                    x_max = x
754 755

        # Compute the next Glances column/line position
756
        self.next_column = max(self.next_column, x_max + self.space_between_column)
757
        self.next_line = max(self.next_line, y + self.space_between_line)
758 759

    def erase(self):
A
PEP 257  
Alessio Sergi 已提交
760
        """Erase the content of the screen."""
761 762
        self.term_window.erase()

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

766 767 768 769 770 771 772
        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()
773
        self.display(stats, cs_status=cs_status)
774

N
Nicolargo 已提交
775
    def update(self, stats, cs_status="None", return_to_browser=False):
A
PEP 257  
Alessio Sergi 已提交
776 777 778 779
        """Update the screen.

        Wait for __refresh_time sec / catch key every 100 ms.

N
Nicolargo 已提交
780
        INPUT
781 782 783 784 785
        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 已提交
786 787 788 789 790 791 792
        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...
793 794
        """
        # Flush display
795
        self.flush(stats, cs_status=cs_status)
796 797

        # Wait
N
Nicolargo 已提交
798
        exitkey = False
799
        countdown = Timer(self.__refresh_time)
N
Nicolargo 已提交
800
        while not countdown.finished() and not exitkey:
801
            # Getkey
N
Nicolargo 已提交
802 803 804 805
            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:
806
                # Redraw display
807
                self.flush(stats, cs_status=cs_status)
808 809 810
            # Wait 100ms...
            curses.napms(100)

N
Nicolargo 已提交
811 812
        return exitkey

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

A
PEP 257  
Alessio Sergi 已提交
816 817
        The height is defined by the maximum line.
        """
818
        try:
819
            if without_option:
820
                # Size without options
A
Alessio Sergi 已提交
821
                c = len(max(''.join([(i['msg'] if not i['optional'] else "")
822
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
823 824
            else:
                # Size with all options
A
Alessio Sergi 已提交
825
                c = len(max(''.join([i['msg']
826
                                     for i in curse_msg['msgdict']]).split('\n'), key=len))
A
Alessio Sergi 已提交
827
        except Exception:
828 829 830 831
            return 0
        else:
            return c

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

A
PEP 257  
Alessio Sergi 已提交
835 836
        The height is defined by the number of '\n' (new line).
        """
837
        try:
A
Alessio Sergi 已提交
838
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
A
Alessio Sergi 已提交
839
        except Exception:
840 841 842
            return 0
        else:
            return c + 1
N
Nicolargo 已提交
843

844

845 846 847 848 849 850 851
class GlancesCursesStandalone(_GlancesCurses):

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

    pass


852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
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 已提交
867 868
        _colors_list = {
            'UNKNOWN': self.no_color,
N
Nicolargo 已提交
869
            'SNMP': self.default_color2,
N
Nicolargo 已提交
870 871 872 873
            'ONLINE': self.default_color2,
            'OFFLINE': self.ifCRITICAL_color2,
            'PROTECTED': self.ifWARNING_color2,
        }
874
        self.colors_list.update(_colors_list)
N
Nicolargo 已提交
875

876 877 878 879
        # First time scan tag
        # Used to display a specific message when the browser is started
        self.first_scan = True

880 881 882 883 884 885
        # Init refresh time
        self.__refresh_time = args.time

        # Init the cursor position for the client browser
        self.cursor_init()

N
Nicolargo 已提交
886 887 888 889 890 891 892 893 894 895 896 897
        # Active Glances server number
        self.set_active()

    def set_active(self, index=None):
        """Set the active server or None if no server selected"""
        self.active_server = index
        return self.active_server

    def get_active(self):
        """Return the active server (the one display in front) or None if it is the browser list"""
        return self.active_server

898 899 900 901 902
    def cursor_init(self):
        """Init the cursor position to the top of the list"""
        return self.cursor_set(0)

    def cursor_set(self, pos):
N
Nicolargo 已提交
903
        """Set the cursor position and return it"""
904 905 906 907 908 909 910
        self.cursor_position = pos
        return self.cursor_position

    def cursor_get(self):
        """Return the cursor position"""
        return self.cursor_position

N
Nicolargo 已提交
911
    def cursor_up(self, servers_list):
912 913 914
        """Set the cursor to position N-1 in the list"""
        if self.cursor_position > 0:
            self.cursor_position -= 1
N
Nicolargo 已提交
915 916
        else:
            self.cursor_position = len(servers_list) - 1
917 918 919 920 921 922
        return self.cursor_position

    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 已提交
923 924
        else:
            self.cursor_position = 0
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939
        return self.cursor_position

    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
            logger.debug("Server number %s selected" % (self.cursor_get() + 1))
N
Nicolargo 已提交
940
            self.set_active(self.cursor_get())
941 942 943
        elif self.pressedkey == 259:
            # 'UP' > Up in the server list
            logger
N
Nicolargo 已提交
944
            self.cursor_up(servers_list)
945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962
        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 已提交
963
        exitkey = False
964
        countdown = Timer(self.__refresh_time)
N
Nicolargo 已提交
965
        while not countdown.finished() and not exitkey:
966
            # Getkey
N
Nicolargo 已提交
967 968
            pressedkey = self.__catch_key(servers_list)
            # Is it an exit or select server key ?
N
Nicolargo 已提交
969 970
            exitkey = (
                pressedkey == ord('\x1b') or pressedkey == ord('q') or pressedkey == 10)
N
Nicolargo 已提交
971
            if not exitkey and pressedkey > -1:
972 973 974 975 976
                # Redraw display
                self.flush(servers_list)
            # Wait 100ms...
            curses.napms(100)

N
Nicolargo 已提交
977 978
        return self.get_active()

979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004
    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 已提交
1005
            if self.first_scan and not self.args.disable_autodiscover:
1006
                msg = _("Glances is scanning your network (please wait)...")
N
Nicolargo 已提交
1007
                self.first_scan = False
1008
            else:
N
Nicolargo 已提交
1009
                msg = _("No Glances servers available")
1010
        elif len(servers_list) == 1:
N
Nicolargo 已提交
1011
            msg = _("One Glances server available")
1012
        else:
N
Nicolargo 已提交
1013
            msg = _("%d Glances servers available" %
1014
                    len(servers_list))
1015
        if self.args.disable_autodiscover:
N
Nicolargo 已提交
1016
            msg += ' ' + _("(auto discover is disabled)")
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031
        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],
1032
            ['alias', None, None],
1033 1034 1035
            ['load_min5', _('LOAD'), 6],
            ['cpu_percent', _('CPU%'), 5],
            ['mem_percent', _('MEM%'), 5],
N
Nicolargo 已提交
1036
            ['status', _('STATUS'), 8],
1037
            ['ip', _('IP'), 15],
1038
            # ['port', _('PORT'), 5],
1039 1040 1041 1042 1043 1044 1045 1046
            ['hr_name', _('OS'), 16],
        ]
        y = 2

        # Display table header
        cpt = 0
        xc = x + 2
        for c in column_def:
1047
            if xc < screen_x and y < screen_y and c[1] is not None:
N
Nicolargo 已提交
1048 1049 1050 1051 1052
                self.term_window.addnstr(y, xc,
                                         c[1],
                                         screen_x - x,
                                         self.colors_list['BOLD'])
                xc += c[2] + self.space_between_column
1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065
            cpt += 1
        y += 1

        # If a servers has been deleted from the list...
        # ... and if the cursor is in the latest position
        if self.cursor_get() > len(servers_list) - 1:
            # Set the cursor position to the latest item
            self.cursor_set(len(servers_list) - 1)

        # Display table
        line = 0
        for v in servers_list:
            # Get server stats
1066 1067 1068
            server_stat = {}
            for c in column_def:
                try:
1069
                    server_stat[c[0]] = v[c[0]]
1070 1071
                except KeyError as e:
                    logger.debug(
A
Alessio Sergi 已提交
1072
                        "Cannot grab stats {0} from server (KeyError: {1})".format(c[0], e))
1073
                    server_stat[c[0]] = '?'
1074
                # Display alias instead of name
N
Nicolargo 已提交
1075 1076 1077 1078 1079
                try:
                    if c[0] == 'alias' and v[c[0]] is not None:
                        server_stat['name'] = v[c[0]]
                except KeyError as e:
                    pass
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089

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

            # Is the line selected ?
            if line == self.cursor_get():
                # Display cursor
                self.term_window.addnstr(y, xc,
                                         ">",
N
Nicolargo 已提交
1090
                                         screen_x - xc,
1091 1092
                                         self.colors_list['BOLD'])

1093 1094 1095 1096
            # Display alias instead of name
            server_stat

            # Display the line
1097 1098
            xc += 2
            for c in column_def:
1099
                if xc < screen_x and y < screen_y and c[1] is not None:
N
Nicolargo 已提交
1100 1101 1102
                    # Display server stats
                    self.term_window.addnstr(y, xc,
                                             "%s" % server_stat[c[0]],
N
Nicolargo 已提交
1103
                                             c[2],
N
Nicolargo 已提交
1104
                                             self.colors_list[v['status']])
N
Nicolargo 已提交
1105
                    xc += c[2] + self.space_between_column
1106 1107 1108 1109 1110 1111 1112 1113
                cpt += 1
            # Next line, next server...
            y += 1
            line += 1

        return True


N
Nicolargo 已提交
1114
if not is_windows:
A
Alessio Sergi 已提交
1115
    class GlancesTextbox(Textbox):
1116

N
Nicolargo 已提交
1117 1118
        def __init__(*args, **kwargs):
            Textbox.__init__(*args, **kwargs)
1119

N
Nicolargo 已提交
1120
        def do_command(self, ch):
1121
            if ch == 10:  # Enter
N
Nicolargo 已提交
1122
                return 0
1123
            if ch == 127:  # Enter
N
Nicolargo 已提交
1124
                return 8
1125
            return Textbox.do_command(self, ch)