diff --git a/.gitignore b/.gitignore index 6665c70..6920eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +plugins/custom*.py *.iml *.swp *.wpr diff --git a/conf.yaml b/conf.yaml index 7416c97..b65cc52 100644 --- a/conf.yaml +++ b/conf.yaml @@ -1,4 +1,14 @@ +# Typical plugin properties: +# - name: plugin name (always required, all other properties are optional) +# note that a plugin can be used more than once (i.e. different +# timezones in the date plugin) +# - title: title representation on the panel +# - hide_ok: false to force display value even if it is below problem threshold +# - problem: a threshold value to make a value urgent and / or display +# the value on the panel + output_format: i3 + plugins: - name: ping title: NET @@ -6,15 +16,27 @@ plugins: - de-ber-as20647.anchors.atlas.ripe.net - nl-ams-as1101.anchors.atlas.ripe.net - uk-boh-as196745.anchors.atlas.ripe.net + hide_ok: false + - name: disk partition: / problem: 80 hide_ok: false + - name: disk partition: /home problem: 90 -- name: pacman + - name: mem + - name: load + +- name: batt + - name: date format: '%a %d %H:%M' + +- name: date + title: UTC + tz: UTC + format: '%H:%M' diff --git a/plugins/__init__.py b/plugins/__init__.py index 68df0c9..262d144 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -2,30 +2,33 @@ import threading import time -def parse_config(config, defaults): - result = dict() - for key in defaults: - result[key] = config[key] if key in config else defaults[key] - return result +PLUGIN_DEFAULTS = {'freq': 1, 'hide_ok': True} class PluginThreadCommon: - def __init__(self, config, defaults=dict()): - if 'freq' not in defaults: - defaults['freq'] = 1 - if 'hide_ok' not in defaults: - defaults['hide_ok'] = True - self.conf = parse_config(config, defaults) + def __init__(self, config, defaults=None): self.status = dict() + self.conf = dict() + self.conf.update(PLUGIN_DEFAULTS) + if defaults: + self.conf.update(defaults) + self.conf.update(config) self.hide = False self.thread = threading.Thread(target=self.run) self.thread.daemon = True + def format_status(self, status, urgent=False): + if 'title' in self.conf and self.conf['title']: + full_text = '{}: {}'.format(self.conf['title'], status) + else: + full_text = status + self.status.update({'full_text': full_text, 'urgent': urgent}) + def start(self): self.thread.start() def main(self): - self.status['full_text'] = 'placeholder' + pass def run(self): while True: diff --git a/plugins/batt.py b/plugins/batt.py index 90c52ea..0876e6a 100644 --- a/plugins/batt.py +++ b/plugins/batt.py @@ -1,29 +1,31 @@ import plugins -BATTERY_DIR = '/sys/class/power_supply/BAT0/' +BATT_DEFAULTS = { + 'title': 'BAT', + 'problem': 15, + 'symbol_charging': '\u2191', + 'symbol_discharging': '\u2193' +} class PluginThread(plugins.PluginThreadCommon): def __init__(self, config): - defaults = { - 'problem': 15, - 'symbol_charging': '\u2191', - 'symbol_discharging': '\u2193' - } - super(PluginThread, self).__init__(config, defaults) + super(PluginThread, self).__init__(config, BATT_DEFAULTS) def main(self): with \ - open(BATTERY_DIR + 'capacity', 'r') as batt_capacity, \ - open(BATTERY_DIR + 'status', 'r') as batt_status: - status = batt_status.read().strip() - capacity = batt_capacity.read().strip() - if status != 'Discharging': + open('/sys/class/power_supply/BAT0/capacity', 'r') as bcap, \ + open('/sys/class/power_supply/BAT0/status', 'r') as bstat: + capacity = bcap.readline().strip() + status_value = bstat.readline().strip() + + if status_value != 'Discharging': symbol = self.conf['symbol_charging'] - self.status['urgent'] = False + urgent = False else: symbol = self.conf['symbol_discharging'] - self.status['urgent'] = float(capacity) <= self.conf['problem'] + urgent = float(capacity) <= self.conf['problem'] - self.status['full_text'] = 'BAT: ' + capacity + '% ' + symbol + status = '{}% {}'.format(capacity, symbol) + self.format_status(status, urgent) diff --git a/plugins/cmd.py b/plugins/cmd.py new file mode 100644 index 0000000..dcf29f3 --- /dev/null +++ b/plugins/cmd.py @@ -0,0 +1,23 @@ +import plugins +import subprocess + + +PACMAN_DEFAULTS = { + 'cmd': ('/usr/bin/echo', 'I am cmd'), + 'title': 'CMD', 'freq': 15 +} + + +class PluginThread(plugins.PluginThreadCommon): + def __init__(self, config): + super(PluginThread, self).__init__(config, PACMAN_DEFAULTS) + + def main(self): + proc = subprocess.Popen( + self.conf['cmd'], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, + encoding='UTF-8' + ) + out = proc.communicate()[0].strip().splitlines()[0] + + self.format_status(out) diff --git a/plugins/date.py b/plugins/date.py index ee6009a..08e8601 100644 --- a/plugins/date.py +++ b/plugins/date.py @@ -2,15 +2,19 @@ import datetime import plugins +DATE_DEFAULTS = {'format': '%c'} + + class PluginThread(plugins.PluginThreadCommon): def __init__(self, config): - defaults = {'format': '%c', 'tz': None} - super(PluginThread, self).__init__(config, defaults) + super(PluginThread, self).__init__(config, DATE_DEFAULTS) self.timezone = None - if self.conf['tz']: + if 'tz' in self.conf: import pytz self.timezone = pytz.timezone(self.conf['tz']) def main(self): - now = datetime.datetime.now(tz=self.timezone) - self.status['full_text'] = now.strftime(self.conf['format']) + now = datetime.datetime\ + .now(tz=self.timezone)\ + .strftime(self.conf['format']) + self.format_status(now) diff --git a/plugins/disk.py b/plugins/disk.py index 6ab9c2b..7797447 100644 --- a/plugins/disk.py +++ b/plugins/disk.py @@ -2,10 +2,14 @@ import plugins import psutil +DISK_DEFAULTS = {'partition': '/', 'problem': 80, 'freq': 15} + + class PluginThread(plugins.PluginThreadCommon): def __init__(self, config): - defaults = {'partition': '/', 'problem': 80, 'freq': 15} - super(PluginThread, self).__init__(config, defaults) + super(PluginThread, self).__init__(config, DISK_DEFAULTS) + if 'title' not in self.conf: + self.conf['title'] = self.conf['partition'] def main(self): du_stat = psutil.disk_usage(self.conf['partition']) @@ -15,6 +19,6 @@ class PluginThread(plugins.PluginThreadCommon): else: self.hide = True self.status['urgent'] = False - du_free = str(round(du_stat.free / 2**30, 2)) - disk_usage = self.conf['partition'] + ': ' + du_free + 'G' - self.status['full_text'] = disk_usage + + status = '{:.2f}G'.format(du_stat.free / 2**30) + self.format_status(status) diff --git a/plugins/filecat.py b/plugins/filecat.py new file mode 100644 index 0000000..0b6d82a --- /dev/null +++ b/plugins/filecat.py @@ -0,0 +1,24 @@ +import plugins + + +FILECAT_DEFAULTS = { + 'filename': '/etc/hostname', 'title': 'CAT', + 'freq': 60, 'nofile': 'unavailable' +} + + +class PluginThread(plugins.PluginThreadCommon): + def __init__(self, config): + super(PluginThread, self).__init__(config, FILECAT_DEFAULTS) + self.hide = False + + def main(self): + try: + with open(self.conf['filename'], 'r') as datafile: + contents = datafile.read().strip() + urgent = False + except FileNotFoundError: + contents = self.conf['nofile'] + urgent = True + + self.format_status(contents, urgent) diff --git a/plugins/fga.py b/plugins/fortune.py similarity index 58% rename from plugins/fga.py rename to plugins/fortune.py index ecc5857..40b3f1d 100644 --- a/plugins/fga.py +++ b/plugins/fortune.py @@ -3,27 +3,30 @@ import requests import time -URI = 'http://fucking-great-advice.ru/api/random' +FORTUNE_DEFAULTS = { + 'uri': 'http://fucking-great-advice.ru/api/random', + 'freq': 120, 'retry': 3 +} + + class PluginThread(plugins.PluginThreadCommon): def __init__(self, config): - defaults = {'freq': 120, 'retry': 3} - super(PluginThread, self).__init__(config, defaults) + super(PluginThread, self).__init__(config, FORTUNE_DEFAULTS) self.retry = False - def main(self): try: - req = requests.get(URI, timeout=2) - advice = req.json()['text'] if req.status_code == 200 else 'N/A' + req = requests.get(self.conf['uri'], timeout=2) + fortune = req.json()['text'] if req.status_code == 200 else 'N/A' self.retry = False except requests.exceptions.Timeout: - advice = 'N/A (timeout)' + fortune = 'N/A (timeout)' self.retry = True except requests.exceptions.ConnectionError: - advice = 'N/A (offline)' + fortune = 'N/A (offline)' self.retry = True - self.status['full_text'] = advice + self.format_status(fortune) def run(self): while True: diff --git a/plugins/load.py b/plugins/load.py index 8567fdd..082ff47 100644 --- a/plugins/load.py +++ b/plugins/load.py @@ -2,17 +2,21 @@ import os import plugins +LOAD_DEFAULTS = {'title': 'LOAD', 'freq': 20, 'problem': 1} + + class PluginThread(plugins.PluginThreadCommon): def __init__(self, config): - defaults = {'freq': 20, 'problem': 1} - super(PluginThread, self).__init__(config, defaults) + super(PluginThread, self).__init__(config, LOAD_DEFAULTS) def main(self): loads = os.getloadavg() if loads[0] >= self.conf['problem']: self.hide = False - self.status['urgent'] = True + urgent = True else: self.hide = True - self.status['urgent'] = False - self.status['full_text'] = 'LA: {:.2f} {:.2f} {:.2f}'.format(*loads) + urgent = False + status = '{:.2f} {:.2f} {:.2f}'.format(*loads) + + self.format_status(status, urgent) diff --git a/plugins/mem.py b/plugins/mem.py index 14c1b16..1463377 100644 --- a/plugins/mem.py +++ b/plugins/mem.py @@ -2,18 +2,21 @@ import psutil import plugins +MEM_DEFAULTS = {'title': 'RAM', 'problem': 85} + + class PluginThread(plugins.PluginThreadCommon): def __init__(self, config): - defaults = {'problem': 85} - super(PluginThread, self).__init__(config, defaults) + super(PluginThread, self).__init__(config, MEM_DEFAULTS) def main(self): mem_stat = psutil.virtual_memory() if mem_stat.percent > self.conf['problem']: self.hide = False - self.status['urgent'] = True + urgent = True else: self.hide = True - self.status['urgent'] = False - mem_available = round(mem_stat.available / 2**30, 2) - self.status['full_text'] = 'RAM: {:.2f}G'.format(mem_available) + urgent = False + + status = '{:.2f}G'.format(mem_stat.available / 2**30) + self.format_status(status, urgent) diff --git a/plugins/pacman.py b/plugins/pacman.py deleted file mode 100644 index ccf0550..0000000 --- a/plugins/pacman.py +++ /dev/null @@ -1,27 +0,0 @@ -import plugins -import subprocess - - -class PluginThread(plugins.PluginThreadCommon): - def __init__(self, config): - defaults = { - 'freq': 15, - 'problem': 10 - } - super(PluginThread, self).__init__(config, defaults) - self.format_status(0) - - def format_status(self, count): - self.hide = count == 0 - self.status['urgent'] = count >= self.conf['problem'] - self.status['full_text'] = 'UPD: ' + str(count) - - def main(self): - pacman_qu = subprocess.Popen( - ('/usr/bin/pacman', '-Qu'), stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, - encoding='UTF-8' - ) - out = pacman_qu.communicate()[0].strip().splitlines() - packages = [pkg for pkg in out if not '[ignored]' in pkg] - self.format_status(len(packages)) diff --git a/plugins/ping.py b/plugins/ping.py index 2f0bb42..983a234 100644 --- a/plugins/ping.py +++ b/plugins/ping.py @@ -1,29 +1,26 @@ import os +import subprocess import random import plugins +PING_DEFAULTS = { + 'hosts': tuple(), 'title': 'PING', 'timeout': 150 +} + + class PluginThread(plugins.PluginThreadCommon): def __init__(self, config): - defaults = {'hosts': list(), 'title': 'PING', 'timeout': 150} - super(PluginThread, self).__init__(config, defaults) - self.format_status('n/a') - - def format_status(self, state): - self.status['full_text'] = self.conf['title'] + ': ' + state - if state == 'on': - self.status['urgent'] = False - self.hide = True - else: - self.status['urgent'] = True + super(PluginThread, self).__init__(config, PING_DEFAULTS) + self.ping_cmd = ('fping', '-c1', '-qt' + str(self.conf['timeout'])) def main(self): - random.shuffle(self.conf['hosts']) - for host in self.conf['hosts']: - fping = 'fping -qc1t' + str(self.conf['timeout'])\ - + ' ' + host + ' &>/dev/null' - response = os.system(fping) - if response == 0: - self.format_status('on') - break - self.format_status('off') + host = random.choice(self.conf['hosts']) + fping = subprocess.run( + (*self.ping_cmd, host), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + if fping.returncode == 0: + self.format_status('up') + else: + self.format_status('down', True) diff --git a/vdstatus b/vdstatus index d3f5425..88b9a91 100755 --- a/vdstatus +++ b/vdstatus @@ -6,7 +6,6 @@ import os import sys import time import yaml -import plugins DEFAULT_CONFIG = os.path.join(os.environ['HOME'], '.config/vdstatus/conf.yaml') @@ -28,10 +27,10 @@ def parse_arguments(): class PluginRunner: def __init__(self, config_file=DEFAULT_CONFIG): - config = dict() + self.conf = dict() + self.conf.update(DEFAULTS) with open(config_file) as config_data: - config = yaml.load(config_data) - self.conf = plugins.parse_config(config, DEFAULTS) + self.conf.update(yaml.safe_load(config_data)) self.plugins_loaded = list() self.format_output = self.format_term for plugin in self.conf['plugins']: @@ -50,8 +49,9 @@ class PluginRunner: outputs = list() for plugin in self.plugins_loaded: if \ - not plugin.conf['hide_ok'] or \ - not plugin.hide: + 'full_text' in plugin.status and ( + not plugin.conf['hide_ok'] or not plugin.hide + ): outputs.append(plugin.status) print(self.format_output(outputs), flush=True)