glances.outputs.glances_curses   F
last analyzed

Complexity

Total Complexity 225

Size/Duplication

Total Lines 1256
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 731
dl 0
loc 1256
rs 1.869
c 0
b 0
f 0
wmc 225

71 Methods

Rating   Name   Duplication   Size   Complexity  
A _GlancesCurses.new_column() 0 3 1
A _GlancesCurses.init_column() 0 4 1
A _GlancesCurses.init_line() 0 4 1
A _GlancesCurses.init_line_column() 0 4 1
A _GlancesCurses._handle_clean_logs() 0 2 1
A _GlancesCurses._handle_sort_key() 0 2 1
A _GlancesCurses._handle_quicklook() 0 6 2
A _GlancesCurses._handle_disable_process() 0 6 2
A _GlancesCurses._handle_erase_filter() 0 2 1
A _GlancesCurses._handle_kill_process() 0 2 1
A _GlancesCurses._handle_clean_critical_logs() 0 2 1
A _GlancesCurses._handle_process_extended() 0 7 2
A _GlancesCurses._handle_increase_nice() 0 2 1
A _GlancesCurses.__catch_key() 0 15 2
A _GlancesCurses._handle_decrease_nice() 0 2 1
A _GlancesCurses._handle_switch() 0 17 5
A _GlancesCurses._handle_fs_stats() 0 3 1
A _GlancesCurses._handle_top_menu() 0 6 2
A _GlancesCurses._handle_enter() 0 2 1
A _GlancesCurses._handle_sort_left() 0 3 1
A _GlancesCurses._handle_cursor_up() 0 3 2
B _GlancesCurses.catch_actions_from_hotkey() 0 8 7
A _GlancesCurses.__init__() 0 67 4
A _GlancesCurses._right_sidebar() 0 8 2
A _GlancesCurses._init_curses_cursor() 0 8 3
A _GlancesCurses._handle_diskio_iops() 0 5 2
A _GlancesCurses._handle_diskio_latency() 0 5 2
A _GlancesCurses.catch_other_actions_maybe_return_to_browser() 0 11 2
A _GlancesCurses._handle_cursor_down() 0 3 2
A _GlancesCurses._init_history() 0 4 1
A _GlancesCurses.get_key() 0 3 1
A _GlancesCurses._handle_sort_right() 0 3 1
A _GlancesCurses.load_config() 0 12 3
A _GlancesCurses.set_cursor() 0 12 3
A _GlancesCurses.enable_fullquicklook() 0 5 2
A _GlancesCurses.nice_increase() 0 2 1
A _GlancesCurses.disable_top() 0 4 2
C _GlancesCurses.update() 0 56 11
A _GlancesCurses.end() 0 14 5
A _GlancesCurses.__get_stat_display() 0 34 4
A _GlancesCurses.display_stats_with_current_size() 0 9 1
A _GlancesCurses._handle_quit() 0 9 2
A _GlancesCurses.flush() 0 13 1
B _GlancesCurses.get_stats_display_width() 0 31 5
F _GlancesCurses.display() 0 106 16
A _GlancesCurses.nice_decrease() 0 2 1
A _GlancesCurses.erase() 0 7 1
A GlancesTextboxYesNo.__init__() 0 2 1
A _GlancesCurses.setup_upper_left_pos() 0 16 3
A _GlancesCurses.new_line() 0 3 1
A _GlancesCurses.refresh() 0 3 1
A _GlancesCurses.clear() 0 7 1
A _GlancesCurses.get_next_x_and_x_max() 0 15 3
F _GlancesCurses.display_popup() 0 119 16
A _GlancesCurses.get_stats_display_height() 0 12 3
B _GlancesCurses.__display_left() 0 17 7
A _GlancesCurses.wait() 0 3 1
F _GlancesCurses.__display_top() 0 88 16
A _GlancesCurses.disable_fullquicklook() 0 4 2
A _GlancesCurses._handle_refresh() 0 2 1
A GlancesTextbox.do_command() 0 6 3
B _GlancesCurses.__display_right() 0 36 7
A _GlancesCurses.enable_top() 0 4 2
A _GlancesCurses.loop_position() 0 6 3
B _GlancesCurses.kill() 0 29 6
A _GlancesCurses.separator_line() 0 16 5
A GlancesTextboxYesNo.do_command() 0 2 1
B _GlancesCurses.__display_header() 0 26 5
C _GlancesCurses.display_stats() 0 34 11
A GlancesTextbox.__init__() 0 2 1
C _GlancesCurses.display_plugin() 0 41 9

How to fix   Complexity   

Complexity

Complex classes like glances.outputs.glances_curses often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""Curses interface class."""
10
11
import functools
12
import getpass
13
import sys
14
15
from glances.events_list import glances_events
16
from glances.globals import MACOS, WINDOWS, disable, enable, nativestr, u
17
from glances.logger import logger
18
from glances.outputs.glances_colors import GlancesColors
19
from glances.outputs.glances_unicode import unicode_message
20
from glances.processes import glances_processes, sort_processes_stats_list
21
from glances.timer import Timer
22
23
# Import curses library for "normal" operating system
24
try:
25
    import curses
26
    import curses.panel
27
    from curses.textpad import Textbox
28
except ImportError:
29
    logger.critical("Curses module not found. Glances cannot start in standalone mode.")
30
    if WINDOWS:
31
        logger.critical("For Windows you can try installing windows-curses with pip install.")
32
    sys.exit(1)
33
34
35
class _GlancesCurses:
36
    """This class manages the curses display (and key pressed).
37
38
    Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser.
