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