diff options
Diffstat (limited to '')
30 files changed, 3790 insertions, 0 deletions
diff --git a/xpybar/config/Makefile b/xpybar/config/Makefile new file mode 100644 index 0000000..dc444aa --- /dev/null +++ b/xpybar/config/Makefile @@ -0,0 +1,8 @@ +.POSIX: + +all: xmonad-monitor + +xmonad-monitor: xmonad-monitor.gpp + gpp -s '%%' < $@.gpp > $@ + +.PHONY: all diff --git a/xpybar/config/adjbacklight b/xpybar/config/adjbacklight new file mode 100644 index 0000000..a480262 --- /dev/null +++ b/xpybar/config/adjbacklight @@ -0,0 +1,90 @@ +# -*- python -*- +import os +import sys +import time + +OUTPUT, HEIGHT, YPOS, TOP = 0, 2 * 12, 0, True + +LEFT_BUTTON = 1 +MIDDLE_BUTTON = 2 +RIGHT_BUTTON = 3 +SCROLL_UP = 4 +SCROLL_DOWN = 5 +SCROLL_LEFT = 6 +SCROLL_RIGHT = 7 +FORWARD_BUTTON = 8 +BACKWARD_BUTTON = 9 + +r, w = os.pipe() +pid = os.fork() +if not pid: + os.dup2(r, 0) + os.close(w) + os.execlp('adjbacklight', 'adjbacklight') + sys.exit(1) +os.close(r) + +def get_backlight(): + r, w = os.pipe() + pid = os.fork() + if not pid: + os.dup2(w, 1) + os.execlp('adjbacklight', 'adjbacklight', '-g') + sys.exit(1) + os.close(w) + ret = b'' + while True: + bs = os.read(r, 512) + if not bs: + break + ret += bs + os.close(r) + os.waitpid(pid, 0) + ret = float(ret.decode('utf-8', 'replace').rstrip('%\n')) / 100 + return ret + +def set_backlight(): + if 0 <= backlight <= 1: + os.write(w, b'%f%%\n' % (backlight * 100)) + time.sleep(0.0001) + +backlight = get_backlight() + +def redraw(): + x = int(bar.width * backlight + 0.5) + bar.change_colour(bar.foreground) + bar.window.fill_rectangle(bar.gc, 0, 0, x, HEIGHT) + bar.change_colour(bar.background) + bar.window.fill_rectangle(bar.gc, x, 0, bar.width - x, HEIGHT) + +import x as _x +_get_event_mask = _x.get_event_mask +def get_event_mask(): + return _get_event_mask() | Xlib.X.ButtonMotionMask +_x.get_event_mask = get_event_mask + +def unhandled_event(e): + global backlight + if isinstance(e, Xlib.protocol.event.ButtonPress): + button, x = e.detail, e.event_x + if button == LEFT_BUTTON: + backlight = min(max(0, x / bar.width), 1) + set_backlight() + bar.invalidate() + elif button == SCROLL_UP: + backlight = min(backlight + 0.05, 1) + set_backlight() + bar.invalidate() + elif button == SCROLL_DOWN: + backlight = max(backlight - 0.05, 0) + set_backlight() + bar.invalidate() + elif button in (MIDDLE_BUTTON, RIGHT_BUTTON, FORWARD_BUTTON, BACKWARD_BUTTON): + os.close(w) + os.waitpid(pid, 0) + raise KeyboardInterrupt() + elif isinstance(e, Xlib.protocol.event.MotionNotify): + if e.detail == 0: + backlight = min(max(0, e.event_x / bar.width), 1) + set_backlight() + bar.invalidate() diff --git a/xpybar/config/adjbrilliance b/xpybar/config/adjbrilliance new file mode 100644 index 0000000..0ffb09d --- /dev/null +++ b/xpybar/config/adjbrilliance @@ -0,0 +1,94 @@ +# -*- python -*- +import os +import sys +import time +import signal + +OUTPUT, HEIGHT, YPOS, TOP = 0, 2 * 12, 0, True + +LEFT_BUTTON = 1 +MIDDLE_BUTTON = 2 +RIGHT_BUTTON = 3 +SCROLL_UP = 4 +SCROLL_DOWN = 5 +SCROLL_LEFT = 6 +SCROLL_RIGHT = 7 +FORWARD_BUTTON = 8 +BACKWARD_BUTTON = 9 + +path = os.getenv('XDG_RUNTIME_DIR', '/tmp') + '/.xpybar.brilliance' +pid = None +pending = False + +def sigchld(*_): + global pid, pending + if pid is not None: + r, _ = os.waitpid(-1, 0) + if r == pid: + pid = None + if pending: + pending = False + set_brilliance() + else: + with open(path, 'wb') as file: + return file.write(('%f' % brilliance).encode('utf-8')) + +def get_brilliance(): + try: + with open(path, 'rb') as file: + return float(file.read().decode('utf-8', 'strict')) + except: + return 1 + +def set_brilliance(): + global pid, pending + if pid is None: + pid = os.fork() + if not pid: + if brilliance == 1: + os.execlp('cg-brilliance', 'cg-brilliance', '-x') + else: + os.execlp('cg-brilliance', 'cg-brilliance', '--', '%f' % brilliance) + sys.exit(1) + else: + pending = True + +signal.signal(signal.SIGCHLD, sigchld) +brilliance = get_brilliance() + +def redraw(): + x = int(bar.width * brilliance + 0.5) + bar.change_colour(bar.foreground) + bar.window.fill_rectangle(bar.gc, 0, 0, x, HEIGHT) + bar.change_colour(bar.background) + bar.window.fill_rectangle(bar.gc, x, 0, bar.width - x, HEIGHT) + +import x as _x +_get_event_mask = _x.get_event_mask +def get_event_mask(): + return _get_event_mask() | Xlib.X.ButtonMotionMask +_x.get_event_mask = get_event_mask + +def unhandled_event(e): + global brilliance + if isinstance(e, Xlib.protocol.event.ButtonPress): + button, x = e.detail, e.event_x + if button == LEFT_BUTTON: + brilliance = min(max(0, x / bar.width), 1) + set_brilliance() + bar.invalidate() + elif button == SCROLL_UP: + brilliance = min(brilliance + 0.05, 1) + set_brilliance() + bar.invalidate() + elif button == SCROLL_DOWN: + brilliance = max(brilliance - 0.05, 0) + set_brilliance() + bar.invalidate() + elif button in (MIDDLE_BUTTON, RIGHT_BUTTON, FORWARD_BUTTON, BACKWARD_BUTTON): + raise KeyboardInterrupt() + elif isinstance(e, Xlib.protocol.event.MotionNotify): + if e.detail == 0: + brilliance = min(max(0, e.event_x / bar.width), 1) + set_brilliance() + bar.invalidate() diff --git a/xpybar/config/bin/sun b/xpybar/config/bin/sun new file mode 100644 index 0000000..072542c --- /dev/null +++ b/xpybar/config/bin/sun @@ -0,0 +1,61 @@ +# -*- python -*- +import os + +# Geographical coodinates. +try: + with open(os.getenv("XDG_CONFIG_HOME") + "/geolocation", 'rb') as file: + latitude, longitude = file.read().split(b'\n')[0].decode('utf-8', 'replace').split(' ') + latitude, longitude = float(latitude), float(longitude) +except: + with open("/etc/geolocation", 'rb') as file: + latitude, longitude = file.read().split(b'\n')[0].decode('utf-8', 'replace').split(' ') + latitude, longitude = float(latitude), float(longitude) + +# The colour temperature at day and at night. +temperature_day, temperature_night = 5500, 3500 + + +# Get current time. +now = epoch() +now_m = now - 1 +now_p = now + 1 +now_d = now_p - now_m +now = epoch_to_julian_centuries(now) +now_m = epoch_to_julian_centuries(now_m) +now_p = epoch_to_julian_centuries(now_p) + +# The visibility of the Sun. +dayness = sun(latitude, longitude, now) +dayness_m = sun(latitude, longitude, now_m) +dayness_p = sun(latitude, longitude, now_p) +dayness_d = (dayness_p - dayness_m) / now_d + +# The Sun's elevation. +elevation = solar_elevation(latitude, longitude, now) +elevation_m = solar_elevation(latitude, longitude, now_m) +elevation_p = solar_elevation(latitude, longitude, now_p) +elevation_d = (elevation_p - elevation_m) / now_d + +# Calculation of the colour temperature. +temperature = temperature_day * dayness + temperature_night * (1 - dayness) +temperature_m = temperature_day * dayness_m + temperature_night * (1 - dayness_m) +temperature_p = temperature_day * dayness_p + temperature_night * (1 - dayness_p) +temperature_d = (temperature_p - temperature_m) / now_d + +# Calculate the whitepoint with adjusted colour temperature. +whitepoint = divide_by_maximum(cmf_10deg(temperature)) +whitepoint = [str(int(c * 255 + 0.5)) for c in whitepoint] +whitepoint = ';'.join(whitepoint) + +# Convert derivative to per minutes from per seconds. +temperature_d *= 60 +elevation_d *= 60 +dayness_d *= 60 + + +# Print information. +dayness *= 100 +dayness_d *= 100 +output = '\033[38;2;%sm%i\033[0mK %.1fK/m %.1f° %.2f°/m %.0f%% %.1f%%/m' +output %= (whitepoint, temperature, temperature_d, elevation, elevation_d, dayness, dayness_d) +print(output) diff --git a/xpybar/config/common.py b/xpybar/config/common.py new file mode 100644 index 0000000..fca2b73 --- /dev/null +++ b/xpybar/config/common.py @@ -0,0 +1,231 @@ +# -*- python -*- + +import os +import pwd +import sys +import time +import threading +import subprocess + +from util import * +from __main__ import Bar + +import Xlib.X, Xlib.XK, Xlib.protocol.event + +class Globals: + def __init__(self): + self.globals = None + + @property + def bar(self): + return self.globals['bar'] + @property + def display(self): + return self.globals['display'] + + @property + def OUTPUT(self): + return self.globals['OUTPUT'] + @property + def HEIGHT_PER_LINE(self): + return self.globals['HEIGHT_PER_LINE'] + @property + def YPOS(self): + return self.globals['YPOS'] + @property + def TOP(self): + return self.globals['TOP'] + @property + def HEIGHT(self): + return self.globals['HEIGHT'] + + @OUTPUT.setter + def OUTPUT(self, value): + self.globals['OUTPUT'] = value + @HEIGHT_PER_LINE.setter + def HEIGHT_PER_LINE(self, value): + self.globals['HEIGHT_PER_LINE'] = value + @YPOS.setter + def YPOS(self, value): + self.globals['YPOS'] = value + @TOP.setter + def TOP(self, value): + self.globals['TOP'] = value + @HEIGHT.setter + def HEIGHT(self, value): + self.globals['HEIGHT'] = value + +G = Globals() + +LEFT_BUTTON = 1 +MIDDLE_BUTTON = 2 +RIGHT_BUTTON = 3 +SCROLL_UP = 4 +SCROLL_DOWN = 5 +SCROLL_LEFT = 6 +SCROLL_RIGHT = 7 +FORWARD_BUTTON = 8 # X1 +BACKWARD_BUTTON = 9 # X2 + +limited = lambda v : min(max(int(v + 0.5), 0), 100) + +def t_(f): + try: + f() + except Exception as e: + time.sleep(1) + +t = lambda f : (lambda : t_(f)) + +def pdeath(signal, *command): + try: + path = os.environ['PATH'].split(':') + for p in path: + p += '/pdeath' + if os.access(p, os.X_OK, effective_ids = True): + return (p, signal, *command) + except: + pass + return command + +HOME = pwd.getpwuid(os.getuid()).pw_dir + +SEPARATOR = ' │ ' + +def invalidate(): + try: + G.bar.invalidate() + except: + pass + +mqueue_map = {} + +class Entry: + def __init__(self, wrap = None, left = True, line = 0, prev = None): + self.left = left + self.line = line + self.prev = prev + self.width = 0 + self.offset = 0 + self.last_text = None + self.short_layout = None + self.wrapped = self.function if wrap is None else wrap(self.function) + + def invalidate(self): + wrapper = self.wrapped + if isinstance(wrapper, Clocked): + wrapper = wrapper.sometimes + if isinstance(wrapper, Sometimes): + wrapper.counter = 0 + invalidate() + + def action(self, col, button, x, y): + pass + + def click(self, row, lcol, rcol, button, x, y): + if row == self.line: + col = (lcol if self.left else rcol) - self.offset + if 0 <= col < self.width: + if self.left: + x -= self.offset * G.bar.font_width + else: + col = self.width - 1 - col + x = G.bar.width - x + x -= self.offset * G.bar.font_width + x = self.width * G.bar.font_width - x + y -= self.line * G.HEIGHT_PER_LINE + try: + self.action(col, button, x, y) + except Exception as err: + print(str(err), file = sys.stderr, flush = True) + return True + return False + + def function(self): + return SEPARATOR + + def __call__(self, *args, **kwargs): + try: + text = self.wrapped(*args, **kwargs) + if text is not self.last_text: + self.width = Bar.coloured_length(text) + self.last_text = text + return text + except Exception as err: + print(err, file = sys.stderr, flush = True) + + def update_position(self): + if self.prev is not None: + self.prev.update_position() + self.offset = self.prev.offset + self.prev.width + +class Group: + def __init__(self, functions): + self.pattern = '' + self.posupdate = [] + left = None + right = None + lineno = 0 + atleft = True + newline = False + lastright = None + new_functions = [] + separator = None + for entry in functions: + if entry is None: + newline = not atleft + lineno += 1 if newline else 0 + atleft = not atleft + continue + entry.left = atleft + entry.line = lineno + if newline: + self.pattern += '\n' + newline = False + if left is not None: + self.posupdate.append(left) + if right is not None: + self.posupdate.append(right) + left = None + right = None + lastright = None + if entry.left: + if left is not None: + separator = Entry(left = True) + new_functions.append(separator) + self.pattern += '%s' + separator.prev = left + left = separator + entry.prev = left + left = entry + elif right is None: + if left is not None: + self.pattern += '\0 ' + right = entry + lastright = entry + else: + separator = Entry(left = False) + new_functions.append(separator) + self.pattern += '%s' + lastright.prev = separator + separator.prev = entry + lastright = entry + new_functions.append(entry) + self.pattern += '%s' + if left is not None: + self.posupdate.append(left) + if right is not None: + self.posupdate.append(right) + height = (lineno + 1) * G.HEIGHT_PER_LINE + if G.HEIGHT < height: + G.HEIGHT = height + self.functions = new_functions + +def colour_interpol(low, high, weight): + rl, rh = (low >> 16) & 255, (high >> 16) & 255 + gl, gh = (low >> 8) & 255, (high >> 8) & 255 + bl, bh = (low >> 0) & 255, (high >> 0) & 255 + r = int(rl * (1 - weight) + rh * weight) + g = int(gl * (1 - weight) + gh * weight) + b = int(bl * (1 - weight) + bh * weight) + return '38;2;%i;%i;%i' % (r, g, b) diff --git a/xpybar/config/myalsa.py b/xpybar/config/myalsa.py new file mode 100644 index 0000000..14b8a7b --- /dev/null +++ b/xpybar/config/myalsa.py @@ -0,0 +1,260 @@ +# -*- python -*- +from plugins.alsa import ALSA + +from common import * + +class MyALSA(Entry): + def __init__(self, *args, timeout = None, sleep = None, cards = -1, mixers = None, colours = {}, **kwargs): ## TODO support multiple cards and all mixers + self.colours = colours if colours is not None else {} + if timeout is not None: + self.timeout = timeout + else: + self.timeout = 5 + if sleep is not None: + self.sleep = sleep + else: + self.sleep = 5 + if mixers is None: + mixers = ('Master', 'PCM') + elif isinstance(mixers, str): + mixers = [mixers] + else: + mixers = mixers + if cards is None: + cards = [-1] + elif isinstance(cards, str) or isinstance(cards, int): + cards = [cards] + else: + cards = cards + self.alsa = [] + for card in cards: + for mixer in mixers: ## TODO support by name (and 'default') + try: + if isinstance(mixer, tuple) or isinstance(mixer, list): + self.alsa.append([ALSA(card, m) for m in mixer]) + else: + self.alsa.append(ALSA(card, mixer)) + except: + pass + if True: + method = 3 + try: + path = os.environ['PATH'].split(':') + for p in path: + p += '/bus' + if os.access(p, os.F_OK | os.X_OK, effective_ids = True): + method = 0 + break + except: + pass + else: + try: + import posix_ipc + method = 1 + except: + method = 3 + try: + path = os.environ['PATH'].split(':') + for p in path: + p += '/cmdipc' + if os.access(p, os.F_OK | os.X_OK, effective_ids = True): + method = 2 + break + except: + pass + self.sep_width = Bar.coloured_length(SEPARATOR) + self.get_volume() + self.broadcast_update = None + if method == 0: + self.create_broadcast_update_bus() + Entry.__init__(self, *args, **kwargs) + xasync((self.refresh_bus, self.refresh_posix_ipc, self.refresh_cmdipc, self.refresh_wait)[method], name = 'alsa') + + def action(self, col, button, x, y): + mixer = 0 + mixer_text = self.text.split(SEPARATOR) + while mixer < len(mixer_text): ## the limit is just a precaution + mixer_width = Bar.coloured_length(mixer_text[mixer]) + if col >= mixer_width: + col -= mixer_width + if col < self.sep_width: + return + col -= self.sep_width + else: + break + mixer += 1 + mixer_text = mixer_text[mixer] + + channel = -1 + mixer_text = mixer_text.split(': ') + mixer_label = ': '.join(mixer_text[:-1]) + ': ' + mixer_text = mixer_text[-1].split(' ') + mixer_label = Bar.coloured_length(mixer_label) + if col >= mixer_label: + col -= mixer_label + while channel < len(mixer_text): ## the limit is just a precaution + channel += 1 + channel_width = Bar.coloured_length(mixer_text[channel]) + if col < channel_width: + break + col -= channel_width + if col < 1: + channel = -1 + break + col -= 1 + + mixer = self.alsa[mixer] + if isinstance(mixer, list): + if button == LEFT_BUTTON: + self.switch_exclusive(mixer) + elif button == LEFT_BUTTON: + muted = mixer.get_mute() + muted = not (any(muted) if channel == -1 else muted[channel]) + mixer.set_mute(muted, channel) + elif button == RIGHT_BUTTON: + volume = mixer.get_volume() + volume = limited(sum(volume) / len(volume)) + mixer.set_volume(volume, -1) + elif button in (SCROLL_UP, SCROLL_DOWN): + volume = mixer.get_volume() + adj = -5 if button == SCROLL_DOWN else 5 + if channel < 0: + for ch in range(len(volume)): + mixer.set_volume(limited(volume[ch] + adj), ch) + else: + mixer.set_volume(limited(volume[channel] + adj), channel) + else: + return + self.get_volume() + self.invalidate() + if self.broadcast_update is not None: + self.broadcast_update() + + def switch_exclusive(self, mixers): + def f(status): + if all(status): + return True + elif any(status): + return None + else: + return False + muted = [f(m.get_mute()) for m in mixers] + count = sum((0 if m else 1) for m in muted) + if None in muted or count != 1: + index = 0 + else: + [index] = [i for i, m in enumerate(muted) if not m] + index = (index + 1) % len(mixers) + for m in mixers: + m.set_mute(True) + mixers[index].set_mute(False) + + def get_exclusive(self, mixers): + def f(status): + if all(status): + return True + elif any(status): + return None + else: + return False + muted = [f(m.get_mute()) for m in mixers] + count = sum((0 if m else 1) for m in muted) + if None in muted or count != 1: + index = 0 + for m in mixers: + m.set_mute(True) + mixers[index].set_mute(False) + else: + [index] = [i for i, m in enumerate(muted) if not m] + name = mixers[index].mixername + if name in self.colours: + name = '\033[%sm%s\033[0m' % (self.colours[name], name) + return name + + def get_volume(self): + text_v = lambda v : '--%' if v is None else ('%2i%%' % v)[:3] + read_m = lambda m : '%s: %s' % (m.mixername, ' '.join(text_v(v) for v in m.get_volume())) + text = SEPARATOR.join((self.get_exclusive(m) if isinstance(m, list) else read_m(m)) for m in self.alsa) + self.text = text + + def refresh_bus(self): + if 'BUS_AUDIO' in os.environ: + bus = os.environ['BUS_AUDIO'] + else: + bus = os.environ['XDG_RUNTIME_DIR'] + '/@bus/audio' + wait = pdeath('HUP', 'bus', 'wait', bus, 'true') + while True: + try: + spawn_read(*wait) + self.get_volume() + except: + time.sleep(self.sleep) + + def create_broadcast_update_bus(self): + if 'BUS_AUDIO' in os.environ: + bus = os.environ['BUS_AUDIO'] + else: + bus = os.environ['XDG_RUNTIME_DIR'] + '/@bus/audio' + self.bus_broadcast = pdeath('HUP', 'bus', 'broadcast', bus, '%i volume' % os.getpid()) + self.broadcast_update = lambda : spawn_read(*(self.bus_broadcast)) + + def refresh_posix_ipc(self): + import posix_ipc + s = posix_ipc.Semaphore('/.xpybar.alsa.0', posix_ipc.O_CREAT, 0o600, 1) + c = posix_ipc.Semaphore('/.xpybar.alsa.1', posix_ipc.O_CREAT, 0o600, 0) + q = posix_ipc.Semaphore('/.xpybar.alsa.2', posix_ipc.O_CREAT, 0o600, 0) + try: + s.acquire(self.timeout) + while True: + failed = False + try: + c.release() + try: + s.release() + try: + q.acquire(timeout) + except posix_ipc.BusyError: + sys.exit(1) + pass + finally: + s.acquire(self.timeout) + finally: + c.acquire(self.timeout) + except: + sys.exit(1) + failed = True + try: + self.get_volume() + except: + sys.exit(1) + failed = True + if failed: + time.sleep(self.sleep) + finally: + s.release() + + def refresh_cmdipc(self): + enter = pdeath('HUP', 'cmdipc', '-PCck', '/.xpybar.alsa.0/.xpybar.alsa.1/.xpybar.alsa.2', 'enter', '-b%i' % self.timeout) + wait = pdeath('HUP', 'cmdipc', '-PCk', '/.xpybar.alsa.0/.xpybar.alsa.1/.xpybar.alsa.2', 'wait', '-b%i' % self.timeout) + leave = pdeath('HUP', 'cmdipc', '-PCk', '/.xpybar.alsa.0/.xpybar.alsa.1/.xpybar.alsa.2', 'leave') + try: + spawn_read(*enter) + while True: + try: + spawn_read(*wait) + self.get_volume() + except: + time.sleep(self.sleep) + finally: + spawn_read(*leave) + + def refresh_wait(self): + while True: + try: + time.sleep(self.sleep) + self.get_volume() + except: + pass + + def function(self): + return self.text diff --git a/xpybar/config/mybacklight.py b/xpybar/config/mybacklight.py new file mode 100644 index 0000000..bcb3d53 --- /dev/null +++ b/xpybar/config/mybacklight.py @@ -0,0 +1,76 @@ +# -*- python -*- +import os +import sys +from common import * + +class MyBacklight(Entry): + def __init__(self, *args, **kwargs): + self.show_label = True + self.backlight = self.get_backlight() + self.counter = 0 + Entry.__init__(self, *args, **kwargs) + + def get_backlight(self): + r, w = os.pipe() + pid = os.fork() + if not pid: + os.dup2(w, 1) + os.execlp('adjbacklight', 'adjbacklight', '-g') + sys.exit(1) + os.close(w) + ret = b'' + while True: + bs = os.read(r, 512) + if not bs: + break + ret += bs + os.close(r) + os.waitpid(pid, 0) + ret = float(ret.decode('utf-8', 'replace').rstrip('%\n')) / 100 + return ret + + def set_backlight(self): + pid = os.fork() + if not pid: + os.execlp('adjbacklight', 'adjbacklight', '-s', '%f%%' % (self.backlight * 100)) + sys.exit(1) + os.waitpid(pid, 0) + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + pid = os.fork() + if not pid: + pid = os.fork() + if not pid: + os.execlp('xpybar', 'xpybar', '-c', '%s/.config/xpybar/adjbacklight' % HOME) + sys.exit(1) + sys.exit(0) + os.waitpid(pid, 0) + elif button == MIDDLE_BUTTON: + self.show_label = not self.show_label + self.invalidate() + elif button == RIGHT_BUTTON: + self.backlight = self.get_backlight() + self.invalidate() + elif button == SCROLL_UP: + self.backlight = min(self.backlight + 0.05, 1) + self.set_backlight() + self.invalidate() + elif button == SCROLL_DOWN: + self.backlight = max(self.backlight - 0.05, 0) + self.set_backlight() + self.invalidate() + + def function(self): + self.counter += 1 + if self.counter == 6: + self.counter = 0 + self.backlight = self.get_backlight() + val = int(self.backlight * 100 + 0.5) + if val >= 100: + val = str(val) + else: + val = '%2i%%' % val + if self.show_label: + val = 'Blight: ' + val + return val diff --git a/xpybar/config/mybattery.py b/xpybar/config/mybattery.py new file mode 100644 index 0000000..3a8e167 --- /dev/null +++ b/xpybar/config/mybattery.py @@ -0,0 +1,163 @@ +# -*- python -*- +from plugins.powersupply import PowerSupply + +from common import * + +class MyBattery(Entry): + COMPACT = 0 + NORMAL = 1 + DETAILED = 2 + + def __init__(self, *args, show_all = False, show_battery = 0, details = 1, batteries = None, **kwargs): + self.show_all = show_all + self.show_battery = show_battery + self.supply_filter = batteries + self.details = details + self.supplies = None + self.batteries = None + self.reload() + Entry.__init__(self, *args, **kwargs) + + def reload(self): + name = None + newsupplies = {} + newbatteries = [] + if self.batteries is not None: + if isinstance(self.batteries, int): + name = self.batteries[self.show_battery] + else: + name = self.show_battery + for supply in PowerSupply.supplies(): + if self.supply_filter is not None: + if supply not in self.supply_filter: + continue + supply = PowerSupply(supply) + if supply.type != 'Battery': + continue + newsupplies[supply.name] = supply + newbatteries.append(supply.name) + if self.supply_filter is not None: + newbatteries = [b for b in self.supply_filter if b in newsupplies] + if name is None: + index = 0 + elif isinstance(name, int): + index = name % len(newbatteries) if len(newbatteries) > 0 else 0 + elif name in self.batteries: + index = self.batteries.index(index) + else: + index = 0 + self.supplies, self.batteries, self.show_battery = newsupplies, newbatteries, index + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + self.show_all = not self.show_all + self.invalidate() + elif button == MIDDLE_BUTTON: + self.details += 1 + self.details %= 3 + self.invalidate() + elif button == RIGHT_BUTTON: + self.reload() + self.invalidate() + elif button == SCROLL_UP: + if len(self.batteries) > 0: + self.show_battery += 1 + self.show_battery %= len(self.batteries) + self.invalidate() + elif button == SCROLL_DOWN: + if len(self.batteries) > 0: + self.show_battery -= 1 + if self.show_battery < 0: + self.show_battery += len(self.batteries) + self.invalidate() + + def colourise(self, text): + try: + value = text.replace(',', '.') + value = float(value) if '.' in value else int(value) + colour = '39' + if value < 80: colour = '32' + if value < 50: colour = '33' + if value < 15: colour = '31' + if value < 5: colour = '7;31' + return '\033[%sm%s\033[0m' % (colour, text) + except: + return text + + def battery(self, bat): + bat = self.supplies[bat] + name = bat.name[:1].upper() + bat.name[1:].lower() + if bat.is_online(): + try: + charge = (bat.get_charge(), bat.get_charge_full(), bat.charge_full_design) + current = bat.get_current() + status = bat.get_status().lower() + left = None + if charge[0] == charge[1]: + capacity = '100' + else: + capacity = '%.1f' % (charge[0] / charge[1] * 100) + capacity = self.colourise(capacity) + + if current > 0: + if status == 'charging': + left = (charge[1] - charge[0]) / current + elif status == 'discharging': + left = charge[0] / current + if left is not None: + hours = int(left) + minutes = int((left - hours) * 60 + 0.5) + if minutes >= 60: + minutes -= 60 + hours += 1 + left = '%ih%02im until %s' % (hours, minutes, status.replace('ing', 'ed')) + + if self.details == MyBattery.COMPACT: + if status == 'full': + return 'full' + elif status == 'charging': + status = '+' + elif status == 'discharging': + status = '-' + else: + status = '?' + if left is None: + return '%s%%%s' % (capacity, status) + else: + return '%s%%%s %s' % (capacity, status, left) + elif self.details == MyBattery.NORMAL: + if left is None: + return '%s: %s%% %s' % (name, capacity, status) + else: + return '%s: %s%% %s %s' % (name, capacity, status, left) + else: + if charge[1] == charge[2]: + health = '100' + else: + health = '%.1f' % (charge[1] / charge[2] * 100) + health = self.colourise(health) + volt = bat.get_voltage() / bat.voltage_min_design + if volt < 1: + volt = '\033[31m%.1f\033[0m' % volt + elif volt < 1.05: + volt = '\033[33m%.1f\033[0m' % volt + else: + volt = '%.1f' % volt + if left is None: + return '%s: %s%% %s, %s%% healthy, %sx voltage' % (name, capacity, status, health, volt) + else: + return '%s: %s%% %s %s, %s%% healthy, %sx voltage' % (name, capacity, status, left, health, volt) + except Exception as e: + return (repr(e)) + return ('%s: ???' % name) if self.details > 0 else '???' + else: + return ('%s: offline' % name) if self.details > 0 else 'offline' + + def function(self): + if len(self.batteries) == 0: + return 'no batteries available' + + if self.show_all: + return SEPARATOR.join(self.battery(bat) for bat in self.batteries) + else: + return self.battery(self.batteries[self.show_battery]) diff --git a/xpybar/config/mybrilliance.py b/xpybar/config/mybrilliance.py new file mode 100644 index 0000000..f7647e7 --- /dev/null +++ b/xpybar/config/mybrilliance.py @@ -0,0 +1,70 @@ +# -*- python -*- +import os +import sys +from common import * + +class MyBrilliance(Entry): + def __init__(self, *args, **kwargs): + self.show_label = True + self.brilliance = self.get_brilliance() + self.counter = 0 + self.path = os.getenv('XDG_RUNTIME_DIR', '/tmp') + '/.xpybar.brilliance' + Entry.__init__(self, *args, **kwargs) + + def get_brilliance(self): + try: + with open(self.path, 'rb') as file: + return float(file.read().decode('utf-8', 'strict')) + except: + return 1 + + def set_brilliance(self): + pid = os.fork() + if not pid: + if self.brilliance == 1: + os.execlp('cg-brilliance', 'cg-brilliance', '-x') + else: + os.execlp('cg-brilliance', 'cg-brilliance', '--', '%f' % self.brilliance) + sys.exit(1) + os.waitpid(pid, 0) + with open(self.path, 'wb') as file: + return file.write(('%f' % self.brilliance).encode('utf-8')) + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + pid = os.fork() + if not pid: + pid = os.fork() + if not pid: + os.execlp('xpybar', 'xpybar', '-c', '%s/.config/xpybar/adjbrilliance' % HOME) + sys.exit(1) + sys.exit(0) + os.waitpid(pid, 0) + elif button == MIDDLE_BUTTON: + self.show_label = not self.show_label + self.invalidate() + elif button == RIGHT_BUTTON: + self.brilliance = self.get_brilliance() + self.invalidate() + elif button == SCROLL_UP: + self.brilliance = min(self.brilliance + 0.05, 1) + self.set_brilliance() + self.invalidate() + elif button == SCROLL_DOWN: + self.brilliance = max(self.brilliance - 0.05, 0) + self.set_brilliance() + self.invalidate() + + def function(self): + self.counter += 1 + if self.counter == 6: + self.counter = 0 + self.brilliance = self.get_brilliance() + val = int(self.brilliance * 100 + 0.5) + if val >= 100: + val = str(val) + else: + val = '%2i%%' % val + if self.show_label: + val = 'Brill: ' + val + return val diff --git a/xpybar/config/myclock.py b/xpybar/config/myclock.py new file mode 100644 index 0000000..f97c8ea --- /dev/null +++ b/xpybar/config/myclock.py @@ -0,0 +1,106 @@ +# -*- python -*- +from plugins.tzclock import TZClock + +from common import * + +class MyClock(Entry): + def __init__(self, *args, format = '%Y-%m-%d %T', long_format = '%Y-(%m)%b-%d %T, %a w%V, %Z', tzs = None, **kwargs): + self.default_format = format + self.long_format = long_format + self.clock = G.clock + self.set_format(self.clock, self.default_format) + self.tzs = [] if tzs is None else tzs + self.tzi = 0 + self.twilight = '39' + mqueue_map['timefmt'] = self.mqueue + mqueue_map['tz'] = self.mqueue + Entry.__init__(self, *args, **kwargs) + + def mqueue(self, args): + if args[0] == 'timefmt': + args = args[1:] + if len(args) == 0: + self.action(0, RIGHT_BUTTON, 0, 0) + else: + self.set_format(self.clock, ' '.join(args)) + self.invalidate() + if args[0] == 'tz': + args = args[1:] + if len(args) == 0: + self.action(0, LEFT_BUTTON, 0, 0) + else: + old_clock = self.clock + tz = '/'.join(args) + try: + tzclock = TZClock(timezone = tz, format = self.clock.u_format) + except: + try: + tzclock = TZClock(timezone = tz.upper(), format = self.clock.u_format) + except: + print('%s: unknown typezone: %s' % (sys.argv[0], tz), file = sys.stderr, flush = True) + return + tzclock.u_format = tzclock.format + self.clock = tzclock + if old_clock is not G.clock: + del old_clock + self.invalidate() + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + if self.clock is G.clock: + G.clock.utc = not G.clock.utc + self.set_format(G.clock, self.clock.u_format) + else: + self.set_format(G.clock, self.clock.u_format) + self.clock = G.clock + self.invalidate() + elif button == RIGHT_BUTTON: + if self.clock.u_format == self.default_format: + self.set_format(self.clock, self.long_format) + else: + self.set_format(self.clock, self.default_format) + self.invalidate() + elif button in (SCROLL_UP, SCROLL_DOWN): + i = self.tzi + (+1 if button == SCROLL_UP else -1) + if not (0 <= i <= len(self.tzs)): + return + self.tzi = i + old_clock = self.clock + if i > 0: + tzclock = TZClock(timezone = self.tzs[i - 1], format = self.clock.u_format) + tzclock.u_format = tzclock.format + self.clock = tzclock + else: + self.clock = G.clock + if old_clock is not G.clock: + del old_clock + self.invalidate() + + def set_format(self, clck, format): + clck.u_format = format + if not (clck is G.clock and clck.utc): + clck.format = format + else: + fmt, esc = '', False + for c in format: + if esc: + fmt += c + if c == '%' or 'a' <= c.lower() <= 'z': + esc = False + if c == 'Z': + while fmt[-1] != '%': + fmt = fmt[:-1] + fmt = fmt[:-1] + 'UTC' + elif c == '%': + esc = True + fmt += c + else: + fmt += c + clck.format = fmt + + def function(self): + clck = self.clock + colour = self.twilight + if clck is not G.clock or clck.utc: + colour += ';7' + return '\033[%sm%s\033[00m' % (colour, clck.read()) diff --git a/xpybar/config/mycomputer.py b/xpybar/config/mycomputer.py new file mode 100644 index 0000000..bbfd539 --- /dev/null +++ b/xpybar/config/mycomputer.py @@ -0,0 +1,160 @@ +# -*- python -*- +from plugins.pacman import Pacman +from plugins.uname import Uname +from plugins.users import Users +from plugins.xdisplay import XDisplay +from plugins.xkb import XKB + +from common import * + +class MyComputer(Entry): + def __init__(self, *args, locks = None, **kwargs): + self.have_linux_libre, self.have_pacman = True, None + self.linux_installed, self.linux_latest = None, None + self.last_uname_read = '' + self.last_users_read = '' + self.xdisplay = None + self.you = pwd.getpwuid(os.getuid()).pw_name + self.display = 0 + self.show_detailed = False + self.keys = [] + if locks is None: + locks = ['num', 'cap'] + self.num_lock = 'num' in locks or 'number' in locks or 'numerical' in locks or 'numpad' in locks + self.cap_lock = 'cap' in locks or 'caps' in locks + self.scr_lock = 'scr' in locks or 'scroll' in locks or 'scrl' in locks + if self.num_lock: + self.keys.append(Xlib.XK.XK_Num_Lock) + if self.cap_lock: + self.keys.append(Xlib.XK.XK_Caps_Lock) + if self.scr_lock: + self.keys.append(Xlib.XK.XK_Scroll_Lock) + Entry.__init__(self, *args, **kwargs) + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + if self.display == 3: + col -= 5 + if col % 4 == 3: + return + col //= 4 + key = self.keys[col] + G.display.xtest_fake_input(Xlib.X.KeyPress, G.display.keysym_to_keycode(key)) + G.display.xtest_fake_input(Xlib.X.KeyRelease, G.display.keysym_to_keycode(key)) + G.display.flush() + elif button == MIDDLE_BUTTON: + self.show_detailed = not self.show_detailed + self.invalidate() + elif button == RIGHT_BUTTON: + if self.display == 0: + xasync(lambda : subprocess.Popen(['mate-system-monitor']).wait()) + else: + self.invalidate() + elif button == SCROLL_UP: + self.display = min(self.display + 1, 2) ## TODO the keyboard support is not too good + self.invalidate() + elif button == SCROLL_DOWN: + self.display = max(self.display - 1, 0) + self.invalidate() + + def function_uname(self): + uname = Uname() + nodename = uname.nodename + kernel_release = uname.kernel_release + operating_system = uname.operating_system + + lts = '-lts' if kernel_release.lower().endswith('-lts') else '' + if self.have_pacman is None: + self.have_pacman = True + try: + self.linux_installed = Pacman('linux-libre' + lts, True) + except: + self.have_linux_libre = False + try: + self.linux_installed = Pacman('linux' + lts, True) + except: + self.have_pacman = False + if self.have_pacman: + try: + self.linux_latest = Pacman(('linux-libre' if self.have_linux_libre else 'linux') + lts, False) + except: + self.have_pacman = None + elif self.have_pacman: + try: + self.linux_installed = Pacman(('linux-libre' if self.have_linux_libre else 'linux') + lts, True) + self.linux_latest = Pacman(('linux-libre' if self.have_linux_libre else 'linux') + lts, False) + except: + self.have_pacman = None + + if (self.have_pacman is not None) and self.have_pacman: + linux_running = kernel_release.split('-') + linux_running, kernel_release = linux_running[:2], linux_running[2:] + linux_running = '-'.join(linux_running) + kernel_release = '-' + '-'.join(kernel_release) + self.linux_installed = self.linux_installed.version + self.linux_latest = self.linux_latest.version + if self.linux_installed == self.linux_latest: + if linux_running == self.linux_installed: + linux_running = '\033[32m%s\033[39m' % linux_running + else: + if linux_running == self.linux_installed: + linux_running = '\033[33m%s\033[39m' % linux_running + else: + linux_running = '\033[31m%s\033[39m' % linux_running + kernel_release = linux_running + kernel_release + + rc = '%s %s %s' + rc %= (nodename, kernel_release, operating_system) + self.last_uname_read = rc + return rc + + def function_users(self): ## TODO use inotify + users = Users('devfs').users + def colour_user(user): + if user == 'root': return '\033[31m%s\033[39m' + elif not user == self.you: return '\033[33m%s\033[39m' + else: return '%s' + users = ['%s{%i}' % (colour_user(u) % u, len(users[u])) for u in users.keys()] + users = 'Usr: %s' % (' '.join(users)) + self.last_users_read = users + return users + + def function_xdisplay(self): + if self.xdisplay is None: + x = XDisplay() + if x.connection is None: + self.xdisplay = 'No DISPLAY environment variable set' + self.xdisplay_detailed = self.xdisplay + else: + self.xdisplay = 'X: %s' % x.connection + self.xdisplay_detailed = 'Host: %s' % ('(localhost)' if x.host is None else x.host) + self.xdisplay_detailed += SEPARATOR + self.xdisplay_detailed += 'Display: %s' % (x.display) + if x.screen is not None: + self.xdisplay_detailed += SEPARATOR + self.xdisplay_detailed += 'Screen: %s' % (x.screen) + if x.vt is not None: + self.xdisplay += '%sVt: %i' % (SEPARATOR, x.vt) + self.xdisplay_detailed += '%sVt: %i' % (SEPARATOR, x.vt) + return self.xdisplay_detailed if self.show_detailed else self.xdisplay + + def function_xkb(self): ## TODO add update listener + xkb = XKB().get_locks() + kbd = 'Kbd:' + if self.num_lock: + kbd += ' \033[3%imnum\033[0m' % (1 if (xkb & XKB.NUM) == 0 else 2) + if self.cap_lock: + kbd += ' \033[3%imcap\033[0m' % (1 if (xkb & XKB.CAPS) == 0 else 2) + if self.scr_lock: + kbd += ' \033[3%imscr\033[0m' % (1 if (xkb & XKB.SCROLL) == 0 else 2) + return kbd + + def function(self): + if self.display == 0: + return self.function_uname() + elif self.display == 1: + return self.function_users() + elif self.display == 2: + return self.function_xdisplay() + elif self.display == 3: + return self.function_xkb() diff --git a/xpybar/config/mycpu.py b/xpybar/config/mycpu.py new file mode 100644 index 0000000..af2c78d --- /dev/null +++ b/xpybar/config/mycpu.py @@ -0,0 +1,214 @@ +# -*- python -*- +from plugins.cpu import CPU +from plugins.cpuonline import CPUOnline +from plugins.softirqs import SoftIRQs + +from common import * + +class MyCPU(Entry): + def __init__(self, *args, **kwargs): + self.last_sirq_split = SoftIRQs() + cpu = CPU() + self.last_time = time.monotonic() + self.last_cpu_stat = cpu.cpu + self.last_cpu_total = sum(cpu.cpu) + self.last_cpus_total = [sum(c) for c in cpu.cpus] + self.last_cpus_stat = [[c[s] for c in cpu.cpus] for s in range(len(cpu.cpu))] + self.last_intr = cpu.intr_total + self.last_ctxt = cpu.ctxt + self.last_fork = cpu.processes + self.last_sirq = cpu.softirq_total + self.none = self.colourise(None) + self.coloured = tuple(self.colourise(i) for i in range(101)) + self.show_all = True + self.show_function = 0 + self.functions = [ lambda *v : self.function_cpu(CPU.idle, '', *v) + , self.function_intr + , self.function_ctxt + , self.function_fork + , self.function_proc + , self.function_sirq + ] + for key in self.last_sirq_split.keys: + make = lambda key : (lambda *v : self.function_sirq_split(key, *v)) + self.functions.append(make(key)) + self.functions += [ lambda *v : self.function_cpu(CPU.user, '(user)', *v) + , lambda *v : self.function_cpu(CPU.nice, '(nice)', *v) + , lambda *v : self.function_cpu(CPU.system, '(system)', *v) + , lambda *v : self.function_cpu(CPU.iowait, '(iowait)', *v) + , lambda *v : self.function_cpu(CPU.irq, '(irq)', *v) + , lambda *v : self.function_cpu(CPU.softirq, '(softirq)', *v) + , lambda *v : self.function_cpu(CPU.steal, '(steal)', *v) + , lambda *v : self.function_cpu(CPU.guest, '(guest)', *v) + , lambda *v : self.function_cpu(CPU.guest_nice, '(guest nice)', *v) + , self.function_load + , self.function_task + , self.function_pid + , self.function_online + ] + Entry.__init__(self, *args, **kwargs) + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + self.show_all = not self.show_all + self.invalidate() + elif button == SCROLL_UP: + n = self.show_function + 1 + if n < len(self.functions): + self.show_function = n + self.invalidate() + elif button == SCROLL_DOWN: + n = self.show_function - 1 + if n >= 0: + self.show_function = n + self.invalidate() + + def usage(self, now_stat, now_total, last_stat, last_total, idle): + total = now_total - last_total + stat = now_stat - last_stat + return None if total == 0 else (total - stat if idle else stat) * 100 / total + + def colourise_(self, value): + if value is None: + return self.none + else: + return self.coloured[limited(value)] + + def colourise(self, value): + if value is None: + return '--%' + elif value >= 100: + return '\033[31m100\033[0m' + colour = '39' + if value >= 5: colour = '32' + if value >= 50: colour = '33' + if value >= 90: colour = '31' + return '\033[%sm%2i\033[0m%%' % (colour, value) + + def function_cpu(self, stat, name, cpu, display, tdiff): + now_cpu_stat = self.now_cpu_stat[stat] + now_cpus_stat = self.now_cpus_stat[stat] + last_cpus_stat = self.last_cpus_stat[stat] + last_cpu_stat = self.last_cpu_stat[stat] + + if display: + cpu = (now_cpu_stat, self.now_cpu_total, last_cpu_stat, self.last_cpu_total) + cpu = self.colourise_(self.usage(*cpu, idle = stat == CPU.idle)) + if self.show_all: + cpus = (now_cpus_stat, self.now_cpus_total, last_cpus_stat, self.last_cpus_total) + cpus = ' '.join(self.colourise_(self.usage(*c, idle = stat == CPU.idle)) for c in zip(*cpus)) + cpu = 'Cpu%s: %s : %s' % (name, cpus, cpu) + else: + cpu = 'Cpu%s: %s' % (name, cpu) + + return cpu + + def function_intr(self, cpu, display, tdiff): + now_intr = cpu.intr_total + if display: + cpu = 'Intr: %.0fHz' % ((now_intr - self.last_intr) / tdiff) + self.last_intr = now_intr + return cpu + + def function_ctxt(self, cpu, display, tdiff): + now_ctxt = cpu.ctxt + if display: + cpu = 'Ctxt: %.0fHz' % ((now_ctxt - self.last_ctxt) / tdiff) + self.last_ctxt = now_ctxt + return cpu + + def function_fork(self, cpu, display, tdiff): + now_fork = cpu.processes + if display: + cpu = 'Fork: %.0fHz' % ((now_fork - self.last_fork) / tdiff) + self.last_fork = now_fork + return cpu + + def function_sirq(self, cpu, display, tdiff): + now_sirq = cpu.softirq_total + if display: + cpu = 'Sirq: %.0fHz' % ((now_sirq - self.last_sirq) / tdiff) + self.last_sirq = now_sirq + return cpu + + def function_proc(self, cpu, display, tdiff): + if display: + cpu = 'Proc: %irun %iio' % (cpu.procs_running, cpu.procs_blocked) + return cpu + + def function_load(self, cpu, display, tdiff): + if display: + load = AverageLoad() + cpu = 'Load: %.2f %.2f %.2f' % (load.total_avg_5_min, load.total_avg_10_min, load.total_avg_15_min) + return cpu + + def function_task(self, cpu, display, tdiff): + if display: + load = AverageLoad() + cpu = 'Task: %i/%i (%.0f%%)' % (load.active_tasks, load.total_tasks, load.active_tasks * 100 / load.total_tasks) + return cpu + + def function_pid(self, cpu, display, tdiff): + if display: + load = AverageLoad() + cpu = 'Last PID: %i' % load.last_pid + return cpu + + def function_online(self, cpu, display, tdiff): + if display: + try: + cpu, cpuonline, on, off = '', CPUOnline(), 0, 0 + online, offline = cpuonline.online, cpuonline.offline + while on < len(online) and off < len(offline): + if online[on] < offline[off]: + cpu += ' \033[32m%s\033[0m' % online[on] + on += 1 + else: + cpu += ' \033[31m%s\033[0m' % offline[off] + off += 1 + cpu += ''.join(' \033[32m%s\033[0m' % c for c in online[on:]) + cpu += ''.join(' \033[31m%s\033[0m' % c for c in offline[off:]) + cpu = 'Online:%s' % cpu + except Exception as e: + cpu = str(e) + return cpu + + def function_sirq_split(self, key, cpu, display, tdiff): + if display: + label = 'Sirq(%s)' % key.lower().replace('_', ' ').replace(' rx', '↓').replace(' tx', '↑') + now = self.now_sirq_split[key] + last = self.last_sirq_split[key] + hz = lambda n, l : '%0.fHz' % ((n - l) / tdiff) + n = len(now) + snow, slast = sum(now), sum(last) + anow, alast = snow / n, slast / n + each = '' + if self.show_all: + each = '%s : ' % ' '.join(hz(n, l) for n, l in zip(now, last)) + else: + each = '' + cpu = '%s: %s%s(%s)' % (label, each, hz(snow, slast), hz(anow, alast)) + return cpu + + def function(self): + now = time.monotonic() + cpu = CPU() + self.now_sirq_split = SoftIRQs() + tdiff = now - self.last_time + self.last_time = now + display = self.show_function + self.now_cpu_stat = cpu.cpu + self.now_cpu_total = sum(cpu.cpu) + self.now_cpus_total = [sum(c) for c in cpu.cpus] + self.now_cpus_stat = [[c[s] for c in cpu.cpus] for s in range(len(cpu.cpu))] + for i in range(len(self.functions)): + if i == display: + ret = self.functions[i](cpu, True, tdiff) + else: + self.functions[i](cpu, False, tdiff) + self.last_cpus_stat = self.now_cpus_stat + self.last_cpu_stat = self.now_cpu_stat + self.last_cpus_total = self.now_cpus_total + self.last_cpu_total = self.now_cpu_total + self.last_sirq_split = self.now_sirq_split + return ret diff --git a/xpybar/config/myii.py b/xpybar/config/myii.py new file mode 100644 index 0000000..77bf813 --- /dev/null +++ b/xpybar/config/myii.py @@ -0,0 +1,113 @@ +# -*- python -*- +from plugins.ii import II + +from common import * + +class MyII(Entry): + def __init__(self, *args, channel = None, prefix = None, display = None, backlog = None, watch = None, **kwargs): + self.semaphore = threading.Semaphore() + self.log = [] + self.text = '' + self.ii = II(channel, prefix = prefix) + self.display = display + self.backlog = backlog + self.watch = (lambda s : False) if watch is None else watch + self.show_line = 0 + Entry.__init__(self, *args, **kwargs) + + def start(self): + xasync(self.wait, name = 'ii') + + def wait(self): + self.refresh() + def refresh_(): + time.sleep(0.1) + self.refresh() + while True: + xasync(refresh_, name = 'ii') + try: + self.ii.wait() + except: + time.sleep(5) + self.refresh() + + def refresh(self): + on_last = False + highlighted = 0 + self.semaphore.acquire() + try: + new = self.ii.read() + if self.backlog is not None: + new = new[-(self.backlog):] + self.backlog = None + for i in range(len(new)): + if self.watch(new[i]): + new[i] = (new[i], True) + highlighted += 1 + else: + new[i] = (new[i], False) + on_last = self.show_line >= len(self.log) - 1 + self.log.extend(new) + if on_last and len(new) > 0: + self.show_line = len(self.log) - 1 + text = self.log[self.show_line][0] + if self.log[self.show_line][1]: + text = '\033[33m%s\033[39m' % text + self.text = text + finally: + self.semaphore.release() + if len(new) > 0: + if on_last: + self.invalidate() + if highlighted > 0 and self.display is not None: + self.display.adjust(highlighted) + + def action(self, col, button, x, y): + if len(self.log) == 0: + return + update = False + self.semaphore.acquire() + try: + if button == LEFT_BUTTON: + if self.log[self.show_line][1]: + self.log[self.show_line] = (self.log[self.show_line][0], False) + self.text = self.log[self.show_line][0] + update = True + if self.display is not None: + self.display.adjust(-1) + elif self.watch(self.log[self.show_line][0]): + self.log[self.show_line] = (self.log[self.show_line][0], True) + self.text = '\033[33m%s\033[39m' % self.log[self.show_line][0] + update = True + if self.display is not None: + self.display.adjust(-1) + elif button == MIDDLE_BUTTON: + adj = 0 + for i in range(self.show_line): + if self.log[i][1]: + adj -= 1 + self.log = self.log[self.show_line:] + self.show_line = 0 + if adj != 0: + self.display.adjust(adj) + elif button == RIGHT_BUTTON: + for i, (text, marked) in enumerate(self.log): + if marked: + self.show_line = i + self.text = '\033[33m%s\033[39m' % text if marked else text + update = True + break + elif button in (SCROLL_UP, SCROLL_DOWN): + line = self.show_line + (-1 if button == SCROLL_UP else +1) + if 0 <= line < len(self.log): + self.show_line = line + (text, marked) = self.log[line] + self.text = '\033[33m%s\033[39m' % text if marked else text + update = True + finally: + self.semaphore.release() + if update: + self.invalidate() + + def function(self): + return self.text diff --git a/xpybar/config/myio.py b/xpybar/config/myio.py new file mode 100644 index 0000000..2475348 --- /dev/null +++ b/xpybar/config/myio.py @@ -0,0 +1,351 @@ +# -*- python -*- +from plugins.dentrystate import DentryState +from plugins.df import Discs +from plugins.discstats import DiscStats +from plugins.files import Files +from plugins.inodestate import InodeState +from plugins.locks import Locks +from plugins.random import Random + +from common import * + +class MyIO(Entry): + def __init__(self, *args, fs_name_map = None, fs_ignore = None, fs_type_ignore = None, **kwargs): + self.fs_name_map = [] if fs_name_map is None else fs_name_map + self.fs_ignore = set([] if fs_ignore is None else fs_ignore) + self.fs_type_ignore = set(MyIO.nodev_fs() if fs_type_ignore is None else fs_type_ignore) + self.show_value = 0 + self.show_disc_value = 0 + self.show_overall = True + self.show_disc = 0 + self.disc_map = None + Entry.__init__(self, *args, **kwargs) + + @staticmethod + def nodev_fs(): + ret = [] + with open('/proc/filesystems', 'rb') as file: + data = file.read() + data = data.decode('utf-8', 'strict')[:-1].replace('\t', ' ').split('\n') + for line in data: + cols = line.split(' ') + if 'nodev' in cols[:-1]: + ret.append(cols[-1]) + ret += ['fuse.gvfsd-fuse'] + return ret + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + overall = self.show_overall + disc_map = self.disc_map + if overall and disc_map is not None: + x = 0 + for text, disc in disc_map: + if col < x: + break + w = Bar.coloured_length(text) + if col < x + w: + if disc is not None: + self.show_disc = disc + break + x += w + 1 + self.show_overall = not overall + elif button == MIDDLE_BUTTON and not self.show_overall: + self.show_disc_value = max(self.show_disc_value - 1, 0) + elif button == RIGHT_BUTTON and not self.show_overall: + self.show_disc_value = min(self.show_disc_value + 1, 4) + elif button == SCROLL_UP: + if self.show_overall: + self.show_value = min(self.show_value + 1, 13) + else: + show_disc = self.show_disc + if isinstance(show_disc, str): + try: + show_disc = [d.filesystem for d in self.get_discs()].index(show_disc) + except: + show_disc = -1 + self.show_disc = show_disc + 1 + elif button == SCROLL_DOWN: + if self.show_overall: + self.show_value = max(self.show_value - 1, 0) + else: + show_disc = self.show_disc + if isinstance(show_disc, str): + try: + show_disc = [d.filesystem for d in self.get_discs()].index(show_disc) + except: + show_disc = -1 + self.show_disc = max(show_disc - 1, 0) + else: + return + self.invalidate() + + def colour(self, percent): + colour = '39' + if percent < 25: colour = '32' + if percent > 50: colour = '33' + if percent > 90: colour = '31' + if percent > 95: colour = '39;41' + return colour + + def unit(self, value): + units = ['', 'K', 'M', 'G', 'T', 'P', 'E'] + unit = 0 + while unit + 1 < len(units) and value >= 1024: + unit += 1 + value /= 1024 + return (value, units[unit]) + + def si(self, value, baseunit = 0): + unit = baseunit + 3 + units = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E'] + while unit + 1 < len(units) and value >= 1000: + unit += 1 + value /= 1000 + return (value, units[unit]) + + def get_discs(self): + discs = Discs() + fs = [discs.filesystems[f] for f in sorted(discs.filesystems.keys()) if + f not in self.fs_ignore and + discs.filesystems[f].fstype not in self.fs_type_ignore and + f[0] == '/'] + return fs + + def function_overall(self): + df = [] + if self.show_value == 0: + label = 'Df' + self.disc_map = [(label + ':', None)] + used = 0 + available = 0 + for disc in self.get_discs(): + percent = disc.used * 100 / (disc.used + disc.available) + text = '\033[%sm%.0f\033[0m%%' % (self.colour(percent), percent) + df.append(text) + self.disc_map.append((text, disc.filesystem)) + used += disc.used + available += disc.available + df.append(':') + percent = used * 100 / (used + available) + df.append('\033[%sm%.0f\033[0m%%' % (self.colour(percent), percent)) + + elif self.show_value == 1: + label = 'Df' + self.disc_map = [(label + ':', None)] + used = 0 + available = 0 + for disc in self.get_discs(): + percent = disc.used * 100 / (disc.used + disc.available) + text = '\033[%sm%.1f\033[0m%s' % (self.colour(percent), *self.unit(disc.available)) + df.append(text) + self.disc_map.append((text, disc.filesystem)) + used += disc.used + available += disc.available + df.append(':') + percent = used * 100 / (used + available) + df.append('\033[%sm%.1f\033[0m%s' % (self.colour(percent), *self.unit(available))) + + elif self.show_value == 2: + label = 'Df(inodes)' + self.disc_map = [(label + ':', None)] + used = 0 + available = 0 + for disc in self.get_discs(): + percent = disc.iused * 100 / (disc.iused + disc.ifree) + text = '\033[%sm%.0f\033[0m%%' % (self.colour(percent), percent) + df.append(text) + self.disc_map.append((text, disc.filesystem)) + used += disc.iused + available += disc.ifree + df.append(':') + percent = used * 100 / (used + available) + df.append('\033[%sm%.0f\033[0m%%' % (self.colour(percent), percent)) + + elif self.show_value == 3: + label = 'Dent' + dentry = DentryState() + percent = (dentry.nr_dentry - dentry.nr_unused) * 100 / dentry.nr_dentry + df.append('\033[%sm%.0f\033[0m%%' % (self.colour(percent), percent)) + df.append('%is' % dentry.age_limit) + df.append('%ipages' % dentry.want_pages) + + elif self.show_value == 4: + label = 'Inode' + inode = InodeState() + percent = (inode.nr_inodes - inode.nr_free_inodes) * 100 / inode.nr_inodes + df.append('\033[%sm%.0f\033[0m%%%s' % (self.colour(percent), percent, + '' if inode.preshrink == 0 else ' preshrink')) + elif self.show_value == 5: + label = 'File' + files = Files() + percent = (files.nr_files - files.nr_free_files) * 100 / files.file_max + df.append('\033[%sm%.0f\033[0m%%' % (self.colour(percent), percent)) + df.append('%i/%i/%i' % (files.nr_files - files.nr_free_files, files.nr_files, files.file_max)) + + elif self.show_value == 6: + label = 'Lock' + locks = Locks().locks + ar, aw, mr, mw = 0, 0, 0, 0 + for lock in locks: + if lock.mandatory: + if lock.shared: + mr += 1 + else: + mw += 1 + else: + if lock.shared: + ar += 1 + else: + aw += 1 + df.append('%iar' % ar) + df.append('%iaw' % aw) + df.append('%imr' % mr) + df.append('%imw' % mw) + df.append(':') + df.append('%ia' % (ar + aw)) + df.append('%im' % (mr + mw)) + df.append('%ir' % (ar + mr)) + df.append('%iw' % (aw + mw)) + df.append(':') + df.append('%i' % (ar + aw + mr + mw)) + + elif self.show_value == 7: + label = 'Rand' + random = Random() + df.append(str(random.poolsize)) + + elif self.show_value in (8, 11): + label = 'Disc(r %s)' % ('sum' if self.show_value == 8 else 'avg') + stats = DiscStats().devices.values() + r_complete, r_merge, r_sectors, r_time = 0, 0, 0, 0 + for stat in stats: + r_complete += stat.r_complete + r_merge += stat.r_merge + r_sectors += stat.r_sectors + r_time += stat.r_time + n = 1 if self.show_value == 8 else len(stats) + r_complete /= n + r_merge /= n + r_sectors /= n + r_time /= n + df.append('%.1f%scompleted' % self.si(r_complete)) + df.append('%.1f%smerge' % self.si(r_merge)) + df.append('%.1f%ssectors' % self.si(r_sectors)) + df.append('%.1f%ss' % self.si(r_time, -1)) + + elif self.show_value in (9, 12): + label = 'Disc(w %s)' % ('sum' if self.show_value == 9 else 'avg') + stats = DiscStats().devices.values() + w_complete, w_merge, w_sectors, w_time = 0, 0, 0, 0 + for stat in stats: + w_complete += stat.w_complete + w_merge += stat.w_merge + w_sectors += stat.w_sectors + w_time += stat.w_time + n = 1 if self.show_value == 9 else len(stats) + w_complete /= n + w_merge /= n + w_sectors /= n + w_time /= n + df.append('%.1f%scomplete' % self.si(w_complete)) + df.append('%.1f%smerge' % self.si(w_merge)) + df.append('%.1f%ssectors' % self.si(w_sectors)) + df.append('%.1f%ss' % self.si(w_time, -1)) + + elif self.show_value in (10, 13): + label = 'Disc(io %s)' % ('sum' if self.show_value == 10 else 'avg') + stats = DiscStats().devices.values() + io_current, io_time, io_weighted_time = 0, 0, 0 + for stat in stats: + io_current += stat.io_current + io_time += stat.io_time + io_weighted_time += stat.io_weighted_time + n = 1 if self.show_value == 10 else len(stats) + io_current /= n + io_time /= n + io_weighted_time /= n + df.append('%.1f%s' % self.si(io_current)) + df.append('%.1f%ss' % self.si(io_time, -1)) + df.append('%.1f%ss(weighted)' % self.si(io_weighted_time, -1)) + + return '%s: %s' % (label, ' '.join(df)) + + def function_disc(self): + discs = self.get_discs() + disc = self.show_disc + if isinstance(disc, str): + for i, d in enumerate(discs): + if d.filesystem == disc: + disc = i + break + if isinstance(disc, str): + disc = 0 + elif disc >= len(discs): + disc = len(discs) - 1 + disc = discs[disc] + name = disc.filesystem + name = self.fs_name_map[name] if name in self.fs_name_map else name.split('/')[-1] + df = [] + + if self.show_disc_value > 1: + try: + devices = DiscStats().devices + device = None + fs = os.path.realpath(disc.filesystem) + stat = None + for dev in devices.keys(): + if '/dev/' + dev == fs: + stat = devices[dev] + break + if stat is None: + self.show_disc_value = 0 + except: + self.show_disc_value = 0 + + if self.show_disc_value == 0: + percent = disc.used * 100 / (disc.used + disc.available) + text = '\033[%sm%.0f\033[0m%%(%.1f%sB/%.f%sB)' % (self.colour(percent), percent, + *self.unit(disc.available), + *self.unit(disc.used + disc.available)) + df.append(text) + percent = disc.iused * 100 / (disc.iused + disc.ifree) + text = '\033[%sm%.0f\033[0m%%(%.1f%s/%.1f%s)inodes' % (self.colour(percent), percent, + *self.si(disc.iused), *self.si(disc.inodes)) + df.append(text) + + elif self.show_disc_value == 1: + df.append(disc.fstype) + df.append(disc.mountpoint) + + elif self.show_disc_value == 2: + name += '(r)' + df.append('%.1f%scompleted' % self.si(stat.r_complete)) + df.append('%.1f%smerge' % self.si(stat.r_merge)) + df.append('%.1f%ssectors' % self.si(stat.r_sectors)) + df.append('%.1f%ss' % self.si(stat.r_time, -1)) + + elif self.show_disc_value == 3: + name += '(w)' + df.append('%.1f%scomplete' % self.si(stat.w_complete)) + df.append('%.1f%smerge' % self.si(stat.w_merge)) + df.append('%.1f%ssectors' % self.si(stat.w_sectors)) + df.append('%.1f%ss' % self.si(stat.w_time, -1)) + + elif self.show_disc_value == 4: + name += '(io)' + df.append('%.1f%s' % self.si(stat.io_current)) + df.append('%.1f%ss' % self.si(stat.io_time, -1)) + df.append('%.1f%ss(weighted)' % self.si(stat.io_weighted_time, -1)) + + return '%s: %s' % (name, ' '.join(df)) + + def function(self): + try: + self.disc_map = None + if self.show_overall: + return self.function_overall() + else: + return self.function_disc() + except Exception as e: + return str(e) diff --git a/xpybar/config/myipaddress.py b/xpybar/config/myipaddress.py new file mode 100644 index 0000000..424dfac --- /dev/null +++ b/xpybar/config/myipaddress.py @@ -0,0 +1,89 @@ +# -*- python -*- +from plugins.ipaddress import IPAddress +from plugins.clock import Clock + +from common import * + +# TODO add IPv6 address +class MyIPAddress(Entry): + def __init__(self, *args, ignore = None, public = True, both = False, **kwargs): + self.ignore = ['lo'] if ignore is None else ignore + self.text = None + self.show_both = both + self.show_public = public + self.show_nic = None + Entry.__init__(self, *args, **kwargs) + def init(): + self.refresh() + xasync(lambda : Clock(sync_to = 10 * Clock.MINUTES).continuous_sync(t(self.refresh)), name = 'ipaddress') + xasync(init, name = 'ipaddress') + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + self.show_public = not self.show_public + self.invalidate() + elif button == MIDDLE_BUTTON: + self.show_both = not self.show_both + self.invalidate() + elif button == RIGHT_BUTTON: + xasync(self.refresh, name = 'ipaddress') + elif button in (SCROLL_UP, SCROLL_DOWN): + nic = self.show_nic + if nic is None: + return + nics = list(sorted(self.text[0].keys())) + try: + nic = nics.index(nic) + except: + if len(nics) == 0: + self.show_nic = None + self.invalidate() + else: + self.show_nic = nics[0] + self.invalidate() + nic += +1 if button == SCROLL_UP else -1 + if 0 <= nic < len(nics): + self.show_nic = nics[nic] + self.invalidate() + + def refresh(self): + text_private, text_public, text_both = {}, {}, {} + ipa = IPAddress(*(self.ignore)) + for nic in ipa.nics.keys(): + (state, a4, a6, a) = ipa.nics[nic] + if state == IPAddress.DOWN: + label = '31' + elif state == IPAddress.UP: + label = '32' + elif state == IPAddress.UNKNOWN: + label = '33' + else: + label = '39' + label = '\033[%sm%s\033[0m' % (label, nic) + prv = pub = '' + if a4 is not None: + prv += ' ' + a4 + if a6 is not None: + prv += ' ' + a6 + if a is not None: + pub += ' ' + a + finalise = lambda addrs : '%s:%s' % (label, (addrs if addrs != '' else ' no address')) + text_private[nic] = finalise(prv) + text_public[nic] = finalise(pub) + text_both[nic] = finalise(pub + prv) + self.text = (text_private, text_public, text_both) + self.invalidate() + + def function(self): + text = self.text + if text is None: + return '...' + text = text[2 if self.show_both else (1 if self.show_public else 0)] + nic = self.show_nic + if nic is None or nic not in text: + if len(text.keys()) == 0: + self.show_nic = None + return 'No NIC available' + nic = sorted(text.keys())[0] + self.show_nic = nic + return text[nic] diff --git a/xpybar/config/myirc.py b/xpybar/config/myirc.py new file mode 100644 index 0000000..3fa4b71 --- /dev/null +++ b/xpybar/config/myirc.py @@ -0,0 +1,38 @@ +# -*- python -*- +from common import * + +class MyIRC(Entry): + def __init__(self, *args, ii = None, **kwargs): + self.semaphore = threading.Semaphore() + self.text = 'Irc: 0' + self.count = 0 + self.ii = ii + Entry.__init__(self, *args, **kwargs) + + def colourise(self, n): + if n >= 1: return '32' + if n >= 5: return '33' + if n >= 10: return '31' + return '39' + + def adjust(self, n): + self.semaphore.acquire() + try: + self.count += n + if self.count < 0: + self.count = 0 + self.text = 'Irc: \033[%sm%i\033[0m' % (self.colourise(self.count), self.count) + finally: + self.semaphore.release() + self.invalidate() + + def action(self, col, button, x, y): + if self.count == 0: + return + if button == LEFT_BUTTON: + G.groupi = 1 + G.group = G.groups[G.groupi] + self.ii.action(0, RIGHT_BUTTON, 0, 0) + + def function(self): + return self.text diff --git a/xpybar/config/myleapsec.py b/xpybar/config/myleapsec.py new file mode 100644 index 0000000..e45c985 --- /dev/null +++ b/xpybar/config/myleapsec.py @@ -0,0 +1,69 @@ +# -*- python -*- +from plugins.leapsec import LeapSeconds + +from common import * + +class MyLeapsec(Entry): + def __init__(self, *args, **kwargs): + self.announcement = None + Entry.__init__(self, *args, **kwargs) + def init(): + self.refresh() + xasync(lambda : Clock(sync_to = Clock.MINUTES).continuous_sync(Sometimes(t(self.refresh), 12 * 60)), name = 'leapsec') + xasync(init, name = 'leapsec') + + def action(self, col, button, x, y): + if button == RIGHT_BUTTON: + self.refresh() + + def refresh(self): + leapanon = LeapSeconds() + found = -1 + now = time.time() + for index in range(len(leapanon)): + if leapanon[index][3] >= now: + found = index + break + announcement = leapanon[found] + text = ('+%i' if announcement[4] > 0 else '%i') % announcement[4] + text += 's ' + ('\033[%sm%s\033[00m ago' if found < 0 else 'in \033[%sm%s\033[00m') + text += ' end of UTC %i-%02i-%0i' % announcement[:3] + if announcement[5] == LeapSeconds.SECONDARY: + text += ' (secondary)' + elif announcement[5] == LeapSeconds.OUT_OF_BAND: + text += ' (out of band)' + self.announcement = (text, announcement[3]) + self.invalidate() + + def dur(self, t): + s, t = t % 60, t // 60 + m, t = t % 60, t // 60 + h, d = t % 24, t // 24 + if d > 0: + return '%id%ih%i\'%02i"' % (d, h, m, s) + elif h > 0: + return '%ih%i\'%02i"' % (h, m, s) + elif m > 0: + return '%i\'%02i"' % (m, s) + else: + return '%02is' % s + + def colour(self, time_until): + c = '00' + if time_until >= 0: + if time_until < 10: + c = '41' + elif time_until < 60: + c = '31' + elif time_until < 60 * 60: + c = '33' + elif time_until < 24 * 60 * 60: + c = '32' + return c + + def function(self): + if self.announcement is None: + return '...' + (text, when) = self.announcement + time_until = when - int(time.time()) + return text % (self.colour(time_until), self.dur(abs(time_until))) diff --git a/xpybar/config/mylid.py b/xpybar/config/mylid.py new file mode 100644 index 0000000..268a860 --- /dev/null +++ b/xpybar/config/mylid.py @@ -0,0 +1,32 @@ +# -*- python -*- +from plugins.lid import Lid + +from common import * + +class MyLid(Entry): + COMPACT = 0 + LONG = 1 + TITLED = 2 + FULL = 3 + + def __init__(self, *args, details = 2, **kwargs): + self.details = details + self.map = [{True : 'o', + False : 'c'}, + {True : 'open', + False : 'closed'}] + Entry.__init__(self, *args, **kwargs) + + def action(self, col, button, x, y): + if button == MIDDLE_BUTTON: + self.details ^= MyLid.TITLED + self.invalidate() + elif button == RIGHT_BUTTON: + self.details ^= MyLid.LONG + self.invalidate() + + def function(self): + prefix = '' if (self.details & MyLid.TITLED) == 0 else 'Lid: ' + smap = self.map[self.details & MyLid.LONG] + state = Lid.is_open() + return prefix + ' '.join(smap[state[lid]] for lid in state) diff --git a/xpybar/config/mymemory.py b/xpybar/config/mymemory.py new file mode 100644 index 0000000..e0eb76b --- /dev/null +++ b/xpybar/config/mymemory.py @@ -0,0 +1,117 @@ +# -*- python -*- +from plugins.mem import Memory +from plugins.swaps import Swaps + +from common import * + +class MyMemory(Entry): + def __init__(self, *args, **kwargs): + self.coloured = tuple(self.colourise(i) for i in range(101)) + self.show_labels = True + self.show_value = -1 + self.show_default = False + self.show_swaps = False + self.show_swap = 0 + self.labels = list(sorted(Memory().keys)) + swaps = Swaps() + self.prio = swaps.header_map['Priority'] + self.used = swaps.header_map['Used'] + self.size = swaps.header_map['Size'] + self.swap = swaps.header_map['Filename'] + Entry.__init__(self, *args, **kwargs) + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + if self.show_value >= 0: + self.show_default = not self.show_default + self.invalidate() + elif button == MIDDLE_BUTTON: + self.show_labels = not self.show_labels + self.invalidate() + elif button == RIGHT_BUTTON: + self.show_swaps = not self.show_swaps + self.invalidate() + elif button == SCROLL_UP: + if self.show_swaps: + self.show_swap += 1 + self.invalidate() + else: + n = self.show_value + 1 + if n < len(self.labels): + self.show_value = n + self.invalidate() + elif button == SCROLL_DOWN: + if self.show_swaps: + n = self.show_swap + if n > 0: + self.show_swap = n - 1 + self.invalidate() + else: + self.show_value -= 1 + if self.show_value < -1: + self.show_value = -1 + self.invalidate() + + def u(self, value): + units = 'KMGTPE' + unit = 0 + while unit + 1 < len(units) and value >= 1024: + unit += 1 + value /= 1024 + return '%.1f%s' % (value, units[unit]) + + def colourise(self, value): + if value >= 100: + return '\033[31m100\033[0m' + colour = '39' + if value > 30: colour = '32' + if value > 50: colour = '33' + if value > 80: colour = '31' + return '\033[%sm%i\033[0m%%' % (colour, value) + + def function(self): + if self.show_swaps: + swaps = Swaps() + while True: + if len(swaps.swaps) == 0: + return 'no swaps available' + try: + swap = swaps.swaps[self.show_swap] + used, size = int(swap[self.used]), int(swap[self.size]) + swap, prio = swap[self.swap], int(swap[self.prio]) + return '%s: %s (%sB/%sB, prio %i)' % (swap, self.colourise(used * 100 / size), + self.u(used), self.u(size), prio) + except: + self.show_swap -= 1 + if self.show_swap < 0: + self.show_swaps = False + return self.function() + else: + memory = Memory() + if not self.show_default and self.show_value > 0: + label = self.labels[self.show_value] + try: + value = memory[label] + unit = 0 + units = ['K', 'M', 'G', 'T', 'P', 'E'] + while (unit + 1 < len(units)) and (value >= 1024): + value /= 1024 + unit += 1 + return '%s: %.1f%sB' % (label, value, units[unit]) + except: + self.show_value = -1 + if memory.mem_total == 0: + mem = '---' + shm = '---' + else: + mem = self.coloured[limited(memory.mem_used * 100 / memory.mem_total)] + shm = self.coloured[limited(memory.shmem * 100 / memory.mem_total)] + if memory.swap_total == 0: + swp = 'n/a' + else: + swp = self.coloured[limited(memory.swap_used * 100 / memory.swap_total)] + if self.show_labels: + mem = 'Mem: %s%sSwp: %s%sShm: %s' % (mem, SEPARATOR, swp, SEPARATOR, shm) + else: + mem = 'Mem: %s %s %s' % (mem, swp, shm) + return mem diff --git a/xpybar/config/mymoc.py b/xpybar/config/mymoc.py new file mode 100644 index 0000000..da16ece --- /dev/null +++ b/xpybar/config/mymoc.py @@ -0,0 +1,70 @@ +# -*- python -*- +from plugins.moc import MOC + +from common import * + +class MyMOC(Entry): + def __init__(self, *args, ignore = None, **kwargs): + self.show_state = False + self.display = 0 + self.function_display = Sometimes(self.function_display_, 4) + Entry.__init__(self, *args, **kwargs) + + def invalidate(self): + self.function_display.counter = 0 + Entry.invalidate(self) + + def action(self, col, button, x, y): + if button == RIGHT_BUTTON: + self.show_state = not self.show_state + self.invalidate() + elif self.show_state: + if button == LEFT_BUTTON: + self.invalidate() + elif button == SCROLL_UP: + n = self.display + 1 + if n >= 6: + return + self.display = n + self.invalidate() + elif button == SCROLL_DOWN: + n = self.display - 1 + if n < 0: + return + self.display = n + self.invalidate() + else: + moc = MOC() + if moc.state == MOC.NOT_RUNNING: + return + elif button == LEFT_BUTTON: + if col < 5: + return + col -= 5 + if col % 3 == 2: + return + col //= 3 + if col == 0: xasync(lambda : moc.play().wait()) + elif col == 1: xasync(lambda : moc.toggle_pause().wait()) + elif col == 2: xasync(lambda : moc.stop().wait()) + elif col == 3: xasync(lambda : moc.previous().wait()) + elif col == 4: xasync(lambda : moc.next().wait()) + elif button == SCROLL_UP: xasync(lambda : moc.seek(+5).wait()) + elif button == SCROLL_DOWN: xasync(lambda : moc.seek(-5).wait()) + + def function_display_(self): + moc = MOC() + display = self.display + if display == 0 or moc.state in (MOC.NOT_RUNNING, MOC.STOPPED): + return 'Moc: %s' % { MOC.NOT_RUNNING : 'not running' + , MOC.STOPPED : 'stopped' + , MOC.PAUSED : 'paused' + , MOC.PLAYING : 'playing'}[moc.state] + key = ['SongTitle', 'Album', 'Artist', 'Title', 'File'][display - 1] + label = key.lower() if display != 1 else 'song' + return 'Moc(%s): %s' % (label, moc[key]) + + def function(self): + if not self.show_state: + return 'Moc: |> || [] |< >|' + return self.function_display() diff --git a/xpybar/config/mynetwork.py b/xpybar/config/mynetwork.py new file mode 100644 index 0000000..79cd271 --- /dev/null +++ b/xpybar/config/mynetwork.py @@ -0,0 +1,205 @@ +# -*- python -*- +from plugins.network import Network +from plugins.ping import Ping + +from common import * + +class MyNetwork(Entry): + def __init__(self, *args, limits = None, ignore = None, pings = None, **kwargs): + self.limits = { 'rx_bytes' : None # Download speed in bytes (not bits) + , 'tx_bytes' : None # Upload speed in bytes (not bits) + , 'rx_total' : None # Download cap in bytes + , 'tx_total' : None # Upload cap in bytes + } + if limits is not None: + self.limits = limits + if pings is None: + pings = ['gateway'] + elif isinstance(pings, str) or isinstance(pings, Ping): + pings = [pings] + pings = [(Ping(targets = Ping.get_nics(p)) if isinstance(p, str) else p).monitors for p in pings] + self.pings = {} + for ping in pings: + for nic in ping.keys(): + if nic not in self.pings: + self.pings[nic] = ping[nic] + else: + self.pings[nic] += ping[nic] + self.ignore = ['lo'] if ignore is None else ignore + self.net_time = time.monotonic() + self.net_last = {} + self.show_all = True + self.show_name = True + self.show_value = 0 + self.labels = ['bytes', 'total', 'packets', 'errs', + 'drop', 'fifo', 'frame', 'colls', + 'carrier', 'compressed', 'multicast'] + self.in_bytes = False # in bits if showing total + Entry.__init__(self, *args, **kwargs) + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + self.show_all = not self.show_all + elif button == MIDDLE_BUTTON: + self.show_name = not self.show_name + elif button == RIGHT_BUTTON: + self.in_bytes = not self.in_bytes + elif button == SCROLL_UP: + n = self.show_value + 1 + if n >= len(self.labels): + return + self.show_value = n + elif button == SCROLL_DOWN: + n = self.show_value - 1 + if self.show_value < 0: + return + self.show_value = n + else: + return + self.invalidate() + + def colourise(self, value, percent): + colour = '39' + if percent > 25: colour = '32' + if percent > 50: colour = '33' + if percent > 90: colour = '31' + return '\033[%sm%3.0f\033[0m' % (colour, value) + + def KBps(self, network, device, direction, net_tdiff): + direction += '_bytes' + value = network[device][direction] + limit = None + if direction in self.limits: + limit = self.limits[direction] + if limit is None: + limit = 12500000 # 100 mbps + if device in self.net_last: + value -= self.net_last[device][direction] + else: + value = 0 + percent = value * 100 / (limit * net_tdiff) + value /= 1024 * net_tdiff + return self.colourise(value, percent) + + def kbps(self, network, device, direction, net_tdiff): + direction += '_bytes' + value = network[device][direction] + limit = None + if direction in self.limits: + limit = self.limits[direction] + if limit is None: + limit = 12500000 # 100 mbps + if device in self.net_last: + value -= self.net_last[device][direction] + else: + value = 0 + percent = value * 100 / (limit * net_tdiff) + value /= 128 * net_tdiff + return self.colourise(value, percent) + + def total(self, network, device, direction, bits): + direction += '_bytes' + value = network[device][direction] + limit = self.limits[direction] if direction in self.limits else None + if limit is not None: + percent = value * 100 / limit + colour = '32' + if percent >= 25: colour = '39' + if percent >= 50: colour = '33' + if percent >= 75: colour = '31' + if percent >= 95: colour = '7;33' + if percent >= 100: colour = '7;31' + else: + colour = '39' + if bits: + value *= 8 + unit = 0 + if bits: + units = ['', 'k', 'm', 'g', 't', 'p', 'e'] + else: + units = ['', 'K', 'M', 'G', 'T', 'P', 'E'] + while (unit + 1 < len(units)) and (value >= 1024): + value /= 1024 + unit += 1 + return '\033[%sm%.1f\033[0m%s' % (colour, value, units[unit]) + + def ping(self, monitor): + monitor.semaphore.acquire() + try: + latency = monitor.get_latency(True)[1] + droptime = monitor.dropped_time(True) + if droptime: + return ' \033[31m%4is\033[00m' % droptime + elif latency is None: + return ' \033[31mdrop?\033[00m' + colour = '31' + if latency < 5: colour = '32' + elif latency < 10: colour = '39' + elif latency < 20: colour = '33' + return ' \033[%sm%5.2f\033[00m' % (colour, latency) + finally: + monitor.semaphore.release() + + def function(self): + net_now = time.monotonic() + net_tdiff, self.net_time = net_now - self.net_time, net_now + network = Network(*self.ignore).devices + label = self.labels[self.show_value] + show_total = label == 'total' + show_bytes = label == 'bytes' + xlabel = 'bytes' if show_total else label + suffix = '' + if show_total and self.in_bytes: + f = lambda dev, dir : self.total(network, dev, dir, True) + 'b' + elif show_total: + f = lambda dev, dir : self.total(network, dev, dir, False) + 'B' + elif show_bytes and self.in_bytes: + f = lambda dev, dir : self.KBps(network, dev, dir, net_tdiff) + 'KB/s' + elif show_bytes: + f = lambda dev, dir : self.kbps(network, dev, dir, net_tdiff) + 'kbps' + else: + f = lambda dev, dir : '%i' % network[dev][dir + '_' + label] + suffix = ' ' + label + def create(lbl, dev): + ret = '' + if lbl is not None: + ret += lbl + ': ' + have_down = ('rx_' + xlabel) in network[dev] + have_up = ('tx_' + xlabel) in network[dev] + if have_down: + ret += f(dev, 'rx') + '↓' + if have_up: + ret += ' ' + if have_up: + ret += f(dev, 'tx') + '↑' + ret += suffix + if show_bytes or show_total: + if dev == '*': + for dev in self.pings.keys(): + for ping in self.pings[dev]: + ret += self.ping(ping) + elif dev in self.pings: + for ping in self.pings[dev]: + ret += self.ping(ping) + return ret + if self.show_all: + if self.show_name: + net = [create(dev, dev) for dev in network] + else: + net = [create(None, dev) for dev in network] + net = (SEPARATOR if self.show_name else ' ').join(net) + else: + devsum = {} + for dev in network.keys(): + table = network[dev] + for key in table.keys(): + if key not in devsum: + devsum[key] = 0 + devsum[key] += table[key] + network['*'] = devsum + if self.show_name: + net = create('Net', '*') + else: + net = create(None, '*') + self.net_last = network + return net diff --git a/xpybar/config/mynews.py b/xpybar/config/mynews.py new file mode 100644 index 0000000..a65396e --- /dev/null +++ b/xpybar/config/mynews.py @@ -0,0 +1,45 @@ +# -*- python -*- +import os +from common import * + +class MyNews(Entry): + def __init__(self, *args, status_path = None, **kwargs): + if status_path is not None: + self.status_path = status_path + else: + self.status_path = HOME + '/.var/lib/featherweight/status' + self.get_news() + Entry.__init__(self, *args, **kwargs) + xasync(self.refresh, name = 'news') + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + xasync(lambda : subprocess.Popen([G.TERMINAL, '-e', 'featherweight']).wait()) + elif button == RIGHT_BUTTON: + xasync(lambda : subprocess.Popen(['featherweight', '--update', '--system']).wait()) + + def get_news(self): + try: + with open(self.status_path, 'rb') as file: + status = int(file.read().decode('utf-8', 'replace').replace('\n', '')) + except: + status = 0 + colour = '31' + if status <= 0: colour = '39' + elif status <= 5: colour = '32' + elif status <= 10: colour = '33' + self.text = 'News: \033[%sm%i\033[0m' % (colour, status) + + def refresh(self): + while True: + if os.path.exists(self.status_path): + try: + spawn_read(*pdeath('HUP', 'inotifywait', self.status_path, '-e', 'close_write')) + self.get_news() + except: + time.sleep(1) + else: + time.sleep(1) + + def function(self): + return self.text diff --git a/xpybar/config/myscroll.py b/xpybar/config/myscroll.py new file mode 100644 index 0000000..c5ef807 --- /dev/null +++ b/xpybar/config/myscroll.py @@ -0,0 +1,12 @@ +# -*- python -*- +from common import * + +class MyScroll(Entry): + def function(self): + return '↓↑' + + def action(self, col, button, x, y): + if button in (LEFT_BUTTON, RIGHT_BUTTON, SCROLL_UP, SCROLL_DOWN): + G.groupi = (G.groupi + (+1 if button in (LEFT_BUTTON, SCROLL_DOWN) else -1)) % len(G.groups) + G.group = G.groups[G.groupi] + self.invalidate() diff --git a/xpybar/config/mystat.py b/xpybar/config/mystat.py new file mode 100644 index 0000000..d358d15 --- /dev/null +++ b/xpybar/config/mystat.py @@ -0,0 +1,91 @@ +# -*- python -*- +from plugins.snmp import SNMP +from plugins.snmp6 import SNMP6 +from plugins.uptime import Uptime +from plugins.vmstat import VMStat + +from common import * + +class MyStat(Entry): + def __init__(self, *args, **kwargs): + self.keys_4 = SNMP().keys + self.keys_6 = SNMP6().keys + self.keys_vm = VMStat().keys + self.show_group = 0 + self.show_value_4 = 0 + self.show_value_6 = 0 + self.show_value_vm = 0 + Entry.__init__(self, *args, **kwargs) + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + n = self.show_group + 1 + if n < 4: + self.show_group = n + self.invalidate() + elif button == MIDDLE_BUTTON: + self.invalidate() + elif button == RIGHT_BUTTON: + n = self.show_group + if n > 0: + self.show_group = n - 1 + self.invalidate() + elif button == SCROLL_UP: + if self.show_group == 0: + n = self.show_value_4 + if n + 1 < len(self.keys_4): + self.show_value_4 = n + 1 + self.invalidate() + elif self.show_group == 1: + n = self.show_value_6 + if n + 1 < len(self.keys_6): + self.show_value_6 = n + 1 + self.invalidate() + elif self.show_group == 2: + n = self.show_value_vm + if n + 1 < len(self.keys_vm): + self.show_value_vm = n + 1 + self.invalidate() + elif self.show_group == 3: + pass + elif button == SCROLL_DOWN: + if self.show_group == 0: + n = self.show_value_4 + if n > 0: + self.show_value_4 = n - 1 + self.invalidate() + elif self.show_group == 1: + n = self.show_value_6 + if n > 0: + self.show_value_6 = n - 1 + self.invalidate() + elif self.show_group == 2: + n = self.show_value_vm + if n > 0: + self.show_value_vm = n - 1 + self.invalidate() + elif self.show_group == 3: + pass + + def function(self): + try: + if self.show_group == 0: + label = self.keys_4[self.show_value_4] + value = SNMP()[label] + elif self.show_group == 1: + label = self.keys_6[self.show_value_6] + value = SNMP6()[label] + elif self.show_group == 2: + label = self.keys_vm[self.show_value_vm] + value = VMStat()[label] + label = ''.join(w[:1].upper() + w[1:] for w in label.split('_')) + elif self.show_group == 3: + uptime = Uptime() + avg = uptime.uptime_seconds - uptime.average_idle_seconds + wrk = (avg) * 100 / uptime.uptime_seconds + up = '%id%i:%02i' % uptime.uptime[:3] + avg = '%id%i:%02i' % Uptime.split_time(avg)[:3] + return 'Uptime: %.0f%% %s / %s' % (wrk, avg, up) + return '%s: %i' % (label, value) + except Exception as e: + return str(e) diff --git a/xpybar/config/mysun.py b/xpybar/config/mysun.py new file mode 100644 index 0000000..cfd1d5c --- /dev/null +++ b/xpybar/config/mysun.py @@ -0,0 +1,43 @@ +# -*- python -*- +from common import * + +class MySun(Entry): + def __init__(self, *args, clock = None, **kwargs): + self.text = '...' + self.clock = clock + Entry.__init__(self, *args, **kwargs) + xasync(self.wait, name = 'solar') + + def wait(self): + palette = None + while palette is None: + try: + palette = G.bar.palette + except: + time.sleep(0.1) + twilight_12 = colour_interpol(palette[1], palette[3], 1 / 3) + twilight_6 = colour_interpol(palette[1], palette[3], 2 / 3) + command = pdeath('HUP', 'blueshift', '-c', HOME + '/.config/xpybar/bin/sun') + while True: + try: + text = 'Sun: %s' % spawn_read(*command) + if self.clock is not None: + elevation = float(text.split(' ')[3][:-1]) + if elevation <= -18: self.clock.twilight = '31' + elif elevation <= -12: self.clock.twilight = twilight_12 + elif elevation <= -6: self.clock.twilight = twilight_6 + elif elevation <= 0: self.clock.twilight = '33' + elif elevation <= 6: self.clock.twilight = '39' + else: self.clock.twilight = '34' + self.clock.invalidate() + except: + text = '¿Cannot get solar information?' + if self.clock is not None: + self.clock.twilight = '39' + self.clock.invalidate() + self.text = text + self.invalidate() + time.sleep(20) + + def function(self): + return self.text diff --git a/xpybar/config/mytimer.py b/xpybar/config/mytimer.py new file mode 100644 index 0000000..e886ed3 --- /dev/null +++ b/xpybar/config/mytimer.py @@ -0,0 +1,142 @@ +# -*- python -*- +from common import * + +from datetime import datetime + + +class MyTimer(Entry): + def __init__(self, *args, alarms = [], **kwargs): + self.start = None + self.length = None + self.notified = False + self.pause_text = None + self.triggered_alarm = None + self.alarms = [] + for alarm in alarms: + alarm = alarm.split(': ') + text = ': '.join(alarm[1:]) + if not text: + text = alarm + weekday = None + alarm = alarm[0].split(':') + if not alarm[0].isdigit(): + weekday, alarm[0] = alarm[0].split(' ') + weekday = {'mon' : 0, + 'tue' : 1, + 'wed' : 2, + 'thu' : 3, + 'fri' : 4, + 'sat' : 5, + 'sun' : 6}[weekday.lower()[:3]] + time = (int(alarm[0]) * 60 + int(alarm[1])) * 60 + if len(alarm) > 2: + time += int(alarm[2]) + self.alarms.append([False, time, weekday, text]) + mqueue_map['timer'] = self.mqueue + Entry.__init__(self, *args, **kwargs) + + def mqueue(self, args): + if args[1] == 'stop': + self.stop() + elif args[1] in ('pause', 'resume'): + self.pause() + else: + duration = args[1].split(':') + duration.reverse() + seconds = 0 + if len(duration) > 0: seconds += int(duration[0]) + if len(duration) > 1: seconds += int(duration[1]) * 60 + if len(duration) > 2: seconds += int(duration[2]) * 60 * 60 + if len(duration) > 3: seconds += int(duration[3]) * 60 * 60 * 24 + self.start = time.time() + self.length = seconds + self.notified = False + self.pause_text = None + self.invalidate() + + def action(self, col, button, x, y): + if self.triggered_alarm is not None: + self.triggered_alarm = None + self.invalidate() + elif button == LEFT_BUTTON: + self.pause() + elif button == RIGHT_BUTTON: + self.stop() + + def pause(self): + if self.pause_text is not None: + self.start = time.time() + self.pause_text = None + elif self.length is None: + pass + elif self.length - int(time.time() - self.start) < 0: + self.start, self.length, self.notified, self.pause_text = None, None, False, None + else: + self.pause_text = '' + self.length -= int(time.time() - self.start) + self.pause_text = self.dur(self.length) + self.invalidate() + + def stop(self): + self.start = None + self.length = None + self.notified = False + self.pause_text = None + self.invalidate() + + def dur(self, t): + s, t = t % 60, t // 60 + m, t = t % 60, t // 60 + h, d = t % 24, t // 24 + if d > 0: + return '%id %i:%02i:%02i' % (d, h, m, s) + elif h > 0: + return '%i:%02i:%02i' % (h, m, s) + else: + return '%i:%02i' % (m, s) + + def notify(self, text = 'You are done with your task'): + subprocess.Popen(['notify-send', '-u', 'critical', text]).wait() + + def wall(self, text = 'You are done with your task'): + subprocess.Popen(['wall', text]).wait() + + def function(self): + now = datetime.now() + weekday = now.weekday() + now = now.hour * 60 * 60 + now.minute * 60 + now.second + + if self.triggered_alarm is not None: + countdown = int(self.alarms[self.triggered_alarm][1] - now) % 2 + if countdown == 0: + return '\033[37;41m%s\033[00m' % self.alarms[self.triggered_alarm][3] + else: + return '\033[31m%s\033[00m' % self.alarms[self.triggered_alarm][3] + + for i, alarm in enumerate(self.alarms): + if (alarm[2] is None or weekday == alarm[2]) and now >= alarm[1] and now < alarm[1] + 30 * 60: + if not alarm[0]: + alarm[0] = True + xasync(lambda : self.notify(alarm[3]), name = 'alarm notify') + xasync(lambda : self.wall(alarm[3]), name = 'alarm wall') + self.triggered_alarm = i + return self.function() + else: + alarm[0] = False + + if self.pause_text is not None: + return '%s (paused)' % self.pause_text + if self.start is None: + return 'Timer inactive' + countdown = self.length - int(time.time() - self.start) + if countdown > 0: + return self.dur(countdown) + if not self.notified: + self.notified = True + xasync(self.notify, name = 'timer notify') + xasync(self.wall, name = 'timer wall') + countdown %= 2 + if countdown == 0: + return '\033[37;41mYou are done\033[00m' + else: + return '\033[31mYou are done\033[00m' diff --git a/xpybar/config/mytop.py b/xpybar/config/mytop.py new file mode 100644 index 0000000..6dc1415 --- /dev/null +++ b/xpybar/config/mytop.py @@ -0,0 +1,40 @@ +# -*- python -*- +from common import * + +class MyTop(Entry): + def __init__(self, *args, **kwargs): + self.show_pid = True + self.show_cpu = True + self.show_label = True + self.show_nic = None + self.top_cmd = pdeath('HUP', 'top', '-b', '-n', '1', '-o', '%CPU', '-w', '10000') + self.refresh() + Entry.__init__(self, *args, **kwargs) + xasync(lambda : watch(5, t(self.refresh)), name = 'top') + + def action(self, col, button, x, y): + if button == LEFT_BUTTON: + self.show_pid = not self.show_pid + elif button == MIDDLE_BUTTON: + self.show_label = not self.show_label + elif button == RIGHT_BUTTON: + self.show_cpu = not self.show_cpu + else: + return + self.refresh() + self.invalidate() + + def refresh(self): + top = spawn_read(*self.top_cmd).split('\n') + top = [line for line in top if not line.startswith('%')][6] + top = [col for col in top.replace('\t', ' ').split(' ') if not col == ''] + text = 'Top: ' if self.show_label else '' + if self.show_pid: + text += top[0] + ' ' + if self.show_cpu: + text += top[6] + ' ' + text += ' '.join(top[10:]) + self.text = text + + def function(self): + return self.text diff --git a/xpybar/config/myweather.py b/xpybar/config/myweather.py new file mode 100644 index 0000000..bb90c80 --- /dev/null +++ b/xpybar/config/myweather.py @@ -0,0 +1,420 @@ +# -*- python -*- +from plugins.weather import Weather + +from common import * + +class MyWeather(Entry): + def __init__(self, *args, stations = None, fahrenheit = False, wind = 'm/s', from_wind = False, visibility = 'km', **kwargs): + self.semaphore = threading.Semaphore() + self.stations = [MyWeather.get_metar_station()] if stations is None else [s.upper() for s in stations] + self.reports = [None] * len(self.stations) + self.up_to_date = [False] * len(self.stations) + self.in_deg_f = fahrenheit + self.wind_unit = wind + self.from_wind = from_wind + self.visibility_unit = visibility + self.show_temp = True + self.show_wind_speed = False + self.show_wind_gusts = False + self.show_wind_dir = False + self.show_wind_chill = False + self.show_humidity = False + self.show_dew = False + self.show_pressure = False + self.show_visibility = False + self.show_sky = False + self.show_station = False + self.station = 0 + self.updated = False + self.segments = [] + self.refresh = Sometimes(lambda : xasync(self.refresh_, name = 'weather'), 20 * 60 * 4) + self.refresh() + Entry.__init__(self, *args, **kwargs) + + def action(self, col, button, x, y): + if button == MIDDLE_BUTTON: + self.show_temp = True + self.show_wind_speed = True + self.show_wind_gusts = True + self.show_wind_dir = True + self.show_wind_chill = True + self.show_humidity = True + self.show_dew = True + self.show_pressure = True + self.show_visibility = True + self.show_sky = True + self.show_station = True + self.invalidate() + elif button == RIGHT_BUTTON: + self.refresh.counter = 0 + self.refresh() + elif button in (LEFT_BUTTON, SCROLL_UP, SCROLL_DOWN): + index = None + for i, seg in enumerate(self.segments): + if seg is None: + continue + (x, width) = seg + if 0 <= col - x < width: + index = i + break + if index is None: + return + if index == 0: # station + if button == LEFT_BUTTON: + self.show_station = False + elif button == SCROLL_UP: + station = self.station + if station + 1 < len(self.stations): + self.station = station + 1 + else: + station = self.station + if station > 0: + self.station = station - 1 + elif index == 1: # temperature + if button == LEFT_BUTTON: + self.show_temp = False + else: + self.in_deg_f = button == SCROLL_UP + elif index == 2: # wind speed/gusts + if button == LEFT_BUTTON: + if self.towards is None or col < self.towards: + self.show_wind_speed = False + else: + self.show_wind_gusts = False + else: + if self.wind_unit == 'm/s': + self.wind_unit = 'mph' if button == SCROLL_UP else 'm/s' + elif self.wind_unit == 'mph': + self.wind_unit = 'kn' if button == SCROLL_UP else 'm/s' + elif self.wind_unit in ('kn', 'kt', 'knot', 'knots'): + self.wind_unit = 'km/h' if button == SCROLL_UP else 'mph' + elif self.wind_unit in ('km/h', 'kmph'): + self.wind_unit = 'ft/s' if button == SCROLL_UP else 'kn' + elif self.wind_unit in ('ft/s', 'ftps'): + self.wind_unit = 'b' if button == SCROLL_UP else 'km/h' + else: + self.wind_unit = 'b' if button == SCROLL_UP else 'ft/s' + elif index == 3: # wind direction + if button == LEFT_BUTTON: + self.show_wind_dir = False + else: + self.from_wind = button == SCROLL_UP + elif index == 4: # wind chill + if button == LEFT_BUTTON: + self.show_wind_chill = False + else: + self.in_deg_f = button == SCROLL_UP + elif index == 5: # relative humidity + if button == LEFT_BUTTON: + self.show_humidity = False + else: + return + elif index == 6: # dew point + if button == LEFT_BUTTON: + self.show_dew = False + else: + self.in_deg_f = button == SCROLL_UP + elif index == 7: # pressure + if button == LEFT_BUTTON: + self.show_pressure = False + else: + return + elif index == 8: # visibility + if button == LEFT_BUTTON: + self.show_visibility = False + elif button == SCROLL_UP: + unit = self.visibility_unit + if unit in {'km' : 'm', 'm' : 'mi'}: + self.visibility_unit = {'km' : 'm', 'm' : 'mi'}[unit] + else: + unit = self.visibility_unit + if unit in {'mi' : 'm', 'mile' : 'm', 'm' : 'km'}: + self.visibility_unit = {'mi' : 'm', 'mile' : 'm', 'm' : 'km'}[unit] + elif index == 9: # sky conditions + if button == LEFT_BUTTON: + self.show_sky = False + else: + return + self.invalidate() + + def refresh_(self): + if self.semaphore.acquire(blocking = False): + for index, station in enumerate(self.stations): + try: + self.reports[index] = Weather(station) + self.up_to_date[index] = True + except: + self.up_to_date[index] = False + self.semaphore.release() + self.invalidate() + + def invalidate(self): + self.updated = False + Entry.invalidate(self) + + def get_weather(self, index): + station = self.stations[index] + report = self.reports[index] + up_to_date = self.up_to_date[index] + station = station[0] + station[1:].lower() + + if report is None: + if self.show_station: + return '%s: ?' % station + else: + return '?' + + text = [] + + if self.show_temp and report.temp is not None: + value = report.temp + colour = '34' + if value < -10: colour = '39;44' + if value >= 18: colour = '39' + if value >= 25: colour = '33' + if value >= 30: colour = '31' + if self.in_deg_f: + value = value * 9 / 5 + 32 + value = '\033[%sm%.0f\033[0m%s' % (colour, value, '°F' if self.in_deg_f else '°C') + text.append(value) + else: + text.append(None) + + if self.show_wind_speed and report.wind_speed is not None: + knot = report.wind_speed + kmph = knot * 1.852 # km/h + mps = kmph / 3.6 # m/s + mph = knot * 1.151 # mph + ftps = mps * 3.281 # ft/s + beaufort = (mps / 0.8365) ** (2 / 3) # B + if self.wind_unit in ('kn', 'kt', 'knot', 'knots'): + value, unit = knot, 'kn' + elif self.wind_unit in ('km/h', 'kmph'): + value, unit = kmph, 'km/h' + elif self.wind_unit == 'mph': + value, unit = mph, 'mph' + elif self.wind_unit in ('ft/s', 'ftps'): + value, unit = ftps, 'ft/s' + elif self.wind_unit.lower() in ('beaufort', 'beaufort scale', 'bf', 'bt', 'b'): + value, unit = beaufort, 'B' + else: + value, unit = mps, 'm/s' + beaufort = int(beaufort + 0.5) + colour = '32' + if beaufort >= 1: colour = '39' + if beaufort >= 3: colour = '32' + if beaufort >= 7: colour = '33' + if beaufort >= 10: colour = '31' + if beaufort >= 12: colour = '39;41' + wind = '\033[%sm%.0f\033[0m' % (colour, value) + if self.show_wind_gusts and report.wind_gusts is not None: + knot = report.wind_gusts + kmph = knot * 1.852 # km/h + mps = kmph / 3.6 # m/s + mph = knot * 1.151 # mph + ftps = mps * 3.281 # ft/s + beaufort = (mps / 0.8365) ** (2 / 3) # B + if self.wind_unit in ('kn', 'kt', 'knot', 'knots'): + value = knot + elif self.wind_unit in ('km/h', 'kmph'): + value = kmph + elif self.wind_unit == 'mph': + value = mph + elif self.wind_unit in ('ft/s', 'ftps'): + value = ftps + elif self.wind_unit.lower() in ('beaufort', 'beaufort scale', 'bf', 'bt', 'b'): + value = beaufort + else: + value = mps + beaufort = int(beaufort + 0.5) + colour = '32' + if beaufort >= 1: colour = '39' + if beaufort >= 3: colour = '32' + if beaufort >= 7: colour = '33' + if beaufort >= 10: colour = '31' + if beaufort >= 12: colour = '39;41' + wind += '→\033[%sm%.0f\033[0m' % (colour, value) + wind += unit + text.append(wind) + else: + text.append(None) + + def wind_dir(direction): + if not self.from_wind: + direction += 180 + direction %= 360 + if direction < 0: + direction += 360 + if (direction < 90): + if direction == 0: + return 'N' + if direction <= 45: + return 'N%.0f°E' % direction + return 'E%.0f°N' % (90 - direction) + elif (90 <= direction < 180): + if direction == 90: + return 'E' + direction -= 90 + if direction <= 45: + return 'E%.0f°S' % direction + return 'S%.0f°E' % (90 - direction) + elif (180 <= direction < 270): + if direction == 180: + return 'S' + direction -= 180 + if direction <= 45: + return 'S%.0f°W' % direction + return 'W%.0f°S' % (90 - direction) + else: + if direction == 270: + return 'W' + direction -= 270 + if direction <= 45: + return 'W%.0f°N' % direction + return 'N%.0f°W' % (90 - direction) + if not self.show_wind_dir: + wind = None + elif report.wind_var is not None: + wind = wind_dir(report.wind_var[0]) + '-' + wind_dir(report.wind_var[1]) + elif report.wind_dir is not None: + wind = wind_dir(report.wind_dir) + else: + wind = None + if wind is not None: + if self.from_wind: + wind += '→' + else: + wind = '→' + wind + text.append(wind) + else: + text.append(None) + + if self.show_wind_chill and report.wind_chill is not None: + value = report.wind_chill + # Coloured by frostbite time, however the wind is not taken + # into account so the colour is approximate + colour = '39' + if value <= -28: colour = '33' + if value <= -37: colour = '31' + if value <= -46: colour = '39;41' + if self.in_deg_f: + value = value * 9 / 5 + 32 + value = 'w\033[%sm%.0f\033[0m%s' % (colour, value, '°F' if self.in_deg_f else '°C') + text.append(value) + else: + text.append(None) + + if self.show_humidity and report.humidity is not None: + value = report.humidity + colour = '34' + if value >= 26: colour = '32' + if value >= 31: colour = '39' + if value >= 37: colour = '33' + if value >= 52: colour = '31' + if value >= 73: colour = '39;41' + value = '\033[%sm%.0f\033[0m%%' % (colour, value) + text.append(value) + else: + text.append(None) + + if self.show_dew and report.dew is not None: + value = report.dew + colour = '34' + if value >= 10: colour = '32' + if value >= 13: colour = '39' + if value >= 16: colour = '33' + if value >= 21: colour = '31' + if value >= 26: colour = '39;41' + if self.in_deg_f: + value = value * 9 / 5 + 32 + value = 'd\033[%sm%.0f\033[0m%s' % (colour, value, '°F' if self.in_deg_f else '°C') + text.append(value) + else: + text.append(None) + + if self.show_pressure and report.pressure is not None: + text.append('%.0fhPa' % report.pressure) + else: + text.append(None) + + if self.show_visibility and report.visibility is not None: + mile = report.visibility + km = mile * 1.609344 + m = mile * 1609.344 + if self.visibility_unit in ('mile', 'mi'): + value, unit = mile, 'mi' + elif self.visibility_unit == 'm': + value, unit = m, 'm' + else: + value, unit = km, 'km' + text.append('%.0f%s' % (value, unit)) + else: + text.append(None) + + if self.show_sky and 'Sky conditions' in report.fields: + value = report.fields['Sky conditions'] + if not value == '': + text.append(value) + else: + text.append(None) + else: + text.append(None) + + segments = [] + x = 0 + if self.show_station: + w = len(station) + segments.append((x, w)) + x += w + 2 + else: + segments.append(None) + x -= 1 + self.towards = None + for i, segment in enumerate(text): + if segment is None: + segments.append(None) + else: + if i == 1 and '→' in segment: + self.towards = x + Bar.coloured_length(segment.split('→')[0]) + w = Bar.coloured_length(segment) + x += 1 + segments.append((x, w)) + x += w + self.segments = segments + text = [s for s in text if s is not None] + + if len(text) == 0: + return station + + if not up_to_date: + if len(text) <= 1: + text = ' '.join(text) + '?' + else: + text = ' '.join(text) + ' ?' + else: + text = ' '.join(text) + if self.show_station: + text = '%s: %s' % (station, text) + return text + + def function(self): + if not self.updated: + self.updated = True + self.text = self.get_weather(self.station) + rc = self.text + self.refresh() + return rc + + @staticmethod + def get_metar_station(): + try: + with open(HOME + '/.config/metar', 'rb') as file: + station = file.read() + except: + try: + with open('/etc/metar', 'rb') as file: + station = file.read() + except: + print('~/.config/metar is not set', file = sys.stderr, flush = True) + station = b'ESSA' + return station.decode('utf-8', 'strict').split('\n')[0].upper() diff --git a/xpybar/config/myxmonad.py b/xpybar/config/myxmonad.py new file mode 100644 index 0000000..0b57cef --- /dev/null +++ b/xpybar/config/myxmonad.py @@ -0,0 +1,158 @@ +# -*- python -*- +from common import * + +class MyXMonad(Entry): + def __init__(self, *args, mod = Xlib.XK.XK_Super_L, ppWsSep = '', ppSep = ' ', **kwargs): + self.text = '' + self.text_short = '' + self.ws_sep_len = len(ppWsSep) + self.sep_len = len(ppSep) + self.mod = mod + self.show_full = True + self.short_layout = None + Entry.__init__(self, *args, **kwargs) + xasync(lambda : forever(t(self.refresh)), name = 'xmonad') + + def action(self, col, button, x, y): + if button == MIDDLE_BUTTON: + self.show_full = not self.show_full + self.invalidate() + return + text = self.text + full = self.show_full + UTF8_STRING = G.display.intern_atom('UTF8_STRING') + _XMONAD_LOG = G.display.intern_atom('_XMONAD_LOG') + _NET_DESKTOP_NAMES = G.display.intern_atom('_NET_DESKTOP_NAMES') + xmonad_log = G.display.screen().root.get_full_property(_XMONAD_LOG, UTF8_STRING).value.decode('utf-8', 'replace') + xmonad_log = xmonad_log.split(' : ') + net_desktop_names = G.display.screen().root.get_full_property(_NET_DESKTOP_NAMES, UTF8_STRING).value.decode('utf-8', 'replace') + net_desktop_names = net_desktop_names[:-1].split('\0') + ws = 0 + ws_hit = False + while ws < len(net_desktop_names): + if ws > 0: + col -= self.ws_sep_len + if col < 0: + break + width = len(net_desktop_names[ws] if full else str(ws + 1)) + 2 + if col < width: + ws_hit = True + break + ws += 1 + col -= width + if ws < len(net_desktop_names): + if button == LEFT_BUTTON: + self.switch_workspace(ws + 1) + elif button in (SCROLL_UP, SCROLL_DOWN): + xmonad_log = xmonad_log[0].split(' ') + active = [net_desktop_names.index(w[1:-1]) for w in xmonad_log if w[:1] == '<'] + ws = [net_desktop_names.index(w[1:-1]) for w in xmonad_log if w[:1] == '['][0] + ws += -1 if button == SCROLL_UP else +1 + ws %= len(net_desktop_names) + while ws in active: + ws += -1 if button == SCROLL_UP else +1 + ws %= len(net_desktop_names) + self.switch_workspace(ws + 1) + elif button == LEFT_BUTTON: + col -= self.sep_len + layout = xmonad_log[1] + if not self.show_full and self.short_layout is not None: + layout = self.short_layout + if col < len(layout) + 2: + G.display.xtest_fake_input(Xlib.X.KeyPress, G.display.keysym_to_keycode(self.mod)) + G.display.xtest_fake_input(Xlib.X.KeyPress, G.display.keysym_to_keycode(Xlib.XK.XK_space)) + G.display.xtest_fake_input(Xlib.X.KeyRelease, G.display.keysym_to_keycode(Xlib.XK.XK_space)) + G.display.xtest_fake_input(Xlib.X.KeyRelease, G.display.keysym_to_keycode(self.mod)) + G.display.flush() + + def refresh(self): + try: + text = input() + except: + sys.exit(0) + buf, esc = '', None + for c in text: + if esc is not None: + esc += c + if esc == '^': + buf += '^' + esc = None + elif esc[-1] == ')': + if esc.startswith('bg(') or esc.startswith('fg('): + c = 4 if esc.startswith('bg(') else 3 + esc = esc[3 : -1] + if esc == '': + buf += '\033[%i9m' % c + else: + r, g, b = esc[1 : 3], esc[3 : 5], esc[5 : 7] + r, g, b = int(r, 16), int(g, 16), int(b, 16) + r, g, b = str(r), str(g), str(b) + buf += '\033[%i8;2;%sm' % (c, ';'.join([r, g, b])) + esc = None + elif c == '^': + esc = '' + else: + buf += c + self.text = buf + short = '' + space, esc, layout, lbuf = False, False, 0, '' + ws = 1 + for c in buf: + if esc: + short += c + esc = not (c == '~' or 'a' <= c.lower() <= 'z') + elif c == '\033': + esc = True + if layout == 2: + layout = 3 + end = '' + while lbuf.startswith(' '): + short += ' ' + lbuf = lbuf[1:] + while lbuf.endswith(' '): + end += ' ' + lbuf = lbuf[:-1] + self.short_layout = self.shorten_layout(lbuf) + short += self.short_layout + end + short += c + elif ws <= 9: + if c in ' <>[]': + space = not space + if not space: + short += '%i' % ws + ws += 1 + if ws == 10: + layout = 1 + short += c + elif not space: + short += c + elif layout == 1: + if c == ' ': + short += c + else: + layout = 2 + lbuf += c + elif layout == 2: + lbuf += c + else: + short += c + self.text_short = short + self.invalidate() + + def shorten_layout(self, text): + text = text.split(' ') + incl = [] + for word in text: + if word not in ('ImageButtonDeco', 'Maximize', 'Minimize'): + incl.append(word) + return ' '.join(incl).replace('Mirror ', '\\') + + def switch_workspace(self, ws): + G.display.xtest_fake_input(Xlib.X.KeyPress, G.display.keysym_to_keycode(self.mod)) + G.display.xtest_fake_input(Xlib.X.KeyPress, G.display.keysym_to_keycode(Xlib.XK.XK_0 + ws)) + G.display.xtest_fake_input(Xlib.X.KeyRelease, G.display.keysym_to_keycode(Xlib.XK.XK_0 + ws)) + G.display.xtest_fake_input(Xlib.X.KeyRelease, G.display.keysym_to_keycode(self.mod)) + G.display.flush() + + def function(self): + return self.text if self.show_full else self.text_short diff --git a/xpybar/config/xmonad-monitor.gpp b/xpybar/config/xmonad-monitor.gpp new file mode 100644 index 0000000..337e661 --- /dev/null +++ b/xpybar/config/xmonad-monitor.gpp @@ -0,0 +1,222 @@ +# -*- python -*- + +import os +import sys + + +from plugins.application import Application +from plugins.clock import Clock +from plugins.cpuinfo import CPUInfo +from plugins.hdparm import HDParm +from plugins.image import Image +from plugins.kmsg import KMsg +from plugins.loadavg import AverageLoad +from plugins.lunar import Lunar +from plugins.menu import Menu +from plugins.ropty import ROPTY +from plugins.solar import Solar + +sys.path.append(os.path.dirname(config_file) if '/' in config_file else '.') +from common import * + +G.globals = globals() +G.clock = Clock(sync_to = 0.5) +%%>if test -x /usr/bin/terminator; then +G.TERMINAL = 'terminator' +%%>elif test -x /usr/bin/st; then +G.TERMINAL = 'st' +%%>else +G.TERMINAL = 'xterm' +%%>fi + +G.OUTPUT, G.HEIGHT_PER_LINE, G.YPOS, G.TOP = 0, 12, 0, True +G.FONT = '-misc-fixed-medium-r-normal-*-10-*-*-*-c-*-iso10646-1' + +from myalsa import MyALSA +from mybacklight import MyBacklight +from mybattery import MyBattery +from mybrilliance import MyBrilliance +from myclock import MyClock +from mycomputer import MyComputer +from mycpu import MyCPU +from myio import MyIO +from myipaddress import MyIPAddress +from myirc import MyIRC +from mylid import MyLid +from myleapsec import MyLeapsec +from mymemory import MyMemory +from mymoc import MyMOC +from mynetwork import MyNetwork, Ping +from mynews import MyNews +from myscroll import MyScroll +from mystat import MyStat +from mysun import MySun +from mytimer import MyTimer +from mytop import MyTop +from myweather import MyWeather +from myxmonad import MyXMonad + + +def mqueue_wait(): + import posix_ipc + qkey = '/.xpybar.' + os.environ['DISPLAY'].split('.')[0] + q = posix_ipc.MessageQueue(qkey, posix_ipc.O_CREAT, 0o600, 8, 128) + while True: + try: + message = q.receive(None)[0].decode('utf-8', 'replace').split(' ') + if message[0] in mqueue_map: + try: + mqueue_map[message[0]](message) + except Exception as err: + print('%s: %s' % (sys.argv[0], str(err)), file = sys.stderr, flush = True) + else: + print('%s: unrecognised message: %s' % (sys.argv[0], ' '.join(message)), file = sys.stderr, flush = True) + except: + time.sleep(1) + + + +myxmonad = MyXMonad(None) +myscroll = MyScroll(None) +myclock = MyClock (lambda f : Clocked(f, 1), format = '%Y-(%m)%b-%d %T, %a w%V, %Z', long_format = '%Y-%m-%d %T') +mixers = ['Master', 'PCM'] ## TODO +#mixers.append(('Headphone', 'Speaker')) +#myii = ... +#myirc = ... +#from plugins.ii import II +#from myii import MyII +#def irc_watch(s): +# s = s.lower() +# return any(map(lambda x : x in s, ['mattias andrée', 'maandree', 'mandree', 'maandre', 'mandre'])) +#ii = II('irc.oftc.net/#suckless', prefix = HOME + '/.irc') +#myii = MyII(None, channel = 'irc.oftc.net/#suckless', prefix = HOME + '/.irc', watch = irc_watch) +#myirc = MyIRC(None, ii = myii) +#myii.display = myirc +mylid = ... +mybattery = ... +%%>if test -d /proc/acpi/button/lid; then +mylid = MyLid (None) +%%>fi +%%>if test ! $(ls /sys/class/power_supply/ | wc -l) = 0; then +mybattery = MyBattery(None) +%%>fi + +pingthese = [] +%%>if test -r ~/.dotfiles/.secrets/ping-"$(hostname | tr '[[:upper:]]' '[[:lower:]]')"; then +%%> for address in $(cat ~/.dotfiles/.secrets/ping-"$(hostname | tr '[[:upper:]]' '[[:lower:]]')"); do +pingthese.append(Ping(targets = Ping.get_nics('%%{address}'), interval = 30)) +%%> done +%%>fi + +try: + with open(HOME + '/.config/metar', 'rb') as file: + metar_stations = file.read().decode('utf-8', 'strict').split('\n') +except: + raise + try: + with open('/etc/metar', 'rb') as file: + metar_stations = file.read().decode('utf-8', 'strict').split('\n') + except: + metar_stations = [] +metar_stations = [x[0].upper() + x[1:].lower() for x in metar_stations if x != ''] + +functions = [ [ myxmonad + , None + , MyTimer (None, alarms = []) + , MyALSA (None, mixers = mixers, colours = {'Speaker' : '31'}) + , MyComputer (lambda f : Clocked(f, 20)) + , myscroll + , None + , myclock + , MyCPU (lambda f : Clocked(f, 2)) + , MyMemory (lambda f : Clocked(f, 2)) + , None + , MyNetwork (lambda f : Clocked(f, 2), pings = pingthese) + #, myirc +%%>if test -x /usr/bin/featherweight; then + , MyNews (None) +%%>fi + , MyWeather (None, stations = metar_stations) if metar_stations else ... + #, MySun (None, clock = myclock) + ] + , [ myxmonad + , None + , MyIPAddress (lambda f : Clocked(f, 20), public = False) + , MyMOC (None) + , myscroll + , None + , myclock + , mylid + , mybattery + #, myii + , None + , MyStat (lambda f : Clocked(f, 10)) + , MyBrilliance(None) + #, MyBacklight (None) + #, MyIO (lambda f : Clocked(f, 10), fs_ignore = []) + ] + ] + +functions = [[mon for mon in group if mon != ...] for group in functions] + +G.HEIGHT = 0 +G.groups = [Group(f) for f in functions] +G.groupi = 0 +G.group = G.groups[G.groupi] +G.semaphore = threading.Semaphore() + +def update_per_clock(): + if G.semaphore.acquire(blocking = False): + try: + for g in G.groups: + for f in g.functions: + if isinstance(f.wrapped, Clocked): + f(True) + finally: + G.semaphore.release() + invalidate() + +start_ = start +def start(): + start_() + #if myii is not ...: + # myii.start() + bar.clear() + get_display().flush() + xasync(lambda : G.clock.continuous_sync(t(update_per_clock)), name = 'clock') + xasync(mqueue_wait, name = 'mqueue') + +def redraw(): + if G.semaphore.acquire(blocking = False): + try: + gr = G.groups[G.groupi] + for g in G.groups: + if g is not gr: + for f in g.functions: + f() + values = gr.pattern % tuple(f() for f in gr.functions) + bar.partial_clear(0, bar.width, 10, 0, 2, values) + bar.draw_coloured_splitted_text(0, bar.width, 10, 0, 2, values) + finally: + G.semaphore.release() + return True + return False + +def unhandled_event(e): + if isinstance(e, Xlib.protocol.event.ButtonPress): + y = e.event_y + x = e.event_x + row = y // HEIGHT_PER_LINE + lcol = x // bar.font_width + rcol = (bar.width - x) // bar.font_width + button = e.detail + if button in (FORWARD_BUTTON, BACKWARD_BUTTON): + G.groupi = (G.groupi + (+1 if button == BACKWARD_BUTTON else -1)) % len(G.groups) + G.group = G.groups[G.groupi] + invalidate() + else: + for f in G.group.posupdate: + f.update_position() + for f in G.group.functions: + if f.click(row, lcol, rcol, button, x, y): + break |