39
    """
40
41
    _hotkeys = {
42
        '\n': {'handler': '_handle_enter'},
43
        '0': {'switch': 'disable_irix'},
44
        '1': {'switch': 'percpu'},
45
        '2': {'switch': 'disable_left_sidebar'},
46
        '3': {'switch': 'disable_quicklook'},
47
        '4': {'handler': '_handle_quicklook'},
48
        '5': {'handler': '_handle_top_menu'},
49
        '6': {'switch': 'meangpu'},
50
        '/': {'switch': 'process_short_name'},
51
        'a': {'sort_key': 'auto'},
52
        'A': {'switch': 'disable_amps'},
53
        'b': {'switch': 'byte'},
54
        'B': {'handler': '_handle_diskio_iops'},
55
        'c': {'sort_key': 'cpu_percent'},
56
        'C': {'switch': 'disable_cloud'},
57
        'd': {'switch': 'disable_diskio'},
58
        'D': {'switch': 'disable_containers'},
59
        # 'e' > Enable/Disable process extended
60
        'E': {'handler': '_handle_erase_filter'},
61
        'f': {'handler': '_handle_fs_stats'},
62
        'F': {'switch': 'fs_free_space'},
63
        'g': {'switch': 'generate_graph'},
64
        'G': {'switch': 'disable_gpu'},
65
        'h': {'switch': 'help_tag'},
66
        'i': {'sort_key': 'io_counters'},
67
        'I': {'switch': 'disable_ip'},
68
        'j': {'switch': 'programs'},
69
        # 'k' > Kill selected process
70
        'K': {'switch': 'disable_connections'},
71
        'l': {'switch': 'disable_alert'},
72
        'L': {'handler': '_handle_diskio_latency'},
73
        'm': {'sort_key': 'memory_percent'},
74
        'M': {'switch': 'reset_minmax_tag'},
75
        'n': {'switch': 'disable_network'},
76
        'N': {'switch': 'disable_now'},
77
        'p': {'sort_key': 'name'},
78
        'P': {'switch': 'disable_ports'},
79
        # 'q' or ESCAPE > Quit
80
        'Q': {'switch': 'enable_irq'},
81
        'r': {'switch': 'disable_smart'},
82
        'R': {'switch': 'disable_raid'},
83
        's': {'switch': 'disable_sensors'},
84
        'S': {'switch': 'sparkline'},
85
        't': {'sort_key': 'cpu_times'},
86
        'T': {'switch': 'network_sum'},
87
        'u': {'sort_key': 'username'},
88
        'U': {'switch': 'network_cumul'},
89
        'V': {'switch': 'disable_vms'},
90
        'w': {'handler': '_handle_clean_logs'},
91
        'W': {'switch': 'disable_wifi'},
92
        'x': {'handler': '_handle_clean_critical_logs'},
93
        'z': {'handler': '_handle_disable_process'},
94
        '+': {'handler': '_handle_increase_nice'},
95
        '-': {'handler': '_handle_decrease_nice'},
96
        # "<" (left arrow) navigation through process sort
97
        # ">" (right arrow) navigation through process sort
98
        # 'UP' > Up in the server list
99
        # 'DOWN' > Down in the server list
100
    }
101
102
    _sort_loop = sort_processes_stats_list
103
104
    # Define top menu
105
    _top = ['quicklook', 'cpu', 'percpu', 'gpu', 'mem', 'memswap', 'load']
106
    _quicklook_max_width = 58
107
108
    # Define left sidebar
109
    # This variable is used in the make webui task in order to generate the
110
    # glances/outputs/static/js/uiconfig.json file for the web interface
111
    # This lidt can also be overwritten by the configuration file ([outputs] left_menu option)
112
    _left_sidebar = [
113
        'network',
114
        'ports',
115
        'wifi',
116
        'connections',
117
        'diskio',
118
        'fs',
119
        'irq',
120
        'folders',
121
        'raid',
122
        'smart',
123
        'sensors',
124
        'now',
125
    ]
126
    _left_sidebar_min_width = 23
127
    _left_sidebar_max_width = 34
128
129
    # Define right sidebar in a method because it depends of self.args.programs
130
    # See def _right_sidebar method
131
132
    def __init__(self, config=None, args=None):
133
        # Init
134
        self.config = config
135
        self.args = args
136
137
        # Init windows positions
138
        self.term_w = 80
139
        self.term_h = 24
140
141
        # Space between stats
142
        self.space_between_column = 3
143
        self.space_between_line = 2
144
145
        # Init the curses screen
146
        try:
147
            self.screen = curses.initscr()
148
            if not self.screen:
149
                logger.critical("Cannot init the curses library.\n")
150
                sys.exit(1)
151
            else:
152
                logger.debug(f"Curses library initialized with term: {curses.longname()}")
153
        except Exception as e:
154
            if args.export:
155
                logger.info("Cannot init the curses library, quiet mode on and export.")
156
                args.quiet = True
157
                return
158
159
            logger.critical(f"Cannot init the curses library ({e})")
160
            sys.exit(1)
161
162
        # Load configuration file
163
        self.load_config(config)
164
165
        # Init Curses cursor
166
        self._init_curses_cursor()
167
168
        # Init the colors
169
        self.colors_list = GlancesColors(args).get()
170
171
        # Init main window
172
        self.term_window = self.screen.subwin(0, 0)
173
174
        # Init edit filter tag
175
        self.edit_filter = False
176
177
        # Init nice increase/decrease tag
178
        self.increase_nice_process = False
179
        self.decrease_nice_process = False
180
181
        # Init kill process tag
182
        self.kill_process = False
183
184
        # Init the process min/max reset
185
        self.args.reset_minmax_tag = False
186
187
        # Init Glances cursor
188
        self.args.cursor_position = 0
189
        # For the moment cursor only available in standalone mode
190
        self.args.disable_cursor = not self.args.is_standalone
191
192
        # Catch key pressed with non blocking mode
193
        self.term_window.keypad(1)
194
        self.term_window.nodelay(1)
195
        self.pressedkey = -1
196
197
        # History tag
198
        self._init_history()
199
200
    def load_config(self, config):
201
        """Load the outputs section of the configuration file."""
202
        if config is not None and config.has_section('outputs'):
203
            logger.debug('Read the outputs section in the configuration file')
204
            # Separator
205
            self.args.enable_separator = config.get_bool_value(
206
                'outputs', 'separator', default=self.args.enable_separator
207
            )
208
            # Set the left sidebar list
209
            self._left_sidebar = config.get_list_value('outputs', 'left_menu', default=self._left_sidebar)
210
            # Background color
211
            self.args.disable_bg = config.get_bool_value('outputs', 'disable_bg', default=self.args.disable_bg)
212
213
    def _right_sidebar(self):
214
        return [
215
            'vms',
216
            'containers',
217
            'processcount',
218
            'amps',
219
            'programlist' if self.args.programs else 'processlist',
220
            'alert',
221
        ]
222
223
    def _init_history(self):
224
        """Init the history option."""
225
226
        self.reset_history_tag = False
227
228
    def _init_curses_cursor(self):
229
        """Init cursors."""
230
231
        if hasattr(curses, 'noecho'):
232
            curses.noecho()
233
        if hasattr(curses, 'cbreak'):
234
            curses.cbreak()
235
        self.set_cursor(0)
236
237
    def set_cursor(self, value):
238
        """Configure the curse cursor appearance.
