diff --git a/docs/conf.py b/docs/conf.py index cda1e9e..c29cb33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,6 +30,7 @@ MOCK_MODULES = [ "i3pystatus.pulseaudio.pulse", "notmuch", "requests", + "bs4" ] for mod_name in MOCK_MODULES: diff --git a/i3pystatus/core/util.py b/i3pystatus/core/util.py index c3b093e..098644c 100644 --- a/i3pystatus/core/util.py +++ b/i3pystatus/core/util.py @@ -439,7 +439,16 @@ def user_open(url_or_command): scheme = urlparse(url_or_command).scheme if scheme == 'http' or scheme == 'https': import webbrowser - webbrowser.open(url_or_command) + import os + # webbrowser.open() sometimes prints a message for some reason and confuses i3 + # Redirect stdout briefly to prevent this from happening. + savout = os.dup(1) + os.close(1) + os.open(os.devnull, os.O_RDWR) + try: + webbrowser.open(url_or_command) + finally: + os.dup2(savout, 1) else: import subprocess subprocess.Popen(url_or_command, shell=True) diff --git a/i3pystatus/github.py b/i3pystatus/github.py new file mode 100644 index 0000000..a40c306 --- /dev/null +++ b/i3pystatus/github.py @@ -0,0 +1,59 @@ +from i3pystatus import IntervalModule +import requests +import json +from i3pystatus.core import ConfigError +from i3pystatus.core.util import user_open, internet, require + + +class Github(IntervalModule): + """ + Check Github for pending notifications. + Requires `requests` + + Formatters: + + * `{unread}` - contains the value of unread_marker when there are pending notifications + * `{unread_count}` - number of unread notifications, empty if 0 + """ + + unread_marker = u"●" + unread = '' + color = '#78EAF2' + username = '' + password = '' + format = '{unread}' + interval = 600 + + on_leftclick = 'open_github' + + settings = ( + ('format', 'format string'), + ('unread_marker', 'sets the string that the "unread" formatter shows when there are pending notifications'), + ("username", ""), + ("password", ""), + ("color", "") + ) + + def open_github(self): + user_open('https://github.com/' + self.username) + + @require(internet) + def run(self): + format_values = dict(unread_count='', unread='') + + response = requests.get('https://api.github.com/notifications', auth=(self.username, self.password)) + data = json.loads(response.text) + + # Bad credentials + if isinstance(data, dict): + raise ConfigError(data['message']) + + unread = len(data) + if unread > 0: + format_values['unread_count'] = unread + format_values['unread'] = self.unread_marker + + self.output = { + 'full_text': self.format.format(**format_values), + 'color': self.color + } diff --git a/i3pystatus/network.py b/i3pystatus/network.py index cca478a..9d021e9 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- import netifaces -import basiciw -import psutil from i3pystatus import IntervalModule from i3pystatus.core.color import ColorRangeModule from i3pystatus.core.util import make_graph, round_dict, make_bar @@ -74,7 +72,7 @@ class NetworkInfo(): Retrieve network information. """ - def __init__(self, interface, ignore_interfaces, detached_down, unknown_up): + def __init__(self, interface, ignore_interfaces, detached_down, unknown_up, get_wifi_info=False): if interface not in netifaces.interfaces() and not detached_down: raise RuntimeError( "Unknown interface {iface}!".format(iface=interface)) @@ -82,6 +80,7 @@ class NetworkInfo(): self.ignore_interfaces = ignore_interfaces self.detached_down = detached_down self.unknown_up = unknown_up + self.get_wifi_info = get_wifi_info def get_info(self, interface): format_dict = dict(v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="") @@ -133,9 +132,15 @@ class NetworkInfo(): break return info - @staticmethod - def extract_wireless_info(interface): + def extract_wireless_info(self, interface): info = dict(essid="", freq="", quality=0.0, quality_bar="") + + # Just return empty values if we're not using any Wifi functionality + if not self.get_wifi_info: + return info + + import basiciw + try: iwi = basiciw.iwinfo(interface) except Exception: @@ -169,6 +174,8 @@ class NetworkTraffic(): self.round_size = round_size def update_counters(self, interface): + import psutil + self.pnic_before = self.pnic counters = psutil.net_io_counters(pernic=True) self.pnic = counters[interface] if interface in counters else None @@ -282,24 +289,41 @@ class Network(IntervalModule, ColorRangeModule): on_leftclick = "nm-connection-editor" on_rightclick = "cycle_interface" + on_upscroll = ['cycle_interface', 1] + on_downscroll = ['cycle_interface', -1] def init(self): - self.network_traffic = NetworkTraffic(self.unknown_up, self.divisor, self.round_size) - self.network_info = NetworkInfo(self.interface, self.ignore_interfaces, self.detached_down, self.unknown_up) + # Don't require importing basiciw unless using the functionality it offers. + if any(s in self.format_up or s in self.format_up for s in + ['essid', 'freq', 'quality', 'quality_bar']): + get_wifi_info = True + else: + get_wifi_info = False + + self.network_info = NetworkInfo(self.interface, self.ignore_interfaces, self.detached_down, self.unknown_up, + get_wifi_info) + + # Don't require importing psutil unless using the functionality it offers. + if any(s in self.format_up or s in self.format_down for s in + ['bytes_sent', 'bytes_recv', 'packets_sent', 'packets_recv', 'network_graph']): + self.network_traffic = NetworkTraffic(self.unknown_up, self.divisor, self.round_size) + else: + self.network_traffic = None self.colors = self.get_hex_color_range(self.start_color, self.end_color, int(self.upper_limit)) self.kbs_arr = [0.0] * self.graph_width - def cycle_interface(self): + def cycle_interface(self, increment=1): interfaces = [i for i in netifaces.interfaces() if i not in self.ignore_interfaces] if self.interface in interfaces: - next_index = (interfaces.index(self.interface) + 1) % len(interfaces) + next_index = (interfaces.index(self.interface) + increment) % len(interfaces) self.interface = interfaces[next_index] elif len(interfaces) > 0: self.interface = interfaces[0] - self.network_traffic.clear_counters() - self.kbs_arr = [0.0] * self.graph_width + if self.network_traffic: + self.network_traffic.clear_counters() + self.kbs_arr = [0.0] * self.graph_width def get_network_graph(self, kbs): # Cycle array by inserting at the start and chopping off the last element @@ -311,28 +335,27 @@ class Network(IntervalModule, ColorRangeModule): format_values = dict(kbs="", network_graph="", bytes_sent="", bytes_recv="", packets_sent="", packets_recv="", interface="", v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="", mac="", essid="", freq="", quality="", quality_bar="") + color = None + if self.network_traffic: + network_usage = self.network_traffic.get_usage(self.interface) + format_values.update(network_usage) + if self.graph_type == 'input': + kbs = network_usage['bytes_recv'] + elif self.graph_type == 'output': + kbs = network_usage['bytes_sent'] + else: + raise Exception("graph_type must be either 'input' or 'output'!") - network_usage = self.network_traffic.get_usage(self.interface) - format_values.update(network_usage) + format_values['network_graph'] = self.get_network_graph(kbs) + format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6) + color = self.get_gradient(kbs, self.colors, self.upper_limit) network_info = self.network_info.get_info(self.interface) format_values.update(network_info) - if self.graph_type == 'input': - kbs = network_usage['bytes_recv'] - elif self.graph_type == 'output': - kbs = network_usage['bytes_sent'] - else: - raise Exception("graph_type must be either 'input' or 'output'!") - - format_values['network_graph'] = self.get_network_graph(kbs) - format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6) format_values['interface'] = self.interface - if sysfs_interface_up(self.interface, self.unknown_up): - if self.dynamic_color: - color = self.get_gradient(kbs, self.colors, self.upper_limit) - else: + if not self.dynamic_color: color = self.color_up self.output = { diff --git a/i3pystatus/reddit.py b/i3pystatus/reddit.py index b5a0f73..f448ecd 100644 --- a/i3pystatus/reddit.py +++ b/i3pystatus/reddit.py @@ -62,7 +62,6 @@ class Reddit(IntervalModule): } on_leftclick = "open_permalink" - on_click = "open_link" _permalink = "" _url = "" @@ -136,6 +135,9 @@ class Reddit(IntervalModule): "color": color, } + def open_mail(self): + user_open('https://www.reddit.com/message/unread/') + def open_permalink(self): user_open(self._permalink) diff --git a/i3pystatus/whosonlocation.py b/i3pystatus/whosonlocation.py new file mode 100644 index 0000000..dad46dc --- /dev/null +++ b/i3pystatus/whosonlocation.py @@ -0,0 +1,107 @@ +from i3pystatus import IntervalModule +import requests +from collections import OrderedDict +from bs4 import BeautifulSoup + + +class WhosOnLocation(): + + email = None + password = None + session = None + + def __init__(self, email, password): + self.email = email + self.password = password + self.session = requests.Session() + + def login(self): + login_details = {'email_input': self.email, + 'password_input': self.password, + '_redirect_url': '', + 'continue_submit': 'Login'} + r = self.session.post('https://login.whosonlocation.com/login', data=login_details) + return r.url == 'https://au.whosonlocation.com/home?justloggedin=true' + + def get_status(self): + r = self.session.get('https://au.whosonlocation.com/home?justloggedin=true') + html = BeautifulSoup(r.content) + status = html.body.find("span", {"class": "my-status-name"}) + if status: + return status.text + + def on_site(self): + return self.__change_status('onsite') + + def off_site(self): + return self.__change_status('offsite') + + def __change_status(self, status): + r = self.session.post('https://au.whosonlocation.com/ajax/changestatus', data={'status': status}) + return r.json() + + # _type can be org or location + def search(self, keyword, _type='location'): + payload = {'keyword': keyword, 'type': _type} + r = self.session.get('https://au.whosonlocation.com/home/search', params=payload) + return self.__parse_results(BeautifulSoup(r.content)) + + @staticmethod + def __parse_results(page): + titles = ['Name', 'Title', 'Department', 'Current Location', 'Home Location'] + table = page.body.find_all("tr", {"class": "dataRow"}) + results = [] + for row in table: + values = [v.string for v in row.findAll('td', {'class': 'truncate'})] + results.append(OrderedDict(zip(titles, values))) + return results + + +class WOL(IntervalModule): + """ + Change your whosonlocation.com status. + + Requires the PyPi module `beautifulsoup4` + """ + location = None + email = None + password = None + + settings = ( + 'email', + 'password' + ) + + color_on_site = '#00FF00' + color_off_site = '#ff0000' + format = 'Status: {status}' + status = None + + on_leftclick = 'change_status' + + def init(self): + self.location = WhosOnLocation(self.email, self.password) + if not self.location.login(): + raise Exception("Failed to login") + + def change_status(self): + if self.status == 'On-Site': + self.location.off_site() + elif self.status == 'Off-Site': + self.location.on_site() + + def run(self): + self.status = self.location.get_status() + color = None + + if self.status == 'Off-Site': + color = self.color_off_site + elif self.status == 'On-Site': + color = self.color_on_site + + self.output = { + "full_text": self.format.format( + status=self.status + ), + "color": color + }