Refactor how sensors is displayed in the curses interface

This commit is contained in:
nicolargo 2024-04-28 10:28:19 +02:00
parent a60e8a42c7
commit 6b0de5d6be
4 changed files with 16 additions and 494 deletions

View File

@ -27,7 +27,7 @@ history_size=1200
separator=True
# Set the the Curses and WebUI interface left menu plugin list (comma-separated)
#left_menu=network,wifi,connections,ports,diskio,fs,irq,folders,raid,smart,sensors,now
# Limit the number of processes to display
# Limit the number of processes to display (in the WebUI)
max_processes_display=25
# Set URL prefix for the WebUI and the API
# Example: url_prefix=/glances/ => http://localhost/glances/

View File

@ -27,7 +27,7 @@ history_size=1200
separator=True
# Set the the Curses and WebUI interface left menu plugin list (comma-separated)
#left_menu=network,wifi,connections,ports,diskio,fs,irq,folders,raid,smart,sensors,now
# Limit the number of processes to display
# Limit the number of processes to display (in the WebUI)
max_processes_display=25
# Set URL prefix for the WebUI and the API
# Example: url_prefix=/glances/ => http://localhost/glances/

View File

@ -596,14 +596,7 @@ class _GlancesCurses(object):
ret = {}
for p in stats.getPluginsList(enable=False):
if p == 'quicklook' or p == 'processlist':
# - processlist is done later
# because we need to know how many processes could be displayed
# - quicklook is done later
# because it is based on CPU, MEM, SWAP and LOAD
continue
# Compute the plugin max size
# Compute the plugin max size for the left sidebar
plugin_max_width = None
if p in self._left_sidebar:
plugin_max_width = min(self._left_sidebar_max_width,
@ -616,33 +609,6 @@ class _GlancesCurses(object):
return ret
def set_number_of_processes(self, stat_display):
"""Set the number of processes to display
The value is dynamicaly computed."""
max_processes_displayed = (
self.term_window.getmaxyx()[0]
- 11 # Glances header + Top + Process header
- (0 if 'containers' not in stat_display else self.get_stats_display_height(stat_display["containers"]))
- (
0
if 'processcount' not in stat_display
else self.get_stats_display_height(stat_display["processcount"])
)
- (0 if 'amps' not in stat_display else self.get_stats_display_height(stat_display["amps"]))
- (0 if 'alert' not in stat_display else self.get_stats_display_height(stat_display["alert"]))
)
try:
if self.args.enable_process_extended:
max_processes_displayed -= 4
except AttributeError:
pass
if max_processes_displayed < 0:
max_processes_displayed = 0
if glances_processes.max_processes is None or glances_processes.max_processes != max_processes_displayed:
logger.info("Set number of displayed processes to {}".format(max_processes_displayed))
glances_processes.max_processes = max_processes_displayed
def display(self, stats, cs_status=None):
"""Display stats on the screen.
@ -661,16 +627,10 @@ class _GlancesCurses(object):
# Update the stats messages
###########################
# Get all the plugins but quicklook and process list
# Get all the plugins view
self.args.cs_status = cs_status
__stat_display = self.__get_stat_display(stats, layer=cs_status)
# Adapt number of processes to the available space
self.set_number_of_processes(__stat_display)
# Get the processlist
__stat_display["processlist"] = stats.get_plugin('processlist').get_stats_display(args=self.args)
# Display the stats on the curses interface
###########################################
@ -681,10 +641,10 @@ class _GlancesCurses(object):
# ... and exit
return False
# =====================================
# =======================================
# Display first line (system+ip+uptime)
# Optionally: Cloud on second line
# =====================================
# Optionally: Cloud is on the second line
# =======================================
self.__display_header(__stat_display)
self.separator_line()
@ -919,7 +879,15 @@ class _GlancesCurses(object):
for p in self._left_sidebar:
if (hasattr(self.args, 'enable_' + p) or hasattr(self.args, 'disable_' + p)) and p in stat_display:
self.new_line()
self.display_plugin(stat_display[p])
if p == 'sensors':
self.display_plugin(
stat_display['sensors'],
max_y=(
self.term_window.getmaxyx()[0] - self.get_stats_display_height(stat_display['now']) - 2
),
)
else:
self.display_plugin(stat_display[p])
def __display_right(self, stat_display):
"""Display the right sidebar in the Curses interface.
@ -937,9 +905,6 @@ class _GlancesCurses(object):
self.new_column()
for p in self._right_sidebar:
if (hasattr(self.args, 'enable_' + p) or hasattr(self.args, 'disable_' + p)) and p in stat_display:
if p not in p:
# Catch for issue #1470
continue
self.new_line()
if p == 'processlist':
self.display_plugin(

View File

@ -1,443 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2021 Nicolargo <nicolas@nicolargo.com>
#
# Glances is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Glances is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
I am your father...
...for all Glances view plugins.
"""
import json
from glances.globals import listkeys
from glances.plugins.plugin.model import fields_unit_short, fields_unit_type
class GlancesPluginView(object):
"""Main class for Glances plugin view."""
def __init__(self, args=None):
"""Init the plugin of plugins class.
All Glances' plugins should inherit from this class. Most of the
methods are already implemented in the father classes.
Your plugin should return a dict or a list of dicts (stored in the
self.stats). As an example, you can have a look on the mem plugin
(for dict) or network (for list of dicts).
A plugin should implement:
- the __init__ constructor: define the self.display_curse
and optionnaly:
- the update_view method: only if you need to trick your output
- the msg_curse: define the curse (UI) message (if display_curse is True)
- all others methods you want to overwrite
:args: args parameters
"""
# Init the args
self.args = args
# Init the default alignement (for curses)
self._align = 'left'
# Init the views
self.views = dict()
# Hide stats if all the hide_zero_fields has never been != 0
# Default is False, always display stats
self.hide_zero = False
self.hide_zero_fields = []
def __repr__(self):
"""Return the raw views."""
return self.views
def __str__(self):
"""Return the human-readable views."""
# TODO: a better method to display views
return str(self.views)
def _json_dumps(self, d):
"""Return the object 'd' in a JSON format.
Manage the issue #815 for Windows OS
"""
try:
return json.dumps(d)
except UnicodeDecodeError:
return json.dumps(d, ensure_ascii=False)
def get_raw(self):
"""Return the stats object."""
return self.views
def get_export(self):
"""Return the stats object to export."""
return self.get_raw()
def update_views_hidden(self):
"""If the self.hide_zero is set then update the hidden field of the view
It will check if all fields values are already be different from 0
In this case, the hidden field is set to True
Note: This function should be called by plugin (in the update_views method)
Example (for network plugin):
__Init__
self.hide_zero_fields = ['rx', 'tx']
Update views
...
self.update_views_hidden()
"""
if not self.hide_zero:
return False
if isinstance(self.get_raw(), list) and self.get_raw() is not None and self.get_key() is not None:
# Stats are stored in a list of dict (ex: NETWORK, FS...)
for i in self.get_raw():
if any([i[f] for f in self.hide_zero_fields]):
for f in self.hide_zero_fields:
self.views[i[self.get_key()]][f]['_zero'] = self.views[i[self.get_key()]][f]['hidden']
for f in self.hide_zero_fields:
self.views[i[self.get_key()]][f]['hidden'] = self.views[i[self.get_key()]][f]['_zero'] and i[f] == 0
elif isinstance(self.get_raw(), dict) and self.get_raw() is not None:
#
# Warning: This code has never been tested because
# no plugin with dict instance use the hidden function...
# vvvv
#
# Stats are stored in a dict (ex: CPU, LOAD...)
for key in listkeys(self.get_raw()):
if any([self.get_raw()[f] for f in self.hide_zero_fields]):
for f in self.hide_zero_fields:
self.views[f]['_zero'] = self.views[f]['hidden']
for f in self.hide_zero_fields:
self.views[f]['hidden'] = self.views['_zero'] and self.views[f] == 0
return True
def update_views(self):
"""Update the stats views.
The V of MVC
A dict of dict with the needed information to display the stats.
Example for the stat xxx:
'xxx': {'decoration': 'DEFAULT', >>> The decoration of the stats
'optional': False, >>> Is the stat optional
'additional': False, >>> Is the stat provide additional information
'splittable': False, >>> Is the stat can be cut (like process lon name)
'hidden': False, >>> Is the stats should be hidden in the UI
'_zero': True} >>> For internal purpose only
"""
ret = {}
if isinstance(self.get_raw(), list) and self.get_raw() is not None and self.get_key() is not None:
# Stats are stored in a list of dict (ex: NETWORK, FS...)
for i in self.get_raw():
# i[self.get_key()] is the interface name (example for NETWORK)
ret[i[self.get_key()]] = {}
for key in listkeys(i):
value = {
'decoration': 'DEFAULT',
'optional': False,
'additional': False,
'splittable': False,
'hidden': False,
'_zero': self.views[i[self.get_key()]][key]['_zero']
if i[self.get_key()] in self.views
and key in self.views[i[self.get_key()]]
and 'zero' in self.views[i[self.get_key()]][key]
else True,
}
ret[i[self.get_key()]][key] = value
elif isinstance(self.get_raw(), dict) and self.get_raw() is not None:
# Stats are stored in a dict (ex: CPU, LOAD...)
for key in listkeys(self.get_raw()):
value = {
'decoration': 'DEFAULT',
'optional': False,
'additional': False,
'splittable': False,
'hidden': False,
'_zero': self.views[key]['_zero'] if key in self.views and '_zero' in self.views[key] else True,
}
ret[key] = value
self.views = ret
return self.views
def set_views(self, input_views):
"""Set the views to input_views."""
self.views = input_views
def get_views(self, item=None, key=None, option=None):
"""Return the views object.
If key is None, return all the view for the current plugin
else if option is None return the view for the specific key (all option)
else return the view fo the specific key/option
Specify item if the stats are stored in a dict of dict (ex: NETWORK, FS...)
"""
if item is None:
item_views = self.views
else:
item_views = self.views[item]
if key is None:
return item_views
else:
if option is None:
return item_views[key]
else:
if option in item_views[key]:
return item_views[key][option]
else:
return 'DEFAULT'
def get_json_views(self, item=None, key=None, option=None):
"""Return the views (in JSON)."""
return self._json_dumps(self.get_views(item, key, option))
def msg_curse(self, args=None, max_width=None):
"""Return default string to display in the curse interface."""
return [self.curse_add_line(str(self.stats))]
def get_stats_display(self, args=None, max_width=None):
"""Return a dict with all the information needed to display the stat.
key | description
----------------------------
display | Display the stat (True or False)
msgdict | Message to display (list of dict [{ 'msg': msg, 'decoration': decoration } ... ])
align | Message position
"""
display_curse = False
if hasattr(self, 'display_curse'):
display_curse = self.display_curse
if hasattr(self, 'align'):
align_curse = self._align
if max_width is not None:
ret = {'display': display_curse, 'msgdict': self.msg_curse(args, max_width=max_width), 'align': align_curse}
else:
ret = {'display': display_curse, 'msgdict': self.msg_curse(args), 'align': align_curse}
return ret
def curse_add_line(self, msg, decoration="DEFAULT", optional=False, additional=False, splittable=False):
"""Return a dict with.
Where:
msg: string
decoration:
DEFAULT: no decoration
UNDERLINE: underline
BOLD: bold
TITLE: for stat title
PROCESS: for process name
STATUS: for process status
NICE: for process niceness
CPU_TIME: for process cpu time
OK: Value is OK and non logged
OK_LOG: Value is OK and logged
CAREFUL: Value is CAREFUL and non logged
CAREFUL_LOG: Value is CAREFUL and logged
WARNING: Value is WARINING and non logged
WARNING_LOG: Value is WARINING and logged
CRITICAL: Value is CRITICAL and non logged
CRITICAL_LOG: Value is CRITICAL and logged
optional: True if the stat is optional (display only if space is available)
additional: True if the stat is additional (display only if space is available after optional)
spittable: Line can be splitted to fit on the screen (default is not)
"""
return {
'msg': msg,
'decoration': decoration,
'optional': optional,
'additional': additional,
'splittable': splittable,
}
def curse_new_line(self):
"""Go to a new line."""
return self.curse_add_line('\n')
def curse_add_stat(self, key, width=None, header='', separator='', trailer=''):
"""Return a list of dict messages with the 'key: value' result
<=== width ===>
__key : 80.5%__
| | | | |_ trailer
| | | |_ self.stats[key]
| | |_ separator
| |_ key
|_ trailer
Instead of:
msg = ' {:8}'.format('idle:')
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
msg = '{:5.1f}%'.format(self.stats['idle'])
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
Use:
ret.extend(self.curse_add_stat('idle', width=15, header=' '))
"""
if key not in self.stats:
return []
# Check if a shortname is defined
if 'short_name' in self.fields_description[key]:
key_name = self.fields_description[key]['short_name']
else:
key_name = key
# Check if unit is defined and get the short unit char in the unit_sort dict
if 'unit' in self.fields_description[key] and self.fields_description[key]['unit'] in fields_unit_short:
# Get the shortname
unit_short = fields_unit_short[self.fields_description[key]['unit']]
else:
unit_short = ''
# Check if unit is defined and get the unit type unit_type dict
if 'unit' in self.fields_description[key] and self.fields_description[key]['unit'] in fields_unit_type:
# Get the shortname
unit_type = fields_unit_type[self.fields_description[key]['unit']]
else:
unit_type = 'float'
# Is it a rate ? Yes, compute it thanks to the time_since_update key
if 'rate' in self.fields_description[key] and self.fields_description[key]['rate'] is True:
value = self.stats[key] // self.stats['time_since_update']
else:
value = self.stats[key]
if width is None:
msg_item = header + '{}'.format(key_name) + separator
if unit_type == 'float':
msg_value = '{:.1f}{}'.format(value, unit_short) + trailer
elif 'min_symbol' in self.fields_description[key]:
msg_value = (
'{}{}'.format(
self.auto_unit(int(value), min_symbol=self.fields_description[key]['min_symbol']), unit_short
)
+ trailer
)
else:
msg_value = '{}{}'.format(int(value), unit_short) + trailer
else:
# Define the size of the message
# item will be on the left
# value will be on the right
msg_item = header + '{:{width}}'.format(key_name, width=width - 7) + separator
if unit_type == 'float':
msg_value = '{:5.1f}{}'.format(value, unit_short) + trailer
elif 'min_symbol' in self.fields_description[key]:
msg_value = (
'{:>5}{}'.format(
self.auto_unit(int(value), min_symbol=self.fields_description[key]['min_symbol']), unit_short
)
+ trailer
)
else:
msg_value = '{:>5}{}'.format(int(value), unit_short) + trailer
decoration = self.get_views(key=key, option='decoration')
optional = self.get_views(key=key, option='optional')
return [
self.curse_add_line(msg_item, optional=optional),
self.curse_add_line(msg_value, decoration=decoration, optional=optional),
]
@property
def align(self):
"""Get the curse align."""
return self._align
@align.setter
def align(self, value):
"""Set the curse align.
value: left, right, bottom.
"""
self._align = value
def auto_unit(self, number, low_precision=False, min_symbol='K'):
"""Make a nice human-readable string out of number.
Number of decimal places increases as quantity approaches 1.
CASE: 613421788 RESULT: 585M low_precision: 585M
CASE: 5307033647 RESULT: 4.94G low_precision: 4.9G
CASE: 44968414685 RESULT: 41.9G low_precision: 41.9G
CASE: 838471403472 RESULT: 781G low_precision: 781G
CASE: 9683209690677 RESULT: 8.81T low_precision: 8.8T
CASE: 1073741824 RESULT: 1024M low_precision: 1024M
CASE: 1181116006 RESULT: 1.10G low_precision: 1.1G
:low_precision: returns less decimal places potentially (default is False)
sacrificing precision for more readability.
:min_symbol: Do not approache if number < min_symbol (default is K)
"""
symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
if min_symbol in symbols:
symbols = symbols[symbols.index(min_symbol) :]
prefix = {
'Y': 1208925819614629174706176,
'Z': 1180591620717411303424,
'E': 1152921504606846976,
'P': 1125899906842624,
'T': 1099511627776,
'G': 1073741824,
'M': 1048576,
'K': 1024,
}
for symbol in reversed(symbols):
value = float(number) / prefix[symbol]
if value > 1:
decimal_precision = 0
if value < 10:
decimal_precision = 2
elif value < 100:
decimal_precision = 1
if low_precision:
if symbol in 'MK':
decimal_precision = 0
else:
decimal_precision = min(1, decimal_precision)
elif symbol in 'K':
decimal_precision = 0
return '{:.{decimal}f}{symbol}'.format(value, decimal=decimal_precision, symbol=symbol)
return '{!s}'.format(number)
def trend_msg(self, trend, significant=1):
"""Return the trend message.
Do not take into account if trend < significant
"""
ret = '-'
if trend is None:
ret = ' '
elif trend > significant:
ret = '/'
elif trend < -significant:
ret = '\\'
return ret