239
240
        0: invisible
241
        1: visible
242
        2: very visible
243
        """
244
        if hasattr(curses, 'curs_set'):
245
            try:
246
                curses.curs_set(value)
247
            except Exception:
248
                pass
249
250
    def get_key(self, window):
251
        # TODO: Check issue #163
252
        return window.getch()
253
254
    def catch_actions_from_hotkey(self, hotkey):
255
        if self.pressedkey == ord(hotkey) and 'switch' in self._hotkeys[hotkey]:
256
            self._handle_switch(hotkey)
257
        elif self.pressedkey == ord(hotkey) and 'sort_key' in self._hotkeys[hotkey]:
258
            self._handle_sort_key(hotkey)
259
        if self.pressedkey == ord(hotkey) and 'handler' in self._hotkeys[hotkey]:
260
            action = getattr(self, self._hotkeys[hotkey]['handler'])
261
            action()
262
263
    def catch_other_actions_maybe_return_to_browser(self, return_to_browser):
264
        {
265
            self.pressedkey in {ord('e')} and not self.args.programs: self._handle_process_extended,
266
            self.pressedkey in {ord('k')} and not self.args.disable_cursor: self._handle_kill_process,
267
            self.pressedkey in {curses.KEY_LEFT}: self._handle_sort_left,
268
            self.pressedkey in {curses.KEY_RIGHT}: self._handle_sort_right,
269
            self.pressedkey in {curses.KEY_UP, 65} and not self.args.disable_cursor: self._handle_cursor_up,
270
            self.pressedkey in {curses.KEY_DOWN, 66} and not self.args.disable_cursor: self._handle_cursor_down,
271
            self.pressedkey in {curses.KEY_F5, 18}: self._handle_refresh,
272
            self.pressedkey in {ord('\x1b'), ord('q')}: functools.partial(self._handle_quit, return_to_browser),
273
        }.get(True, lambda: None)()
274
275
    def __catch_key(self, return_to_browser=False):
276
        # Catch the pressed key
277
        self.pressedkey = self.get_key(self.term_window)
278
        if self.pressedkey == -1:
279
            return self.pressedkey
280
281
        # Actions (available in the global hotkey dict)...
282
        logger.debug(f"Keypressed (code: {self.pressedkey})")
283
        [self.catch_actions_from_hotkey(hotkey) for hotkey in self._hotkeys]
284
285
        # Other actions with key > 255 (ord will not work) and/or additional test...
286
        self.catch_other_actions_maybe_return_to_browser(return_to_browser)
287
288
        # Return the key code
289
        return self.pressedkey
290
291
    def _handle_switch(self, hotkey):
292
        option = '_'.join(self._hotkeys[hotkey]['switch'].split('_')[1:])
293
        if self._hotkeys[hotkey]['switch'].startswith('disable_'):
294
            if getattr(self.args, self._hotkeys[hotkey]['switch']):
295
                enable(self.args, option)
296
            else:
297
                disable(self.args, option)
298
        elif self._hotkeys[hotkey]['switch'].startswith('enable_'):
299
            if getattr(self.args, self._hotkeys[hotkey]['switch']):
300
                disable(self.args, option)
301
            else:
302
                enable(self.args, option)
303
        else:
304
            setattr(
305
                self.args,
306
                self._hotkeys[hotkey]['switch'],
307
                not getattr(self.args, self._hotkeys[hotkey]['switch']),
308
            )
309
310
    def _handle_sort_key(self, hotkey):
311
        glances_processes.set_sort_key(self._hotkeys[hotkey]['sort_key'], self._hotkeys[hotkey]['sort_key'] == 'auto')
312
313
    def _handle_enter(self):
314
        self.edit_filter = not self.edit_filter
315
316
    def _handle_quicklook(self):
317
        self.args.full_quicklook = not self.args.full_quicklook
318
        if self.args.full_quicklook:
319
            self.enable_fullquicklook()
320
        else:
321
            self.disable_fullquicklook()
322
323
    def _handle_top_menu(self):
324
        self.args.disable_top = not self.args.disable_top
325
        if self.args.disable_top:
326
            self.disable_top()
327
        else:
328
            self.enable_top()
329
330
    def _handle_process_extended(self):
331
        self.args.enable_process_extended = not self.args.enable_process_extended
332
        if not self.args.enable_process_extended:
333
            glances_processes.disable_extended()
334
        else:
335
            glances_processes.enable_extended()
336
        self.args.disable_cursor = self.args.enable_process_extended and self.args.is_standalone
337
338
    def _handle_erase_filter(self):
339
        glances_processes.process_filter = None
340
341
    def _handle_fs_stats(self):
342
        self.args.disable_fs = not self.args.disable_fs
343
        self.args.disable_folders = not self.args.disable_folders
344
345
    def _handle_increase_nice(self):
346
        self.increase_nice_process = not self.increase_nice_process
347
348
    def _handle_decrease_nice(self):
349
        self.decrease_nice_process = not self.decrease_nice_process
350
351
    def _handle_kill_process(self):
352
        self.kill_process = not self.kill_process
353
354
    def _handle_clean_logs(self):
355
        glances_events.clean()
356
357
    def _handle_clean_critical_logs(self):
358
        glances_events.clean(critical=True)
359
360
    def _handle_disable_process(self):
361
        self.args.disable_process = not self.args.disable_process
362
        if self.args.disable_process:
363
            glances_processes.disable()
364
        else:
365
            glances_processes.enable()
366
367
    def _handle_diskio_iops(self):
368
        """Switch between bytes/s and IOPS for Disk IO."""
369
        self.args.diskio_iops = not self.args.diskio_iops
370
        if self.args.diskio_iops:
371
            self.args.diskio_latency = False
372
373
    def _handle_diskio_latency(self):
374
        """Switch between bytes/s and latency for Disk IO."""
375
        self.args.diskio_latency = not self.args.diskio_latency
376
        if self.args.diskio_latency:
377
            self.args.diskio_iops = False
378
379
    def _handle_sort_left(self):
380
        next_sort = (self.loop_position() - 1) % len(self._sort_loop)
381
        glances_processes.set_sort_key(self._sort_loop[next_sort], False)
382
383
    def _handle_sort_right(self):
384
        next_sort = (self.loop_position() + 1) % len(self._sort_loop)
385
        glances_processes.set_sort_key(self._sort_loop[next_sort], False)
386
387
    def _handle_cursor_up(self):
388
        if self.args.cursor_position > 0:
389
            self.args.cursor_position -= 1
390
391
    def _handle_cursor_down(self):
392
        if self.args.cursor_position < glances_processes.processes_count:
393
            self.args.cursor_position += 1
394
395
    def _handle_quit(self, return_to_browser):
396
        if return_to_browser:
397
            logger.info("Stop Glances client and return to the browser")
398
        else:
399
            logger.info(f"Stop Glances (keypressed: {self.pressedkey})")
400
            # End the curses window
401
            self.end()
402
            # Exit the program
403
            sys.exit(0)
404
405
    def _handle_refresh(self):
406
        glances_processes.reset_internal_cache()
407
408
    def loop_position(self):
409
        """Return the current sort in the loop"""
410
        for i, v in enumerate(self._sort_loop):
411
            if v == glances_processes.sort_key:
412
                return i
413
        return 0
414
415
    def disable_top(self):
416
        """Disable the top panel"""
417
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']:
418
            setattr(self.args, 'disable_' + p, True)
419
420
    def enable_top(self):
421
        """Enable the top panel"""
422
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']:
423
            setattr(self.args, 'disable_' + p, False)
424
425
    def disable_fullquicklook(self):
426
        """Disable the full quicklook mode"""
427
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap']:
428
            setattr(self.args, 'disable_' + p, False)
429
430
    def enable_fullquicklook(self):
431
        """Disable the full quicklook mode"""
432
        self.args.disable_quicklook = False
433
        for p in ['cpu', 'gpu', 'mem', 'memswap']:
434
            setattr(self.args, 'disable_' + p, True)
435
436
    def end(self):
437
        """Shutdown the curses window."""
438
        if hasattr(curses, 'echo'):
439
            curses.echo()
440
        if hasattr(curses, 'nocbreak'):
441
            curses.nocbreak()
442
        try:
443
            curses.curs_set(1)
444
        except Exception:
445
            pass
446
        try:
447
            curses.endwin()
448
        except Exception:
449
            pass
450
451
    def init_line_column(self):
452
        """Init the line and column position for the curses interface."""
453
        self.init_line()
454
        self.init_column()
455
456
    def init_line(self):
457
        """Init the line position for the curses interface."""
458
        self.line = 0
459
        self.next_line = 0
460
461
    def init_column(self):
462
        """Init the column position for the curses interface."""
463
        self.column = 0
464
        self.next_column = 0
465
466
    def new_line(self, separator=False):
467
        """New line in the curses interface."""
468
        self.line = self.next_line
469
470
    def new_column(self):
471
        """New column in the curses interface."""
472
        self.column = self.next_column
473
474
    def separator_line(self, color='SEPARATOR'):
475
        """Add a separator line in the curses interface."""
476
        if not self.args.enable_separator:
477
            return
478
        self.new_line()
479
        self.line -= 1
480
        line_width = self.term_window.getmaxyx()[1] - self.column
481
        if self.line >= 0 and self.line < self.term_window.getmaxyx()[0]:
482
            position = [self.line, self.column]
483
            line_color = self.colors_list[color]
484
            line_type = curses.ACS_HLINE if not self.args.disable_unicode else unicode_message('MEDIUM_LINE', self.args)
485
            self.term_window.hline(
486
                *position,
487
                line_type,
488
                line_width,
489
                line_color,
490
            )
491
492
    def __get_stat_display(self, stats, layer):
493
        """Return a dict of dict with all the stats display.
