Test Failed
Pull Request — develop (#2687)
by
unknown
02:09
created

_GlancesCurses._handle_cursor_up()   A

Complexity

Conditions 2

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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