summaryrefslogtreecommitdiffstats
path: root/glances/outputs/glances_curses.py
diff options
context:
space:
mode:
Diffstat (limited to 'glances/outputs/glances_curses.py')
-rw-r--r--glances/outputs/glances_curses.py192
1 files changed, 149 insertions, 43 deletions
diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py
index 87764316..121bbf8c 100644
--- a/glances/outputs/glances_curses.py
+++ b/glances/outputs/glances_curses.py
@@ -50,10 +50,13 @@ class _GlancesCurses(object):
"""
_hotkeys = {
+ # 'ENTER' > Edit the process filter
'0': {'switch': 'disable_irix'},
'1': {'switch': 'percpu'},
'2': {'switch': 'disable_left_sidebar'},
'3': {'switch': 'disable_quicklook'},
+ # '4' > Enable or disable quicklook
+ # '5' > Enable or disable top menu
'6': {'switch': 'meangpu'},
'/': {'switch': 'process_short_name'},
'a': {'sort_key': 'auto'},
@@ -64,13 +67,17 @@ class _GlancesCurses(object):
'C': {'switch': 'disable_cloud'},
'd': {'switch': 'disable_diskio'},
'D': {'switch': 'disable_docker'},
+ # 'e' > Enable/Disable process extended
+ # 'E' > Erase the process filter
+ # 'f' > Show/hide fs / folder stats
'F': {'switch': 'fs_free_space'},
'g': {'switch': 'generate_graph'},
'G': {'switch': 'disable_gpu'},
'h': {'switch': 'help_tag'},
'i': {'sort_key': 'io_counters'},
'I': {'switch': 'disable_ip'},
- 'k': {'switch': 'disable_connections'},
+ # 'k' > Kill selected process
+ 'K': {'switch': 'disable_connections'},
'l': {'switch': 'disable_alert'},
'm': {'sort_key': 'memory_percent'},
'M': {'switch': 'reset_minmax_tag'},
@@ -78,6 +85,7 @@ class _GlancesCurses(object):
'N': {'switch': 'disable_now'},
'p': {'sort_key': 'name'},
'P': {'switch': 'disable_ports'},
+ # 'q' or ESCAPE > Quit
'Q': {'switch': 'enable_irq'},
'r': {'switch': 'disable_smart'},
'R': {'switch': 'disable_raid'},
@@ -87,7 +95,14 @@ class _GlancesCurses(object):
'T': {'switch': 'network_sum'},
'u': {'sort_key': 'username'},
'U': {'switch': 'network_cumul'},
+ # 'w' > Delete finished warning logs
'W': {'switch': 'disable_wifi'},
+ # 'x' > Delete finished warning and critical logs
+ # 'z' > Enable or disable processes
+ # "<" (left arrow) navigation through process sort
+ # ">" (right arrow) navigation through process sort
+ # 'UP' > Up in the server list
+ # 'DOWN' > Down in the server list
}
_sort_loop = ['cpu_percent', 'memory_percent', 'username',
@@ -144,9 +159,15 @@ class _GlancesCurses(object):
# Init edit filter tag
self.edit_filter = False
+ # Init kill process tag
+ self.kill_process = False
+
# Init the process min/max reset
self.args.reset_minmax_tag = False
+ # Init cursor
+ self.args.cursor_position = 0
+
# Catch key pressed with non blocking mode
self.term_window.keypad(1)
self.term_window.nodelay(1)
@@ -188,6 +209,8 @@ class _GlancesCurses(object):
try:
if hasattr(curses, 'start_color'):
curses.start_color()
+ logger.debug(
+ 'Curses interface compatible with {} colors'.format(curses.COLORS))
if hasattr(curses, 'use_default_colors'):
curses.use_default_colors()
except Exception as e:
@@ -226,35 +249,35 @@ class _GlancesCurses(object):
curses.init_pair(8, curses.COLOR_BLUE, -1)
# Colors text styles
- if curses.COLOR_PAIRS > 8:
- try:
- curses.init_pair(9, curses.COLOR_MAGENTA, -1)
- except Exception:
- if self.is_theme('white'):
- curses.init_pair(9, curses.COLOR_BLACK, -1)
- else:
- curses.init_pair(9, curses.COLOR_WHITE, -1)
- try:
- curses.init_pair(10, curses.COLOR_CYAN, -1)
- except Exception:
- if self.is_theme('white'):
- curses.init_pair(10, curses.COLOR_BLACK, -1)
- else:
- curses.init_pair(10, curses.COLOR_WHITE, -1)
-
- self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
- self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
- self.filter_color = curses.color_pair(10) | A_BOLD
-
self.no_color = curses.color_pair(1)
self.default_color = curses.color_pair(3) | A_BOLD
- self.nice_color = curses.color_pair(9)
- self.cpu_time_color = curses.color_pair(9)
+ self.nice_color = curses.color_pair(5)
+ self.cpu_time_color = curses.color_pair(5)
self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD
self.ifWARNING_color = curses.color_pair(5) | A_BOLD
self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD
self.default_color2 = curses.color_pair(7)
self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
+ self.ifWARNING_color2 = curses.color_pair(5) | A_BOLD
+ self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
+ self.filter_color = A_BOLD
+ self.selected_color = A_BOLD
+
+ if curses.COLOR_PAIRS > 8:
+ colors_list = [curses.COLOR_MAGENTA, curses.COLOR_CYAN, curses.COLOR_YELLOW]
+ for i in range(0, 3):
+ try:
+ curses.init_pair(i + 9, colors_list[i], -1)
+ except Exception:
+ if self.is_theme('white'):
+ curses.init_pair(i + 9, curses.COLOR_BLACK, -1)
+ else:
+ curses.init_pair(i + 9, curses.COLOR_WHITE, -1)
+ self.nice_color = curses.color_pair(9)
+ self.cpu_time_color = curses.color_pair(9)
+ self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
+ self.filter_color = curses.color_pair(10) | A_BOLD
+ self.selected_color = curses.color_pair(11) | A_BOLD
else:
# The screen is NOT compatible with a colored design
@@ -271,6 +294,7 @@ class _GlancesCurses(object):
self.ifWARNING_color2 = A_BOLD
self.ifCRITICAL_color2 = curses.A_REVERSE
self.filter_color = A_BOLD
+ self.selected_color = A_BOLD
# Define the colors list (hash table) for stats
self.colors_list = {
@@ -283,6 +307,7 @@ class _GlancesCurses(object):
'FILTER': self.filter_color,
'TITLE': self.title_color,
'PROCESS': self.default_color2,
+ 'PROCESS_SELECTED': self.default_color2 | curses.A_UNDERLINE,
'STATUS': self.default_color2,
'NICE': self.nice_color,
'CPU_TIME': self.cpu_time_color,
@@ -293,7 +318,8 @@ class _GlancesCurses(object):
'CAREFUL_LOG': self.ifCAREFUL_color,
'WARNING_LOG': self.ifWARNING_color,
'CRITICAL_LOG': self.ifCRITICAL_color,
- 'PASSWORD': curses.A_PROTECT
+ 'PASSWORD': curses.A_PROTECT,
+ 'SELECTED': self.selected_color
}
def set_cursor(self, value):
@@ -331,22 +357,18 @@ class _GlancesCurses(object):
self._hotkeys[hotkey]['sort_key'] == 'auto')
# Other actions...
- if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
- # 'ESC'|'q' > Quit
- if return_to_browser:
- logger.info("Stop Glances client and return to the browser")
- else:
- logger.info("Stop Glances (keypressed: {})".format(self.pressedkey))
- elif self.pressedkey == ord('\n'):
+ if self.pressedkey == ord('\n'):
# 'ENTER' > Edit the process filter
self.edit_filter = not self.edit_filter
elif self.pressedkey == ord('4'):
+ # '4' > Enable or disable quicklook
self.args.full_quicklook = not self.args.full_quicklook
if self.args.full_quicklook:
self.enable_fullquicklook()
else:
self.disable_fullquicklook()
elif self.pressedkey == ord('5'):
+ # '5' > Enable or disable top menu
self.args.disable_top = not self.args.disable_top
if self.args.disable_top:
self.disable_top()
@@ -366,6 +388,9 @@ class _GlancesCurses(object):
# 'f' > Show/hide fs / folder stats
self.args.disable_fs = not self.args.disable_fs
self.args.disable_folders = not self.args.disable_folders
+ elif self.pressedkey == ord('k'):
+ # 'k' > Kill selected process (after confirmation)
+ self.kill_process = not self.kill_process
elif self.pressedkey == ord('w'):
# 'w' > Delete finished warning logs
glances_events.clean()
@@ -387,6 +412,25 @@ class _GlancesCurses(object):
# ">" (right arrow) navigation through process sort
next_sort = (self.loop_position() + 1) % len(self._sort_loop)
glances_processes.set_sort_key(self._sort_loop[next_sort], False)
+ elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65:
+ # 'UP' > Up in the server list
+ if self.args.cursor_position > 0:
+ self.args.cursor_position -= 1
+ elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66:
+ # 'DOWN' > Down in the server list
+ # if self.args.cursor_position < glances_processes.max_processes - 2:
+ if self.args.cursor_position < glances_processes.processes_count:
+ self.args.cursor_position += 1
+ elif self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
+ # 'ESC'|'q' > Quit
+ if return_to_browser:
+ logger.info("Stop Glances client and return to the browser")
+ else:
+ logger.info(
+ "Stop Glances (keypressed: {})".format(self.pressedkey))
+ elif self.pressedkey == curses.KEY_F5:
+ # "F5" manual refresh requested
+ pass
# Return the key code
return self.pressedkey
@@ -591,13 +635,39 @@ class _GlancesCurses(object):
'- cmdline:.*glances.*\n' +
'- username:nicolargo\n' +
'- username:^root ',
- is_input=True,
+ popup_type='input',
input_value=glances_processes.process_filter_input)
glances_processes.process_filter = new_filter
elif self.edit_filter and cs_status is not None:
self.display_popup('Process filter only available in standalone mode')
self.edit_filter = False
+ # Display kill process confirmation popup
+ # Only in standalone mode (cs_status is None)
+ if self.kill_process and cs_status is None:
+ selected_process_raw = stats.get_plugin('processlist').get_raw()[
+ self.args.cursor_position]
+ confirm = self.display_popup(
+ 'Kill process: {} (pid: {}) ?\n\nConfirm ([y]es/[n]o): '.format(
+ selected_process_raw['name'],
+ selected_process_raw['pid']),
+ popup_type='yesno')
+ if confirm.lower().startswith('y'):
+ try:
+ ret_kill = glances_processes.kill(selected_process_raw['pid'])
+ except Exception as e:
+ logger.error('Can not kill process {} ({})'.format(
+ selected_process_raw['name'], e))
+ else:
+ logger.info('Kill signal has been sent to process {} (return code: {})'.format(
+ selected_process_raw['name'], ret_kill))
+
+ elif self.kill_process and cs_status is not None:
+ self.display_popup(
+ 'Kill process only available in standalone mode')
+ self.kill_process = False
+
+
# Display graph generation popup
if self.args.generate_graph:
self.display_popup('Generate graph in {}'.format(self.args.export_graph_path))
@@ -753,30 +823,37 @@ class _GlancesCurses(object):
def display_popup(self, message,
size_x=None, size_y=None,
duration=3,
- is_input=False,
+ popup_type='info',
input_size=30,
input_value=None):
"""
Display a centered popup.
- If is_input is False:
+ popup_type='info'
+ Just an infotmation popup, no user interaction
Display a centered popup with the given message during duration seconds
If size_x and size_y: set the popup size
else set it automatically
Return True if the popup could be displayed
- If is_input is True:
+ popup_type='input'
Display a centered popup with the given message and a input field
If size_x and size_y: set the popup size
else set it automatically
Return the input string or None if the field is empty
+
+ popup_type='yesno'
+ Display a centered popup with the given message
+ If size_x and size_y: set the popup size
+ else set it automatically
+ Return True (yes) or False (no)
"""
# Center the popup
sentence_list = message.split('\n')
if size_x is None:
size_x = len(max(sentence_list, key=len)) + 4
# Add space for the input field
- if is_input:
+ if popup_type == 'input':
size_x += input_size
if size_y is None:
size_y = len(sentence_list) + 4
@@ -795,10 +872,15 @@ class _GlancesCurses(object):
popup.border()
# Add the message
- for y, m in enumerate(message.split('\n')):
+ for y, m in enumerate(sentence_list):
popup.addnstr(2 + y, 2, m, len(m))
- if is_input:
+ if popup_type == 'info':
+ # Display the popup
+ popup.refresh()
+ self.wait(duration * 1000)
+ return True
+ elif popup_type == 'input':
# Create a subwindow for the text field
subpop = popup.derwin(1, input_size, 2, 2 + len(m))
subpop.attron(self.colors_list['FILTER'])
@@ -811,10 +893,10 @@ class _GlancesCurses(object):
# Create the textbox inside the subwindows
self.set_cursor(2)
self.term_window.keypad(1)
- textbox = GlancesTextbox(subpop, insert_mode=False)
+ textbox = GlancesTextbox(subpop, insert_mode=True)
textbox.edit()
self.set_cursor(0)
- self.term_window.keypad(0)
+ # self.term_window.keypad(0)
if textbox.gather() != '':
logger.debug(
"User enters the following string: %s" % textbox.gather())
@@ -822,11 +904,23 @@ class _GlancesCurses(object):
else:
logger.debug("User centers an empty string")
return None
- else:
+ elif popup_type == 'yesno':
+ # # Create a subwindow for the text field
+ subpop = popup.derwin(1, 2, len(sentence_list) + 1, len(m) + 2)
+ subpop.attron(self.colors_list['FILTER'])
+ # Init the field with the current value
+ subpop.addnstr(0, 0, '', 0)
# Display the popup
popup.refresh()
- self.wait(duration * 1000)
- return True
+ subpop.refresh()
+ # Create the textbox inside the subwindows
+ self.set_cursor(2)
+ self.term_window.keypad(1)
+ textbox = GlancesTextboxYesNo(subpop, insert_mode=False)
+ textbox.edit()
+ self.set_cursor(0)
+ # self.term_window.keypad(0)
+ return textbox.gather()
def display_plugin(self, plugin_stats,
display_optional=True,
@@ -981,6 +1075,9 @@ class _GlancesCurses(object):
pressedkey = self.__catch_key(return_to_browser=return_to_browser)
# Is it an exit key ?
exitkey = (pressedkey == ord('\x1b') or pressedkey == ord('q'))
+ if pressedkey == curses.KEY_F5:
+ # were asked to refresh
+ return exitkey
if not exitkey and pressedkey > -1:
# Redraw display
self.flush(stats, cs_status=cs_status)
@@ -1049,3 +1146,12 @@ class GlancesTextbox(Textbox, object):
if ch == 127: # Back
return 8
return super(GlancesTextbox, self).do_command(ch)
+
+
+class GlancesTextboxYesNo(Textbox, object):
+
+ def __init__(self, *args, **kwargs):
+ super(GlancesTextboxYesNo, self).__init__(*args, **kwargs)
+
+ def do_command(self, ch):
+ return super(GlancesTextboxYesNo, self).do_command(ch)