494
        # TODO: Drop extra parameter
495
496
        :param stats: Global stats dict
497
        :param layer: ~ cs_status
498
            "None": standalone or server mode
499
            "Connected": Client is connected to a Glances server
500
            "SNMP": Client is connected to a SNMP server
501
            "Disconnected": Client is disconnected from the server
502
503
        :returns: dict of dict
504
            * key: plugin name
505
            * value: dict returned by the get_stats_display Plugin method
506
        """
507
        ret = {}
508
509
        for p in stats.getPluginsList(enable=False):
510
            # Ignore Quicklook because it is compute later in __display_top
511
            if p == 'quicklook':
512
                continue
513
514
            # Compute the plugin max size for the left sidebar
515
            plugin_max_width = None
516
            if p in self._left_sidebar:
517
                plugin_max_width = min(
518
                    self._left_sidebar_max_width,
519
                    max(self._left_sidebar_min_width, self.term_window.getmaxyx()[1] - 105),
520
                )
521
522
            # Get the view
523
            ret[p] = stats.get_plugin(p).get_stats_display(args=self.args, max_width=plugin_max_width)
524
525
        return ret
526
527
    def display(self, stats, cs_status=None):
528
        """Display stats on the screen.
529
530
        :param stats: Stats database to display
531
        :param cs_status:
532
            "None": standalone or server mode
533
            "Connected": Client is connected to a Glances server
534
            "SNMP": Client is connected to a SNMP server
535
            "Disconnected": Client is disconnected from the server
536
537
        :return: True if the stats have been displayed else False if the help have been displayed
538
        """
539
        # Init the internal line/column for Glances Curses
540
        self.init_line_column()
541
542
        # Update the stats messages
543
        ###########################
544
545
        # Get all the plugins view
546
        self.args.cs_status = cs_status
547
        __stat_display = self.__get_stat_display(stats, layer=cs_status)
548
549
        # Display the stats on the curses interface
550
        ###########################################
551
552
        # Help screen (on top of the other stats)
553
        if self.args.help_tag:
554
            # Display the stats...
555
            self.display_plugin(stats.get_plugin('help').get_stats_display(args=self.args))
556
            # ... and exit
557
            return False
558
559
        # =======================================
560
        # Display first line (system+ip+uptime)
561
        # Optionally: Cloud is on the second line
562
        # =======================================
563
        self.__display_header(__stat_display)
564
        self.separator_line()
565
566
        # ==============================================================
567
        # Display second line (<SUMMARY>+CPU|PERCPU+<GPU>+LOAD+MEM+SWAP)
568
        # ==============================================================
569
        self.__display_top(__stat_display, stats)
570
        self.init_column()
571
        self.separator_line()
572
573
        # ==================================================================
574
        # Display left sidebar (NETWORK+PORTS+DISKIO+FS+SENSORS+Current time)
575
        # ==================================================================
576
        self.__display_left(__stat_display)
577
578
        # ====================================
579
        # Display right stats (process and co)
580
        # ====================================
581
        self.__display_right(__stat_display)
582
583
        # =====================
584
        # Others popup messages
585
        # =====================
586
587
        # Display edit filter popup
588
        # Only in standalone mode (cs_status is None)
589
        if self.edit_filter and cs_status is None:
590
            new_filter = self.display_popup(
591
                'Process filter pattern: \n\n'
592
                + 'Examples:\n'
593
                + '- .*python.*\n'
594
                + '- /usr/lib.*\n'
595
                + '- name:.*nautilus.*\n'
596
                + '- cmdline:.*glances.*\n'
597
                + '- username:nicolargo\n'
598
                + '- username:^root        ',
599
                popup_type='input',
600
                input_value=glances_processes.process_filter_input,
601
            )
602
            glances_processes.process_filter = new_filter
603
        elif self.edit_filter and cs_status is not None:
604
            self.display_popup('Process filter only available in standalone mode')
605
        self.edit_filter = False
606
607
        # Manage increase/decrease nice level of the selected process
608
        # Only in standalone mode (cs_status is None)
609
        if self.increase_nice_process and cs_status is None:
610
            self.nice_increase(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
611
        self.increase_nice_process = False
612
        if self.decrease_nice_process and cs_status is None:
613
            self.nice_decrease(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
614
        self.decrease_nice_process = False
615
616
        # Display kill process confirmation popup
617
        # Only in standalone mode (cs_status is None)
618
        if self.kill_process and cs_status is None:
619
            self.kill(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
620
        elif self.kill_process and cs_status is not None:
621
            self.display_popup('Kill process only available for local processes')
622
        self.kill_process = False
623
624
        # Display graph generation popup
625
        if self.args.generate_graph:
626
            if 'graph' in stats.getExportsList():
627
                self.display_popup(f'Generate graph in {self.args.export_graph_path}')
628
            else:
629
                logger.warning('Graph export module is disable. Run Glances with --export graph to enable it.')
630
                self.args.generate_graph = False
631
632
        return True
633
634
    def nice_increase(self, process):
635
        glances_processes.nice_increase(process['pid'])
636
637
    def nice_decrease(self, process):
638
        glances_processes.nice_decrease(process['pid'])
639
640
    def kill(self, process):
641
        """Kill a process, or a list of process if the process has a childrens field.
642
643
        :param process
644
        :return: None
645
        """
646
        logger.debug(f"Selected process to kill: {process}")
647
648
        if 'childrens' in process:
649
            pid_to_kill = process['childrens']
650
        else:
651
            pid_to_kill = [process['pid']]
652
653
        confirm = self.display_popup(
654
            'Kill process: {} (pid: {}) ?\n\nConfirm ([y]es/[n]o): '.format(
655
                process['name'],
656
                ', '.join(map(str, pid_to_kill)),
657
            ),
658
            popup_type='yesno',
659
        )
660
661
        if confirm.lower().startswith('y'):
662
            for pid in pid_to_kill:
663
                try:
664
                    ret_kill = glances_processes.kill(pid)
665
                except Exception as e:
666
                    logger.error(f'Can not kill process {pid} ({e})')
667
                else:
668
                    logger.info(f'Kill signal has been sent to process {pid} (return code: {ret_kill})')
669
670
    def __display_header(self, stat_display):
671
        """Display the firsts lines (header) in the Curses interface.
672
673
        system + ip + uptime
674
        (cloud)
675
        """
676
        # First line
677
        self.new_line()
678
        self.space_between_column = 0
679
        l_uptime = 1
680
        for i in ['system', 'ip', 'uptime']:
681
            if i in stat_display:
682
                l_uptime += self.get_stats_display_width(stat_display[i])
683
        self.display_plugin(stat_display["system"], display_optional=(self.term_window.getmaxyx()[1] >= l_uptime))
684
        self.space_between_column = 3
685
        if 'ip' in stat_display:
686
            self.new_column()
687
            self.display_plugin(stat_display["ip"], display_optional=(self.term_window.getmaxyx()[1] >= 100))
688
        self.new_column()
689
        cloud_width = self.get_stats_display_width(stat_display.get("cloud", 0))
690
        self.display_plugin(stat_display["uptime"], add_space=-(cloud_width != 0))
691
        self.init_column()
692
        if cloud_width != 0:
693
            # Second line (optional)
694
            self.new_line()
695
            self.display_plugin(stat_display["cloud"])
696
697
    def __display_top(self, stat_display, stats):
698
        """Display the second line in the Curses interface.
699
700
        <QUICKLOOK> + CPU|PERCPU + <GPU> + MEM + SWAP + LOAD
701
        """
702
        self.init_column()
703
        self.new_line()
704
705
        # Init quicklook
706
        stat_display['quicklook'] = {'msgdict': []}
707
708
        # Dict for plugins width
709
        plugin_widths = {}
710
        for p in self._top:
711
            plugin_widths[p] = (
712
                self.get_stats_display_width(stat_display.get(p, 0)) if hasattr(self.args, 'disable_' + p) else 0
713
            )
714
715
        # Width of all plugins
716
        stats_width = sum(plugin_widths.values())
717
718
        # Number of plugin but quicklook
719
        stats_number = sum(
720
            [int(stat_display[p]['msgdict'] != []) for p in self._top if not getattr(self.args, 'disable_' + p)]
721
        )
722
723
        if not self.args.disable_quicklook:
724
            # Quick look is in the place !
725
            if self.args.full_quicklook:
726
                quicklook_width = self.term_window.getmaxyx()[1] - (
727
                    stats_width + 8 + stats_number * self.space_between_column
728
                )
729
            else:
730
                quicklook_width = min(
731
                    self.term_window.getmaxyx()[1] - (stats_width + 8 + stats_number * self.space_between_column),
732
                    self._quicklook_max_width - 5,
733
                )
734
            try:
735
                stat_display["quicklook"] = stats.get_plugin('quicklook').get_stats_display(
736
                    max_width=quicklook_width, args=self.args
737
                )
738
            except AttributeError as e:
739
                logger.debug(f"Quicklook plugin not available ({e})")
740
            else:
741
                plugin_widths['quicklook'] = self.get_stats_display_width(stat_display["quicklook"])
742
                stats_width = sum(plugin_widths.values()) + 1
743
            self.space_between_column = 1
744
            self.display_plugin(stat_display["quicklook"])
745
            self.new_column()
746
747
        # Compute spaces between plugins
748
        # Note: Only one space between Quicklook and others
749
        plugin_display_optional = {}
750
        for p in self._top:
751
            plugin_display_optional[p] = True
752
        if stats_number > 1:
753
            self.space_between_column = max(1, int((self.term_window.getmaxyx()[1] - stats_width) / (stats_number - 1)))
754
            for p in ['mem', 'cpu']:
755
                # No space ? Remove optional stats
756
                if self.space_between_column < 3:
757
                    plugin_display_optional[p] = False
758
                    plugin_widths[p] = (
759
                        self.get_stats_display_width(stat_display[p], without_option=True)
760
                        if hasattr(self.args, 'disable_' + p)
761
                        else 0
762
                    )
763
                    stats_width = sum(plugin_widths.values()) + 1
764
                    self.space_between_column = max(
765
                        1, int((self.term_window.getmaxyx()[1] - stats_width) / (stats_number - 1))
766
                    )
767
        else:
768
            self.space_between_column = 0
769
770
        # Display CPU, MEM, SWAP and LOAD
771
        for p in self._top:
772
            if p == 'quicklook':
773
                continue
774
            if p in stat_display:
775
                self.display_plugin(stat_display[p], display_optional=plugin_display_optional[p])
776
            if p != 'load':
777
                # Skip last column
778
                self.new_column()
779
780
        # Space between column
781
        self.space_between_column = 3
782
783
        # Backup line position
784
        self.saved_line = self.next_line
785
786
    def __display_left(self, stat_display):
787
        """Display the left sidebar in the Curses interface."""
788
        self.init_column()
789
790
        if self.args.disable_left_sidebar:
791
            return
792
793
        for p in self._left_sidebar:
794
            if (hasattr(self.args, 'enable_' + p) or hasattr(self.args, 'disable_' + p)) and p in stat_display:
795
                self.new_line()
796
                if p == 'sensors':
797
                    self.display_plugin(
798
                        stat_display['sensors'],
799
                        max_y=(self.term_window.getmaxyx()[0] - self.get_stats_display_height(stat_display['now']) - 2),
800
                    )
801
                else:
802
                    self.display_plugin(stat_display[p])
803
804
    def __display_right(self, stat_display):
805
        """Display the right sidebar in the Curses interface.
806
807
        docker + processcount + amps + processlist + alert
808
        """
809
        # Do not display anything if space is not available...
810
        if self.term_window.getmaxyx()[1] < self._left_sidebar_min_width:
811
            return
812
813
        # Restore line position
814
        self.next_line = self.saved_line
815
816
        # Display right sidebar
817
        self.new_column()
818
        for p in self._right_sidebar():
819
            if (hasattr(self.args, 'enable_' + p) or hasattr(self.args, 'disable_' + p)) and p in stat_display:
820
                self.new_line()
821
                if p in ['processlist', 'programlist']:
822
                    p_index = self._right_sidebar().index(p) + 1
823
                    self.display_plugin(
824
                        stat_display[p],
825
                        display_optional=(self.term_window.getmaxyx()[1] > 102),
826
                        display_additional=(not MACOS),
827
                        max_y=(
828
                            self.term_window.getmaxyx()[0]
829
                            - sum(
830
                                [
831
                                    self.get_stats_display_height(stat_display[i])
832
                                    for i in self._right_sidebar()[p_index:]
833
                                ]
834
                            )
835
                            - 2
836
                        ),
837
                    )
838
                else:
839
                    self.display_plugin(stat_display[p])
840
841
    def display_popup(
842
        self,
843
        message,
844
        size_x=None,
845
        size_y=None,
846
        duration=3,
847
        popup_type='info',
848
        input_size=30,
849
        input_value=None,
850
        is_password=False,
851
    ):
852
        """
853
        Display a centered popup.
854
855
        popup_type: ='info'
856
         Just an information popup, no user interaction
857
         Display a centered popup with the given message during duration seconds
858
         If size_x and size_y: set the popup size
859
         else set it automatically
860
         Return True if the popup could be displayed
861
862
        popup_type='input'
863
         Display a centered popup with the given message and a input field
864
         If size_x and size_y: set the popup size
865
         else set it automatically
866
         Return the input string or None if the field is empty
867
868
        popup_type='yesno'
869
         Display a centered popup with the given message
870
         If size_x and size_y: set the popup size
871
         else set it automatically
872
         Return True (yes) or False (no)
873
        """
874
        # Center the popup
875
        sentence_list = message.split('\n')
876
        if size_x is None:
877
            size_x = len(max(sentence_list, key=len)) + 4
878
            # Add space for the input field
879
            if popup_type == 'input':
880
                size_x += input_size
881
        if size_y is None:
882
            size_y = len(sentence_list) + 4
883
        screen_x = self.term_window.getmaxyx()[1]
884
        screen_y = self.term_window.getmaxyx()[0]
885
        if size_x > screen_x or size_y > screen_y:
886
            # No size to display the popup => abord
887
            return False
888
        pos_x = int((screen_x - size_x) / 2)
889
        pos_y = int((screen_y - size_y) / 2)
890
891
        # Create the popup
892
        popup = curses.newwin(size_y, size_x, pos_y, pos_x)
893
894
        # Fill the popup
895
        popup.border()
896
897
        # Add the message
898
        for y, m in enumerate(sentence_list):
899
            if m:
900
                popup.addnstr(2 + y, 2, m, len(m))
901
902
        if popup_type == 'info':
903
            # Display the popup
904
            popup.refresh()
905
            self.wait(duration * 1000)
906
            return True
907
908
        if popup_type == 'input':
909
            logger.info(popup_type)
910
            logger.info(is_password)
911
            # Create a sub-window for the text field
912
            sub_pop = popup.derwin(1, input_size, 2, 2 + len(m))
0 ignored issues
show
introduced by
The variable m does not seem to be defined in case the for loop on line 898 is not entered. Are you sure this can never be the case?
Loading history...
913
            sub_pop.attron(self.colors_list['FILTER'])
914
            # Init the field with the current value
915
            if input_value is not None:
916
                sub_pop.addnstr(0, 0, input_value, len(input_value))
917
            # Display the popup
918
            popup.refresh()
919
            sub_pop.refresh()
920
            # Create the textbox inside the sub-windows
921
            self.set_cursor(2)
922
            self.term_window.keypad(1)
923
            if is_password:
924
                textbox = getpass.getpass('')
925
                self.set_cursor(0)
926
                if textbox != '':
927
                    return textbox
928
                return None
929
930
            # No password
931
            textbox = GlancesTextbox(sub_pop, insert_mode=True)
932
            textbox.edit()
933
            self.set_cursor(0)
934
            if textbox.gather() != '':
935
                return textbox.gather()[:-1]
936
            return None
937
938
        if popup_type == 'yesno':
939
            # Create a sub-window for the text field
940
            sub_pop = popup.derwin(1, 2, len(sentence_list) + 1, len(m) + 2)
941
            sub_pop.attron(self.colors_list['FILTER'])
942
            # Init the field with the current value
943
            try:
944
                sub_pop.addnstr(0, 0, '', 0)
945
            except curses.error:
946
                pass
947
            # Display the popup
948
            popup.refresh()
949
            sub_pop.refresh()
950
            # Create the textbox inside the sub-windows
951
            self.set_cursor(2)
952
            self.term_window.keypad(1)
953
            textbox = GlancesTextboxYesNo(sub_pop, insert_mode=False)
954
            textbox.edit()
955
            self.set_cursor(0)
956
            # self.term_window.keypad(0)
957
            return textbox.gather()
958
959
        return None
960
961
    def setup_upper_left_pos(self, plugin_stats):
962
        screen_y, screen_x = self.term_window.getmaxyx()
963
964
        if plugin_stats['align'] == 'right':
965
            # Right align (last column)
966
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
967
        else:
968
            display_x = self.column
969
970
        if plugin_stats['align'] == 'bottom':
971
            # Bottom (last line)
972
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
973
        else:
974
            display_y = self.line
975
976
        return display_y, display_x
977
978
    def get_next_x_and_x_max(self, m, x, x_max):
979
        # New column
980
        # Python 2: we need to decode to get real screen size because
981
        # UTF-8 special tree chars occupy several bytes.
982
        # Python 3: strings are strings and bytes are bytes, all is
983
        # good.
984
        try:
985
            x += len(u(m['msg']))
986
        except UnicodeDecodeError:
987
            # Quick and dirty hack for issue #745
988
            pass
989
        if x > x_max:
990
            x_max = x
991
992
        return x, x_max
993
994
    def display_stats_with_current_size(self, m, y, x):
995
        screen_x = self.term_window.getmaxyx()[1]
996
        self.term_window.addnstr(
997
            y,
998
            x,
999
            m['msg'],
1000
            # Do not display outside the screen
1001
            screen_x - x,
1002
            self.colors_list[m['decoration']],
1003
        )
1004
1005
    def display_stats(self, plugin_stats, init, helper):
1006
        y, x, x_max = init
1007
        for m in plugin_stats['msgdict']:
1008
            # New line
1009
            try:
1010
                if m['msg'].startswith('\n'):
1011
                    y, x = helper['goto next, add first col'](y, x)
1012
                    continue
1013
            except Exception:
1014
                # Avoid exception (see issue #1692)
1015
                pass
1016
            # Do not display outside the screen
1017
            if x < 0:
1018
                continue
1019
            if helper['x overbound?'](m, x):
1020
                continue
1021
            if helper['y overbound?'](y):
1022
                break
1023
            # If display_optional = False do not display optional stats
1024
            if helper['display optional?'](m):
1025
                continue
1026
            # If display_additional = False do not display additional stats
1027
            if helper['display additional?'](m):
1028
                continue
1029
            # Is it possible to display the stat with the current screen size
1030
            # !!! Crash if not try/except... Why ???
1031
            try:
1032
                self.display_stats_with_current_size(m, y, x)
1033
            except Exception:
1034
                pass
1035
            else:
1036
                x, x_max = self.get_next_x_and_x_max(m, x, x_max)
1037
1038
        return y, x, x_max
1039
1040
    def display_plugin(self, plugin_stats, display_optional=True, display_additional=True, max_y=65535, add_space=0):
1041
        """Display the plugin_stats on the screen.
1042
1043
        :param plugin_stats:
1044
        :param display_optional: display the optional stats if True
1045
        :param display_additional: display additional stats if True
1046
        :param max_y: do not display line > max_y
1047
        :param add_space: add x space (line) after the plugin
1048
        """
1049
        # Exit if:
1050
        # - the plugin_stats message is empty
1051
        # - the display tag = False
1052
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
1053
            # Exit
1054
            return 0
1055
1056
        # Get the screen size
1057
        screen_y, screen_x = self.term_window.getmaxyx()
1058
1059
        # Set the upper/left position of the message
1060
        display_y, display_x = self.setup_upper_left_pos(plugin_stats)
1061
1062
        helper = {
1063
            'goto next, add first col': lambda y, x: (y + 1, display_x),
0 ignored issues
show
introduced by
The variable display_x does not seem to be defined for all execution paths.
Loading history...
1064
            'x overbound?': lambda m, x: not m['splittable'] and (x + len(m['msg']) > screen_x),
0 ignored issues
show
introduced by
The variable screen_x does not seem to be defined for all execution paths.
Loading history...
1065
            'y overbound?': lambda y: y < 0 or (y + 1 > screen_y) or (y > max_y),
0 ignored issues
show
introduced by
The variable screen_y does not seem to be defined for all execution paths.
Loading history...
1066
            'display optional?': lambda m: not display_optional and m['optional'],
1067
            'display additional?': lambda m: not display_additional and m['additional'],
1068
        }
1069
1070
        # Display
1071
        init = display_y, display_x, display_x
1072
        y, x, x_max = self.display_stats(plugin_stats, init, helper)
1073
1074
        # Compute the next Glances column/line position
1075
        self.next_column = max(self.next_column, x_max + self.space_between_column)
1076
        self.next_line = max(self.next_line, y + self.space_between_line)
1077
1078
        # Have empty lines after the plugins
1079
        self.next_line += add_space
1080
        return None
1081
1082
    def clear(self):
1083
        """Erase the content of the screen.
1084
        The difference is that clear() also calls clearok(). clearok()
1085
        basically tells ncurses to forget whatever it knows about the current
1086
        terminal contents, so that when refresh() is called, it will actually
1087
        begin by clearing the entire terminal screen before redrawing any of it."""
1088
        self.term_window.clear()
1089
1090
    def erase(self):
1091
        """Erase the content of the screen.
1092
        erase() on the other hand, just clears the screen (the internal
1093
        object, not the terminal screen). When refresh() is later called,
1094
        ncurses will still compute the minimum number of characters to send to
1095
        update the terminal."""
1096
        self.term_window.erase()
1097
1098
    def refresh(self):
1099
        """Refresh the windows"""
1100
        self.term_window.refresh()
1101
1102
    def flush(self, stats, cs_status=None):
1103
        """Erase and update the screen.
1104
1105
        :param stats: Stats database to display
1106
        :param cs_status:
1107
            "None": standalone or server mode
1108
            "Connected": Client is connected to the server
1109
            "Disconnected": Client is disconnected from the server
1110
        """
1111
        # See https://stackoverflow.com/a/43486979/1919431
1112
        self.erase()
1113
        self.display(stats, cs_status=cs_status)
1114
        self.refresh()
1115
1116
    def update(self, stats, duration=3, cs_status=None, return_to_browser=False):
1117
        """Update the screen.
1118
1119
        :param stats: Stats database to display
1120
        :param duration: duration of the loop
1121
        :param cs_status:
1122
            "None": standalone or server mode
1123
            "Connected": Client is connected to the server
1124
            "Disconnected": Client is disconnected from the server
1125
        :param return_to_browser:
1126
            True: Do not exist, return to the browser list
1127
            False: Exit and return to the shell
1128
1129
        :return: True if exit key has been pressed else False
1130
        """
1131
        # Flush display
1132
        self.flush(stats, cs_status=cs_status)
1133
1134
        # If the duration is < 0 (update + export time > refresh_time)
1135
        # Then display the interface and log a message
1136
        if duration <= 0:
1137
            logger.warning('Update and export time higher than refresh_time.')
1138
            duration = 0.1
1139
1140
        # Wait duration (in s) time
1141
        isexitkey = False
1142
        countdown = Timer(duration)
1143
        # Set the default timeout (in ms) between two getch
1144
        self.term_window.timeout(100)
1145
        while not countdown.finished() and not isexitkey:
1146
            # Getkey
1147
            pressedkey = self.__catch_key(return_to_browser=return_to_browser)
1148
            isexitkey = pressedkey == ord('\x1b') or pressedkey == ord('q')
1149
1150
            if pressedkey == curses.KEY_F5 or self.pressedkey == 18:
1151
                # Were asked to refresh (F5 or Ctrl-R)
1152
                self.clear()
1153
                return isexitkey
1154
1155
            if pressedkey in (curses.KEY_UP, 65, curses.KEY_DOWN, 66):
1156
                # Up of won key pressed, reset the countdown
1157
                # Better for user experience
1158
                countdown.reset()
1159
1160
            if isexitkey and self.args.help_tag:
1161
                # Quit from help should return to main screen, not exit #1874
1162
                self.args.help_tag = not self.args.help_tag
1163
                return False
1164
1165
            if not isexitkey and pressedkey > -1:
1166
                # Redraw display
1167
                self.flush(stats, cs_status=cs_status)
1168
                # Overwrite the timeout with the countdown
1169
                self.wait(delay=int(countdown.get() * 1000))
1170
1171
        return isexitkey
1172
1173
    def wait(self, delay=100):
1174
        """Wait delay in ms"""
1175
        curses.napms(delay)
1176
1177
    def get_stats_display_width(self, curse_msg, without_option=False):
1178
        """Return the width of the formatted curses message."""
1179
        try:
1180
            if without_option:
1181
                # Size without options
1182
                c = len(
1183
                    max(
1184
                        ''.join(
1185
                            [
1186
                                (u(u(nativestr(i['msg'])).encode('ascii', 'replace')) if not i['optional'] else "")
1187
                                for i in curse_msg['msgdict']
1188
                            ]
1189
                        ).split('\n'),
1190
                        key=len,
1191
                    )
1192
                )
1193
            else:
1194
                # Size with all options
1195
                c = len(
1196
                    max(
1197
                        ''.join(
1198
                            [u(u(nativestr(i['msg'])).encode('ascii', 'replace')) for i in curse_msg['msgdict']]
1199
                        ).split('\n'),
1200
                        key=len,
1201
                    )
1202
                )
1203
        except Exception as e:
1204
            logger.debug(f'ERROR: Can not compute plugin width ({e})')
1205
            return 0
1206
        else:
1207
            return c
1208
1209
    def get_stats_display_height(self, curse_msg):
1210
        """Return the height of the formatted curses message.
1211
1212
        The height is defined by the number of '\n' (new line).
1213
        """
1214
        try:
1215
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
1216
        except Exception as e:
1217
            logger.debug(f'ERROR: Can not compute plugin height ({e})')
1218
            return 0
1219
        else:
1220
            return c + 1
1221
1222
1223
class GlancesCursesStandalone(_GlancesCurses):
1224
    """Class for the Glances curse standalone."""
1225
1226
    # Default number of processes to displayed is set to 50
1227
    glances_processes.max_processes = 50
1228
1229
1230
class GlancesCursesClient(_GlancesCurses):
1231
    """Class for the Glances curse client."""
1232
1233
    # Default number of processes to displayed is set to 50
1234
    # For the moment, cursor in client/server mode is not supported see #3221
1235
    glances_processes.max_processes = 50
1236
1237
1238
class GlancesTextbox(Textbox):
1239
    def __init__(self, *args, **kwargs):
1240
        super().__init__(*args, **kwargs)
1241
1242
    def do_command(self, ch):
1243
        if ch == 10:  # Enter
1244
            return 0
1245
        if ch == 127:  # Back
1246
            return 8
1247
        return super().do_command(ch)
1248
1249
1250
class GlancesTextboxYesNo(Textbox):
1251
    def __init__(self, *args, **kwargs):
1252
        super().__init__(*args, **kwargs)
1253
1254
    def do_command(self, ch):
1255
        return super().do_command(ch)
1256