summaryrefslogtreecommitdiffstats
path: root/powerline/segments/common
diff options
context:
space:
mode:
Diffstat (limited to 'powerline/segments/common')
-rw-r--r--powerline/segments/common/__init__.py0
-rw-r--r--powerline/segments/common/bat.py302
-rw-r--r--powerline/segments/common/env.py201
-rw-r--r--powerline/segments/common/mail.py78
-rw-r--r--powerline/segments/common/net.py315
-rw-r--r--powerline/segments/common/players.py636
-rw-r--r--powerline/segments/common/sys.py184
-rw-r--r--powerline/segments/common/time.py123
-rw-r--r--powerline/segments/common/vcs.py89
-rw-r--r--powerline/segments/common/wthr.py234
10 files changed, 2162 insertions, 0 deletions
diff --git a/powerline/segments/common/__init__.py b/powerline/segments/common/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/powerline/segments/common/__init__.py
diff --git a/powerline/segments/common/bat.py b/powerline/segments/common/bat.py
new file mode 100644
index 0000000..c892f62
--- /dev/null
+++ b/powerline/segments/common/bat.py
@@ -0,0 +1,302 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import os
+import sys
+import re
+
+from powerline.lib.shell import run_cmd
+
+
+def _fetch_battery_info(pl):
+ try:
+ import dbus
+ except ImportError:
+ pl.debug('Not using DBUS+UPower as dbus is not available')
+ else:
+ try:
+ bus = dbus.SystemBus()
+ except Exception as e:
+ pl.exception('Failed to connect to system bus: {0}', str(e))
+ else:
+ interface = 'org.freedesktop.UPower'
+ try:
+ up = bus.get_object(interface, '/org/freedesktop/UPower')
+ except dbus.exceptions.DBusException as e:
+ if getattr(e, '_dbus_error_name', '').endswith('ServiceUnknown'):
+ pl.debug('Not using DBUS+UPower as UPower is not available via dbus')
+ else:
+ pl.exception('Failed to get UPower service with dbus: {0}', str(e))
+ else:
+ devinterface = 'org.freedesktop.DBus.Properties'
+ devtype_name = interface + '.Device'
+ devices = []
+ for devpath in up.EnumerateDevices(dbus_interface=interface):
+ dev = bus.get_object(interface, devpath)
+ devget = lambda what: dev.Get(
+ devtype_name,
+ what,
+ dbus_interface=devinterface
+ )
+ if int(devget('Type')) != 2:
+ pl.debug('Not using DBUS+UPower with {0}: invalid type', devpath)
+ continue
+ if not bool(devget('IsPresent')):
+ pl.debug('Not using DBUS+UPower with {0}: not present', devpath)
+ continue
+ if not bool(devget('PowerSupply')):
+ pl.debug('Not using DBUS+UPower with {0}: not a power supply', devpath)
+ continue
+ devices.append(devpath)
+ pl.debug('Using DBUS+UPower with {0}', devpath)
+ if devices:
+ def _flatten_battery(pl):
+ energy = 0.0
+ energy_full = 0.0
+ state = True
+ for devpath in devices:
+ dev = bus.get_object(interface, devpath)
+ energy_full += float(
+ dbus.Interface(dev, dbus_interface=devinterface).Get(
+ devtype_name,
+ 'EnergyFull'
+ ),
+ )
+ energy += float(
+ dbus.Interface(dev, dbus_interface=devinterface).Get(
+ devtype_name,
+ 'Energy'
+ ),
+ )
+ state &= dbus.Interface(dev, dbus_interface=devinterface).Get(
+ devtype_name,
+ 'State'
+ ) != 2
+ if energy_full > 0:
+ return (energy * 100.0 / energy_full), state
+ else:
+ return 0.0, state
+ return _flatten_battery
+ pl.debug('Not using DBUS+UPower as no batteries were found')
+
+ if os.path.isdir('/sys/class/power_supply'):
+ # ENERGY_* attributes represents capacity in µWh only.
+ # CHARGE_* attributes represents capacity in µAh only.
+ linux_capacity_units = ('energy', 'charge')
+ linux_energy_full_fmt = '/sys/class/power_supply/{0}/{1}_full'
+ linux_energy_fmt = '/sys/class/power_supply/{0}/{1}_now'
+ linux_status_fmt = '/sys/class/power_supply/{0}/status'
+ devices = []
+ for linux_supplier in os.listdir('/sys/class/power_supply'):
+ for unit in linux_capacity_units:
+ energy_path = linux_energy_fmt.format(linux_supplier, unit)
+ if not os.path.exists(energy_path):
+ continue
+ pl.debug('Using /sys/class/power_supply with battery {0} and unit {1}',
+ linux_supplier, unit)
+ devices.append((linux_supplier, unit))
+ break # energy or charge, not both
+ if devices:
+ def _get_battery_status(pl):
+ energy = 0.0
+ energy_full = 0.0
+ state = True
+ for device, unit in devices:
+ with open(linux_energy_full_fmt.format(device, unit), 'r') as f:
+ energy_full += int(float(f.readline().split()[0]))
+ with open(linux_energy_fmt.format(device, unit), 'r') as f:
+ energy += int(float(f.readline().split()[0]))
+ try:
+ with open(linux_status_fmt.format(device), 'r') as f:
+ state &= (f.readline().strip() != 'Discharging')
+ except IOError:
+ state = None
+ return (energy * 100.0 / energy_full), state
+ return _get_battery_status
+ pl.debug('Not using /sys/class/power_supply as no batteries were found')
+ else:
+ pl.debug("Checking for first capacity battery percentage")
+ for batt in os.listdir('/sys/class/power_supply'):
+ if os.path.exists('/sys/class/power_supply/{0}/capacity'.format(batt)):
+ def _get_battery_perc(pl):
+ state = True
+ with open('/sys/class/power_supply/{0}/capacity'.format(batt), 'r') as f:
+ perc = int(f.readline().split()[0])
+ try:
+ with open(linux_status_fmt.format(batt), 'r') as f:
+ state &= (f.readline().strip() != 'Discharging')
+ except IOError:
+ state = None
+ return perc, state
+ return _get_battery_perc
+ else:
+ pl.debug('Not using /sys/class/power_supply: no directory')
+
+ try:
+ from shutil import which # Python-3.3 and later
+ except ImportError:
+ pl.info('Using dumb “which” which only checks for file in /usr/bin')
+ which = lambda f: (lambda fp: os.path.exists(fp) and fp)(os.path.join('/usr/bin', f))
+
+ if which('pmset'):
+ pl.debug('Using pmset')
+
+ BATTERY_PERCENT_RE = re.compile(r'(\d+)%')
+
+ def _get_battery_status(pl):
+ battery_summary = run_cmd(pl, ['pmset', '-g', 'batt'])
+ battery_percent = BATTERY_PERCENT_RE.search(battery_summary).group(1)
+ ac_charging = 'AC' in battery_summary
+ return int(battery_percent), ac_charging
+ return _get_battery_status
+ else:
+ pl.debug('Not using pmset: executable not found')
+
+ if sys.platform.startswith('win') or sys.platform == 'cygwin':
+ # From http://stackoverflow.com/a/21083571/273566, reworked
+ try:
+ from win32com.client import GetObject
+ except ImportError:
+ pl.debug('Not using win32com.client as it is not available')
+ else:
+ try:
+ wmi = GetObject('winmgmts:')
+ except Exception as e:
+ pl.exception('Failed to run GetObject from win32com.client: {0}', str(e))
+ else:
+ for battery in wmi.InstancesOf('Win32_Battery'):
+ pl.debug('Using win32com.client with Win32_Battery')
+
+ def _get_battery_status(pl):
+ # http://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx
+ return battery.EstimatedChargeRemaining, battery.BatteryStatus == 6
+
+ return _get_battery_status
+ pl.debug('Not using win32com.client as no batteries were found')
+ from ctypes import Structure, c_byte, c_ulong, byref
+ if sys.platform == 'cygwin':
+ pl.debug('Using cdll to communicate with kernel32 (Cygwin)')
+ from ctypes import cdll
+ library_loader = cdll
+ else:
+ pl.debug('Using windll to communicate with kernel32 (Windows)')
+ from ctypes import windll
+ library_loader = windll
+
+ class PowerClass(Structure):
+ _fields_ = [
+ ('ACLineStatus', c_byte),
+ ('BatteryFlag', c_byte),
+ ('BatteryLifePercent', c_byte),
+ ('Reserved1', c_byte),
+ ('BatteryLifeTime', c_ulong),
+ ('BatteryFullLifeTime', c_ulong)
+ ]
+
+ def _get_battery_status(pl):
+ powerclass = PowerClass()
+ result = library_loader.kernel32.GetSystemPowerStatus(byref(powerclass))
+ # http://msdn.microsoft.com/en-us/library/windows/desktop/aa372693(v=vs.85).aspx
+ if result:
+ return None
+ return powerclass.BatteryLifePercent, powerclass.ACLineStatus == 1
+
+ if _get_battery_status() is None:
+ pl.debug('Not using GetSystemPowerStatus because it failed')
+ else:
+ pl.debug('Using GetSystemPowerStatus')
+
+ return _get_battery_status
+
+ raise NotImplementedError
+
+
+def _get_battery_status(pl):
+ global _get_battery_status
+
+ def _failing_get_status(pl):
+ raise NotImplementedError
+
+ try:
+ _get_battery_status = _fetch_battery_info(pl)
+ except NotImplementedError:
+ _get_battery_status = _failing_get_status
+ except Exception as e:
+ pl.exception('Exception while obtaining battery status: {0}', str(e))
+ _get_battery_status = _failing_get_status
+ return _get_battery_status(pl)
+
+
+def battery(pl, format='{ac_state} {capacity:3.0%}', steps=5, gamify=False, full_heart='O', empty_heart='O', online='C', offline=' '):
+ '''Return battery charge status.
+
+ :param str format:
+ Percent format in case gamify is False. Format arguments: ``ac_state``
+ which is equal to either ``online`` or ``offline`` string arguments and
+ ``capacity`` which is equal to current battery capacity in interval [0,
+ 100].
+ :param int steps:
+ Number of discrete steps to show between 0% and 100% capacity if gamify
+ is True.
+ :param bool gamify:
+ Measure in hearts (♥) instead of percentages. For full hearts
+ ``battery_full`` highlighting group is preferred, for empty hearts there
+ is ``battery_empty``. ``battery_online`` or ``battery_offline`` group
+ will be used for leading segment containing ``online`` or ``offline``
+ argument contents.
+ :param str full_heart:
+ Heart displayed for “full” part of battery.
+ :param str empty_heart:
+ Heart displayed for “used” part of battery. It is also displayed using
+ another gradient level and highlighting group, so it is OK for it to be
+ the same as full_heart as long as necessary highlighting groups are
+ defined.
+ :param str online:
+ Symbol used if computer is connected to a power supply.
+ :param str offline:
+ Symbol used if computer is not connected to a power supply.
+
+ ``battery_gradient`` and ``battery`` groups are used in any case, first is
+ preferred.
+
+ Highlight groups used: ``battery_full`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_empty`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_online`` or ``battery_ac_state`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_offline`` or ``battery_ac_state`` or ``battery_gradient`` (gradient) or ``battery``.
+ '''
+ try:
+ capacity, ac_powered = _get_battery_status(pl)
+ except NotImplementedError:
+ pl.info('Unable to get battery status.')
+ return None
+
+ ret = []
+ if gamify:
+ denom = int(steps)
+ numer = int(denom * capacity / 100)
+ ret.append({
+ 'contents': online if ac_powered else offline,
+ 'draw_inner_divider': False,
+ 'highlight_groups': ['battery_online' if ac_powered else 'battery_offline', 'battery_ac_state', 'battery_gradient', 'battery'],
+ 'gradient_level': 0,
+ })
+ ret.append({
+ 'contents': full_heart * numer,
+ 'draw_inner_divider': False,
+ 'highlight_groups': ['battery_full', 'battery_gradient', 'battery'],
+ # Using zero as “nothing to worry about”: it is least alert color.
+ 'gradient_level': 0,
+ })
+ ret.append({
+ 'contents': empty_heart * (denom - numer),
+ 'draw_inner_divider': False,
+ 'highlight_groups': ['battery_empty', 'battery_gradient', 'battery'],
+ # Using a hundred as it is most alert color.
+ 'gradient_level': 100,
+ })
+ else:
+ ret.append({
+ 'contents': format.format(ac_state=(online if ac_powered else offline), capacity=(capacity / 100.0)),
+ 'highlight_groups': ['battery_gradient', 'battery'],
+ # Gradients are “least alert – most alert” by default, capacity has
+ # the opposite semantics.
+ 'gradient_level': 100 - capacity,
+ })
+ return ret
diff --git a/powerline/segments/common/env.py b/powerline/segments/common/env.py
new file mode 100644
index 0000000..bbfe3e2
--- /dev/null
+++ b/powerline/segments/common/env.py
@@ -0,0 +1,201 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import os
+
+from powerline.lib.unicode import out_u
+from powerline.theme import requires_segment_info
+from powerline.segments import Segment, with_docstring
+
+
+@requires_segment_info
+def environment(pl, segment_info, variable=None):
+ '''Return the value of any defined environment variable
+
+ :param string variable:
+ The environment variable to return if found
+ '''
+ return segment_info['environ'].get(variable, None)
+
+
+@requires_segment_info
+def virtualenv(pl, segment_info, ignore_venv=False, ignore_conda=False, ignored_names=("venv", ".venv")):
+ '''Return the name of the current Python or conda virtualenv.
+ :param list ignored_names:
+ Names of venvs to ignore. Will then get the name of the venv by ascending to the parent directory
+ :param bool ignore_venv:
+ Whether to ignore virtual environments. Default is False.
+ :param bool ignore_conda:
+ Whether to ignore conda environments. Default is False.
+ '''
+ if not ignore_venv:
+ for candidate in reversed(segment_info['environ'].get('VIRTUAL_ENV', '').split("/")):
+ if candidate and candidate not in ignored_names:
+ return candidate
+ if not ignore_conda:
+ for candidate in reversed(segment_info['environ'].get('CONDA_DEFAULT_ENV', '').split("/")):
+ if candidate and candidate not in ignored_names:
+ return candidate
+ return None
+
+
+@requires_segment_info
+class CwdSegment(Segment):
+ def argspecobjs(self):
+ for obj in super(CwdSegment, self).argspecobjs():
+ yield obj
+ yield 'get_shortened_path', self.get_shortened_path
+
+ def omitted_args(self, name, method):
+ if method is self.get_shortened_path:
+ return ()
+ else:
+ return super(CwdSegment, self).omitted_args(name, method)
+
+ def get_shortened_path(self, pl, segment_info, shorten_home=True, **kwargs):
+ try:
+ path = out_u(segment_info['getcwd']())
+ except OSError as e:
+ if e.errno == 2:
+ # user most probably deleted the directory
+ # this happens when removing files from Mercurial repos for example
+ pl.warn('Current directory not found')
+ return '[not found]'
+ else:
+ raise
+ if shorten_home:
+ home = segment_info['home']
+ if home:
+ home = out_u(home)
+ if path.startswith(home):
+ path = '~' + path[len(home):]
+ return path
+
+ def __call__(self, pl, segment_info,
+ dir_shorten_len=None,
+ dir_limit_depth=None,
+ use_path_separator=False,
+ ellipsis='...',
+ **kwargs):
+ cwd = self.get_shortened_path(pl, segment_info, **kwargs)
+ cwd_split = cwd.split(os.sep)
+ cwd_split_len = len(cwd_split)
+ cwd = [i[0:dir_shorten_len] if dir_shorten_len and i else i for i in cwd_split[:-1]] + [cwd_split[-1]]
+ if dir_limit_depth and cwd_split_len > dir_limit_depth + 1:
+ del(cwd[0:-dir_limit_depth])
+ if ellipsis is not None:
+ cwd.insert(0, ellipsis)
+ ret = []
+ if not cwd[0]:
+ cwd[0] = '/'
+ draw_inner_divider = not use_path_separator
+ for part in cwd:
+ if not part:
+ continue
+ if use_path_separator:
+ part += os.sep
+ ret.append({
+ 'contents': part,
+ 'divider_highlight_group': 'cwd:divider',
+ 'draw_inner_divider': draw_inner_divider,
+ })
+ ret[-1]['highlight_groups'] = ['cwd:current_folder', 'cwd']
+ if use_path_separator:
+ ret[-1]['contents'] = ret[-1]['contents'][:-1]
+ if len(ret) > 1 and ret[0]['contents'][0] == os.sep:
+ ret[0]['contents'] = ret[0]['contents'][1:]
+ return ret
+
+
+cwd = with_docstring(CwdSegment(),
+'''Return the current working directory.
+
+Returns a segment list to create a breadcrumb-like effect.
+
+:param int dir_shorten_len:
+ shorten parent directory names to this length (e.g.
+ :file:`/long/path/to/powerline` → :file:`/l/p/t/powerline`)
+:param int dir_limit_depth:
+ limit directory depth to this number (e.g.
+ :file:`/long/path/to/powerline` → :file:`⋯/to/powerline`)
+:param bool use_path_separator:
+ Use path separator in place of soft divider.
+:param bool shorten_home:
+ Shorten home directory to ``~``.
+:param str ellipsis:
+ Specifies what to use in place of omitted directories. Use None to not
+ show this subsegment at all.
+
+Divider highlight group used: ``cwd:divider``.
+
+Highlight groups used: ``cwd:current_folder`` or ``cwd``. It is recommended to define all highlight groups.
+''')
+
+
+try:
+ import psutil
+
+ # psutil-2.0.0: psutil.Process.username is unbound method
+ if callable(psutil.Process.username):
+ def _get_user():
+ return psutil.Process(os.getpid()).username()
+ # pre psutil-2.0.0: psutil.Process.username has type property
+ else:
+ def _get_user():
+ return psutil.Process(os.getpid()).username
+except ImportError:
+ try:
+ import pwd
+ except ImportError:
+ from getpass import getuser as _get_user
+ else:
+ try:
+ from os import geteuid as getuid
+ except ImportError:
+ from os import getuid
+
+ def _get_user():
+ return pwd.getpwuid(getuid()).pw_name
+
+
+username = False
+# os.geteuid is not available on windows
+_geteuid = getattr(os, 'geteuid', lambda: 1)
+
+
+@requires_segment_info
+def user(pl, segment_info, hide_user=None, hide_domain=False):
+ '''Return the current user.
+
+ :param str hide_user:
+ Omit showing segment for users with names equal to this string.
+ :param bool hide_domain:
+ Drop domain component if it exists in a username (delimited by '@').
+
+ Highlights the user with the ``superuser`` if the effective user ID is 0.
+
+ Highlight groups used: ``superuser`` or ``user``. It is recommended to define all highlight groups.
+ '''
+ global username
+ if (
+ segment_info['environ'].get('_POWERLINE_RUNNING_SHELL_TESTS')
+ == 'ee5bcdc6-b749-11e7-9456-50465d597777'
+ ):
+ return 'user'
+ if username is False:
+ username = _get_user()
+ if username is None:
+ pl.warn('Failed to get username')
+ return None
+ if username == hide_user:
+ return None
+ if hide_domain:
+ try:
+ username = username[:username.index('@')]
+ except ValueError:
+ pass
+ euid = _geteuid()
+ return [{
+ 'contents': username,
+ 'highlight_groups': ['user'] if euid != 0 else ['superuser', 'user'],
+ }]
diff --git a/powerline/segments/common/mail.py b/powerline/segments/common/mail.py
new file mode 100644
index 0000000..8202492
--- /dev/null
+++ b/powerline/segments/common/mail.py
@@ -0,0 +1,78 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import re
+
+from imaplib import IMAP4_SSL_PORT, IMAP4_SSL, IMAP4
+from collections import namedtuple
+
+from powerline.lib.threaded import KwThreadedSegment
+from powerline.segments import with_docstring
+
+
+_IMAPKey = namedtuple('Key', 'username password server port folder use_ssl')
+
+
+class EmailIMAPSegment(KwThreadedSegment):
+ interval = 60
+
+ @staticmethod
+ def key(username, password, server='imap.gmail.com', port=IMAP4_SSL_PORT, folder='INBOX', use_ssl=None, **kwargs):
+ if use_ssl is None:
+ use_ssl = (port == IMAP4_SSL_PORT)
+ return _IMAPKey(username, password, server, port, folder, use_ssl)
+
+ def compute_state(self, key):
+ if not key.username or not key.password:
+ self.warn('Username and password are not configured')
+ return None
+ if key.use_ssl:
+ mail = IMAP4_SSL(key.server, key.port)
+ else:
+ mail = IMAP4(key.server, key.port)
+ mail.login(key.username, key.password)
+ rc, message = mail.status(key.folder, '(UNSEEN)')
+ unread_str = message[0].decode('utf-8')
+ unread_count = int(re.search('UNSEEN (\d+)', unread_str).group(1))
+ return unread_count
+
+ @staticmethod
+ def render_one(unread_count, max_msgs=None, **kwargs):
+ if not unread_count:
+ return None
+ elif type(unread_count) != int or not max_msgs:
+ return [{
+ 'contents': str(unread_count),
+ 'highlight_groups': ['email_alert'],
+ }]
+ else:
+ return [{
+ 'contents': str(unread_count),
+ 'highlight_groups': ['email_alert_gradient', 'email_alert'],
+ 'gradient_level': min(unread_count * 100.0 / max_msgs, 100),
+ }]
+
+
+email_imap_alert = with_docstring(EmailIMAPSegment(),
+('''Return unread e-mail count for IMAP servers.
+
+:param str username:
+ login username
+:param str password:
+ login password
+:param str server:
+ e-mail server
+:param int port:
+ e-mail server port
+:param str folder:
+ folder to check for e-mails
+:param int max_msgs:
+ Maximum number of messages. If there are more messages then max_msgs then it
+ will use gradient level equal to 100, otherwise gradient level is equal to
+ ``100 * msgs_num / max_msgs``. If not present gradient is not computed.
+:param bool use_ssl:
+ If ``True`` then use SSL connection. If ``False`` then do not use it.
+ Default is ``True`` if port is equal to {ssl_port} and ``False`` otherwise.
+
+Highlight groups used: ``email_alert_gradient`` (gradient), ``email_alert``.
+''').format(ssl_port=IMAP4_SSL_PORT))
diff --git a/powerline/segments/common/net.py b/powerline/segments/common/net.py
new file mode 100644
index 0000000..b5d9062
--- /dev/null
+++ b/powerline/segments/common/net.py
@@ -0,0 +1,315 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import re
+import os
+import socket
+
+from powerline.lib.url import urllib_read
+from powerline.lib.threaded import ThreadedSegment, KwThreadedSegment
+from powerline.lib.monotonic import monotonic
+from powerline.lib.humanize_bytes import humanize_bytes
+from powerline.segments import with_docstring
+from powerline.theme import requires_segment_info
+
+
+@requires_segment_info
+def hostname(pl, segment_info, only_if_ssh=False, exclude_domain=False):
+ '''Return the current hostname.
+
+ :param bool only_if_ssh:
+ only return the hostname if currently in an SSH session
+ :param bool exclude_domain:
+ return the hostname without domain if there is one
+ '''
+ if (
+ segment_info['environ'].get('_POWERLINE_RUNNING_SHELL_TESTS')
+ == 'ee5bcdc6-b749-11e7-9456-50465d597777'
+ ):
+ return 'hostname'
+ if only_if_ssh and not segment_info['environ'].get('SSH_CLIENT'):
+ return None
+ if exclude_domain:
+ return socket.gethostname().split('.')[0]
+ return socket.gethostname()
+
+
+def _external_ip(query_url='http://ipv4.icanhazip.com/'):
+ return urllib_read(query_url).strip()
+
+
+class ExternalIpSegment(ThreadedSegment):
+ interval = 300
+
+ def set_state(self, query_url='http://ipv4.icanhazip.com/', **kwargs):
+ self.query_url = query_url
+ super(ExternalIpSegment, self).set_state(**kwargs)
+
+ def update(self, old_ip):
+ return _external_ip(query_url=self.query_url)
+
+ def render(self, ip, **kwargs):
+ if not ip:
+ return None
+ return [{'contents': ip, 'divider_highlight_group': 'background:divider'}]
+
+
+external_ip = with_docstring(ExternalIpSegment(),
+'''Return external IP address.
+
+:param str query_url:
+ URI to query for IP address, should return only the IP address as a text string
+
+ Suggested URIs:
+
+ * http://ipv4.icanhazip.com/
+ * http://ipv6.icanhazip.com/
+ * http://icanhazip.com/ (returns IPv6 address if available, else IPv4)
+
+Divider highlight group used: ``background:divider``.
+''')
+
+
+try:
+ import netifaces
+except ImportError:
+ def internal_ip(pl, interface='auto', ipv=4):
+ return None
+else:
+ _interface_starts = {
+ 'eth': 10, # Regular ethernet adapters : eth1
+ 'enp': 10, # Regular ethernet adapters, Gentoo : enp2s0
+ 'en': 10, # OS X : en0
+ 'ath': 9, # Atheros WiFi adapters : ath0
+ 'wlan': 9, # Other WiFi adapters : wlan1
+ 'wlp': 9, # Other WiFi adapters, Gentoo : wlp5s0
+ 'teredo': 1, # miredo interface : teredo
+ 'lo': -10, # Loopback interface : lo
+ 'docker': -5, # Docker bridge interface : docker0
+ 'vmnet': -5, # VMWare bridge interface : vmnet1
+ 'vboxnet': -5, # VirtualBox bridge interface : vboxnet0
+ }
+
+ _interface_start_re = re.compile(r'^([a-z]+?)(\d|$)')
+
+ def _interface_key(interface):
+ match = _interface_start_re.match(interface)
+ if match:
+ try:
+ base = _interface_starts[match.group(1)] * 100
+ except KeyError:
+ base = 500
+ if match.group(2):
+ return base - int(match.group(2))
+ else:
+ return base
+ else:
+ return 0
+
+ def internal_ip(pl, interface='auto', ipv=4):
+ family = netifaces.AF_INET6 if ipv == 6 else netifaces.AF_INET
+ if interface == 'auto':
+ try:
+ interface = next(iter(sorted(netifaces.interfaces(), key=_interface_key, reverse=True)))
+ except StopIteration:
+ pl.info('No network interfaces found')
+ return None
+ elif interface == 'default_gateway':
+ try:
+ interface = netifaces.gateways()['default'][family][1]
+ except KeyError:
+ pl.info('No default gateway found for IPv{0}', ipv)
+ return None
+ addrs = netifaces.ifaddresses(interface)
+ try:
+ return addrs[family][0]['addr']
+ except (KeyError, IndexError):
+ pl.info("No IPv{0} address found for interface {1}", ipv, interface)
+ return None
+
+
+internal_ip = with_docstring(internal_ip,
+'''Return internal IP address
+
+Requires ``netifaces`` module to work properly.
+
+:param str interface:
+ Interface on which IP will be checked. Use ``auto`` to automatically
+ detect interface. In this case interfaces with lower numbers will be
+ preferred over interfaces with similar names. Order of preference based on
+ names:
+
+ #. ``eth`` and ``enp`` followed by number or the end of string.
+ #. ``ath``, ``wlan`` and ``wlp`` followed by number or the end of string.
+ #. ``teredo`` followed by number or the end of string.
+ #. Any other interface that is not ``lo*``.
+ #. ``lo`` followed by number or the end of string.
+
+ Use ``default_gateway`` to detect the interface based on the machine's
+ `default gateway <https://en.wikipedia.org/wiki/Default_gateway>`_ (i.e.,
+ the router to which it is connected).
+
+:param int ipv:
+ 4 or 6 for ipv4 and ipv6 respectively, depending on which IP address you
+ need exactly.
+''')
+
+
+try:
+ import psutil
+
+ def _get_bytes(interface):
+ try:
+ io_counters = psutil.net_io_counters(pernic=True)
+ except AttributeError:
+ io_counters = psutil.network_io_counters(pernic=True)
+ if_io = io_counters.get(interface)
+ if not if_io:
+ return None
+ return if_io.bytes_recv, if_io.bytes_sent
+
+ def _get_interfaces():
+ try:
+ io_counters = psutil.net_io_counters(pernic=True)
+ except AttributeError:
+ io_counters = psutil.network_io_counters(pernic=True)
+ for interface, data in io_counters.items():
+ if data:
+ yield interface, data.bytes_recv, data.bytes_sent
+except ImportError:
+ def _get_bytes(interface):
+ with open('/sys/class/net/{interface}/statistics/rx_bytes'.format(interface=interface), 'rb') as file_obj:
+ rx = int(file_obj.read())
+ with open('/sys/class/net/{interface}/statistics/tx_bytes'.format(interface=interface), 'rb') as file_obj:
+ tx = int(file_obj.read())
+ return (rx, tx)
+
+ def _get_interfaces():
+ for interface in os.listdir('/sys/class/net'):
+ x = _get_bytes(interface)
+ if x is not None:
+ yield interface, x[0], x[1]
+
+
+class NetworkLoadSegment(KwThreadedSegment):
+ interfaces = {}
+ replace_num_pat = re.compile(r'[a-zA-Z]+')
+
+ @staticmethod
+ def key(interface='auto', **kwargs):
+ return interface
+
+ def compute_state(self, interface):
+ if interface == 'auto':
+ proc_exists = getattr(self, 'proc_exists', None)
+ if proc_exists is None:
+ proc_exists = self.proc_exists = os.path.exists('/proc/net/route')
+ if proc_exists:
+ # Look for default interface in routing table
+ with open('/proc/net/route', 'rb') as f:
+ for line in f.readlines():
+ parts = line.split()
+ if len(parts) > 1:
+ iface, destination = parts[:2]
+ if not destination.replace(b'0', b''):
+ interface = iface.decode('utf-8')
+ break
+ if interface == 'auto':
+ # Choose interface with most total activity, excluding some
+ # well known interface names
+ interface, total = 'eth0', -1
+ for name, rx, tx in _get_interfaces():
+ base = self.replace_num_pat.match(name)
+ if None in (base, rx, tx) or base.group() in ('lo', 'vmnet', 'sit'):
+ continue
+ activity = rx + tx
+ if activity > total:
+ total = activity
+ interface = name
+
+ try:
+ idata = self.interfaces[interface]
+ try:
+ idata['prev'] = idata['last']
+ except KeyError:
+ pass
+ except KeyError:
+ idata = {}
+ if self.run_once:
+ idata['prev'] = (monotonic(), _get_bytes(interface))
+ self.shutdown_event.wait(self.interval)
+ self.interfaces[interface] = idata
+
+ idata['last'] = (monotonic(), _get_bytes(interface))
+ return idata.copy()
+
+ def render_one(self, idata, recv_format='DL {value:>8}', sent_format='UL {value:>8}', suffix='B/s', si_prefix=False, **kwargs):
+ if not idata or 'prev' not in idata:
+ return None
+
+ t1, b1 = idata['prev']
+ t2, b2 = idata['last']
+ measure_interval = t2 - t1
+
+ if None in (b1, b2):
+ return None
+
+ r = []
+ for i, key in zip((0, 1), ('recv', 'sent')):
+ format = locals()[key + '_format']
+ try:
+ value = (b2[i] - b1[i]) / measure_interval
+ except ZeroDivisionError:
+ self.warn('Measure interval zero.')
+ value = 0
+ max_key = key + '_max'
+ is_gradient = max_key in kwargs
+ hl_groups = ['network_load_' + key, 'network_load']
+ if is_gradient:
+ hl_groups[:0] = (group + '_gradient' for group in hl_groups)
+ r.append({
+ 'contents': format.format(value=humanize_bytes(value, suffix, si_prefix)),
+ 'divider_highlight_group': 'network_load:divider',
+ 'highlight_groups': hl_groups,
+ })
+ if is_gradient:
+ max = kwargs[max_key]
+ if value >= max:
+ r[-1]['gradient_level'] = 100
+ else:
+ r[-1]['gradient_level'] = value * 100.0 / max
+
+ return r
+
+
+network_load = with_docstring(NetworkLoadSegment(),
+'''Return the network load.
+
+Uses the ``psutil`` module if available for multi-platform compatibility,
+falls back to reading
+:file:`/sys/class/net/{interface}/statistics/{rx,tx}_bytes`.
+
+:param str interface:
+ Network interface to measure (use the special value "auto" to have powerline
+ try to auto-detect the network interface).
+:param str suffix:
+ String appended to each load string.
+:param bool si_prefix:
+ Use SI prefix, e.g. MB instead of MiB.
+:param str recv_format:
+ Format string that determines how download speed should look like. Receives
+ ``value`` as argument.
+:param str sent_format:
+ Format string that determines how upload speed should look like. Receives
+ ``value`` as argument.
+:param float recv_max:
+ Maximum number of received bytes per second. Is only used to compute
+ gradient level.
+:param float sent_max:
+ Maximum number of sent bytes per second. Is only used to compute gradient
+ level.
+
+Divider highlight group used: ``network_load:divider``.
+
+Highlight groups used: ``network_load_sent_gradient`` (gradient) or ``network_load_recv_gradient`` (gradient) or ``network_load_gradient`` (gradient), ``network_load_sent`` or ``network_load_recv`` or ``network_load``.
+''')
diff --git a/powerline/segments/common/players.py b/powerline/segments/common/players.py
new file mode 100644
index 0000000..f43db0c
--- /dev/null
+++ b/powerline/segments/common/players.py
@@ -0,0 +1,636 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import sys
+import re
+
+from powerline.lib.shell import asrun, run_cmd
+from powerline.lib.unicode import out_u
+from powerline.segments import Segment, with_docstring
+
+
+STATE_SYMBOLS = {
+ 'fallback': '',
+ 'play': '>',
+ 'pause': '~',
+ 'stop': 'X',
+}
+
+
+def _convert_state(state):
+ '''Guess player state'''
+ state = state.lower()
+ if 'play' in state:
+ return 'play'
+ if 'pause' in state:
+ return 'pause'
+ if 'stop' in state:
+ return 'stop'
+ return 'fallback'
+
+
+def _convert_seconds(seconds):
+ '''Convert seconds to minutes:seconds format'''
+ if isinstance(seconds, str):
+ seconds = seconds.replace(",",".")
+ return '{0:.0f}:{1:02.0f}'.format(*divmod(float(seconds), 60))
+
+
+class PlayerSegment(Segment):
+ def __call__(self, format='{state_symbol} {artist} - {title} ({total})', state_symbols=STATE_SYMBOLS, **kwargs):
+ stats = {
+ 'state': 'fallback',
+ 'album': None,
+ 'artist': None,
+ 'title': None,
+ 'elapsed': None,
+ 'total': None,
+ }
+ func_stats = self.get_player_status(**kwargs)
+ if not func_stats:
+ return None
+ stats.update(func_stats)
+ stats['state_symbol'] = state_symbols.get(stats['state'])
+ return [{
+ 'contents': format.format(**stats),
+ 'highlight_groups': ['player_' + (stats['state'] or 'fallback'), 'player'],
+ }]
+
+ def get_player_status(self, pl):
+ pass
+
+ def argspecobjs(self):
+ for ret in super(PlayerSegment, self).argspecobjs():
+ yield ret
+ yield 'get_player_status', self.get_player_status
+
+ def omitted_args(self, name, method):
+ return ()
+
+
+_common_args = '''
+This player segment should be added like this:
+
+.. code-block:: json
+
+ {{
+ "function": "powerline.segments.common.players.{0}",
+ "name": "player"
+ }}
+
+(with additional ``"args": {{…}}`` if needed).
+
+Highlight groups used: ``player_fallback`` or ``player``, ``player_play`` or ``player``, ``player_pause`` or ``player``, ``player_stop`` or ``player``.
+
+:param str format:
+ Format used for displaying data from player. Should be a str.format-like
+ string with the following keyword parameters:
+
+ +------------+-------------------------------------------------------------+
+ |Parameter |Description |
+ +============+=============================================================+
+ |state_symbol|Symbol displayed for play/pause/stop states. There is also |
+ | |“fallback” state used in case function failed to get player |
+ | |state. For this state symbol is by default empty. All |
+ | |symbols are defined in ``state_symbols`` argument. |
+ +------------+-------------------------------------------------------------+
+ |album |Album that is currently played. |
+ +------------+-------------------------------------------------------------+
+ |artist |Artist whose song is currently played |
+ +------------+-------------------------------------------------------------+
+ |title |Currently played composition. |
+ +------------+-------------------------------------------------------------+
+ |elapsed |Composition duration in format M:SS (minutes:seconds). |
+ +------------+-------------------------------------------------------------+
+ |total |Composition length in format M:SS. |
+ +------------+-------------------------------------------------------------+
+:param dict state_symbols:
+ Symbols used for displaying state. Must contain all of the following keys:
+
+ ======== ========================================================
+ Key Description
+ ======== ========================================================
+ play Displayed when player is playing.
+ pause Displayed when player is paused.
+ stop Displayed when player is not playing anything.
+ fallback Displayed if state is not one of the above or not known.
+ ======== ========================================================
+'''
+
+
+_player = with_docstring(PlayerSegment(), _common_args.format('_player'))
+
+
+class CmusPlayerSegment(PlayerSegment):
+ def get_player_status(self, pl):
+ '''Return cmus player information.
+
+ cmus-remote -Q returns data with multi-level information i.e.
+ status playing
+ file <file_name>
+ tag artist <artist_name>
+ tag title <track_title>
+ tag ..
+ tag n
+ set continue <true|false>
+ set repeat <true|false>
+ set ..
+ set n
+
+ For the information we are looking for we don’t really care if we’re on
+ the tag level or the set level. The dictionary comprehension in this
+ method takes anything in ignore_levels and brings the key inside that
+ to the first level of the dictionary.
+ '''
+ now_playing_str = run_cmd(pl, ['cmus-remote', '-Q'])
+ if not now_playing_str:
+ return
+ ignore_levels = ('tag', 'set',)
+ now_playing = dict(((token[0] if token[0] not in ignore_levels else token[1],
+ (' '.join(token[1:]) if token[0] not in ignore_levels else
+ ' '.join(token[2:]))) for token in [line.split(' ') for line in now_playing_str.split('\n')[:-1]]))
+ state = _convert_state(now_playing.get('status'))
+ return {
+ 'state': state,
+ 'album': now_playing.get('album'),
+ 'artist': now_playing.get('artist'),
+ 'title': now_playing.get('title'),
+ 'elapsed': _convert_seconds(now_playing.get('position', 0)),
+ 'total': _convert_seconds(now_playing.get('duration', 0)),
+ }
+
+
+cmus = with_docstring(CmusPlayerSegment(),
+('''Return CMUS player information
+
+Requires cmus-remote command be accessible from $PATH.
+
+{0}
+''').format(_common_args.format('cmus')))
+
+
+class MpdPlayerSegment(PlayerSegment):
+ def get_player_status(self, pl, host='localhost', password=None, port=6600):
+ try:
+ import mpd
+ except ImportError:
+ if password:
+ host = password + '@' + host
+ now_playing = run_cmd(pl, [
+ 'mpc',
+ '-h', host,
+ '-p', str(port)
+ ], strip=False)
+ album = run_cmd(pl, [
+ 'mpc', 'current',
+ '-f', '%album%',
+ '-h', host,
+ '-p', str(port)
+ ])
+ if not now_playing or now_playing.count("\n") != 3:
+ return
+ now_playing = re.match(
+ r"(.*) - (.*)\n\[([a-z]+)\] +[#0-9\/]+ +([0-9\:]+)\/([0-9\:]+)",
+ now_playing
+ )
+ return {
+ 'state': _convert_state(now_playing[3]),
+ 'album': album,
+ 'artist': now_playing[1],
+ 'title': now_playing[2],
+ 'elapsed': now_playing[4],
+ 'total': now_playing[5]
+ }
+ else:
+ try:
+ client = mpd.MPDClient(use_unicode=True)
+ except TypeError:
+ # python-mpd 1.x does not support use_unicode
+ client = mpd.MPDClient()
+ client.connect(host, port)
+ if password:
+ client.password(password)
+ now_playing = client.currentsong()
+ if not now_playing:
+ return
+ status = client.status()
+ client.close()
+ client.disconnect()
+ return {
+ 'state': status.get('state'),
+ 'album': now_playing.get('album'),
+ 'artist': now_playing.get('artist'),
+ 'title': now_playing.get('title'),
+ 'elapsed': _convert_seconds(status.get('elapsed', 0)),
+ 'total': _convert_seconds(now_playing.get('time', 0)),
+ }
+
+
+mpd = with_docstring(MpdPlayerSegment(),
+('''Return Music Player Daemon information
+
+Requires ``mpd`` Python module (e.g. |python-mpd2|_ or |python-mpd|_ Python
+package) or alternatively the ``mpc`` command to be accessible from $PATH.
+
+.. |python-mpd| replace:: ``python-mpd``
+.. _python-mpd: https://pypi.python.org/pypi/python-mpd
+
+.. |python-mpd2| replace:: ``python-mpd2``
+.. _python-mpd2: https://pypi.python.org/pypi/python-mpd2
+
+{0}
+:param str host:
+ Host on which mpd runs.
+:param str password:
+ Password used for connecting to daemon.
+:param int port:
+ Port which should be connected to.
+''').format(_common_args.format('mpd')))
+
+
+try:
+ import dbus
+except ImportError:
+ def _get_dbus_player_status(pl, player_name, **kwargs):
+ pl.error('Could not add {0} segment: requires dbus module', player_name)
+ return
+else:
+ def _get_dbus_player_status(pl,
+ bus_name=None,
+ iface_prop='org.freedesktop.DBus.Properties',
+ iface_player='org.mpris.MediaPlayer2.Player',
+ player_path='/org/mpris/MediaPlayer2',
+ player_name='player'):
+ bus = dbus.SessionBus()
+
+ if bus_name is None:
+ for service in bus.list_names():
+ if re.match('org.mpris.MediaPlayer2.', service):
+ bus_name = service
+ break
+
+ try:
+ player = bus.get_object(bus_name, player_path)
+ iface = dbus.Interface(player, iface_prop)
+ info = iface.Get(iface_player, 'Metadata')
+ status = iface.Get(iface_player, 'PlaybackStatus')
+ except dbus.exceptions.DBusException:
+ return
+ if not info:
+ return
+
+ try:
+ elapsed = iface.Get(iface_player, 'Position')
+ except dbus.exceptions.DBusException:
+ pl.warning('Missing player elapsed time')
+ elapsed = None
+ else:
+ elapsed = _convert_seconds(elapsed / 1e6)
+ album = info.get('xesam:album')
+ title = info.get('xesam:title')
+ artist = info.get('xesam:artist')
+ state = _convert_state(status)
+ if album:
+ album = out_u(album)
+ if title:
+ title = out_u(title)
+ if artist:
+ artist = out_u(artist[0])
+
+ length = info.get('mpris:length')
+ # avoid parsing `None` length values, that would
+ # raise an error otherwise
+ parsed_length = length and _convert_seconds(length / 1e6)
+
+ return {
+ 'state': state,
+ 'album': album,
+ 'artist': artist,
+ 'title': title,
+ 'elapsed': elapsed,
+ 'total': parsed_length,
+ }
+
+
+class DbusPlayerSegment(PlayerSegment):
+ get_player_status = staticmethod(_get_dbus_player_status)
+
+
+dbus_player = with_docstring(DbusPlayerSegment(),
+('''Return generic dbus player state
+
+Requires ``dbus`` python module. Only for players that support specific protocol
+ (e.g. like :py:func:`spotify` and :py:func:`clementine`).
+
+{0}
+:param str player_name:
+ Player name. Used in error messages only.
+:param str bus_name:
+ Dbus bus name.
+:param str player_path:
+ Path to the player on the given bus.
+:param str iface_prop:
+ Interface properties name for use with dbus.Interface.
+:param str iface_player:
+ Player name.
+''').format(_common_args.format('dbus_player')))
+
+
+class SpotifyDbusPlayerSegment(PlayerSegment):
+ def get_player_status(self, pl):
+ player_status = _get_dbus_player_status(
+ pl=pl,
+ player_name='Spotify',
+ bus_name='org.mpris.MediaPlayer2.spotify',
+ player_path='/org/mpris/MediaPlayer2',
+ iface_prop='org.freedesktop.DBus.Properties',
+ iface_player='org.mpris.MediaPlayer2.Player',
+ )
+ if player_status is not None:
+ return player_status
+ # Fallback for legacy spotify client with different DBus protocol
+ return _get_dbus_player_status(
+ pl=pl,
+ player_name='Spotify',
+ bus_name='com.spotify.qt',
+ player_path='/',
+ iface_prop='org.freedesktop.DBus.Properties',
+ iface_player='org.freedesktop.MediaPlayer2',
+ )
+
+
+spotify_dbus = with_docstring(SpotifyDbusPlayerSegment(),
+('''Return spotify player information
+
+Requires ``dbus`` python module.
+
+{0}
+''').format(_common_args.format('spotify_dbus')))
+
+
+class SpotifyAppleScriptPlayerSegment(PlayerSegment):
+ def get_player_status(self, pl):
+ status_delimiter = '-~`/='
+ ascript = '''
+ tell application "System Events"
+ set process_list to (name of every process)
+ end tell
+
+ if process_list contains "Spotify" then
+ tell application "Spotify"
+ if player state is playing or player state is paused then
+ set track_name to name of current track
+ set artist_name to artist of current track
+ set album_name to album of current track
+ set track_length to duration of current track
+ set now_playing to "" & player state & "{0}" & album_name & "{0}" & artist_name & "{0}" & track_name & "{0}" & track_length & "{0}" & player position
+ return now_playing
+ else
+ return player state
+ end if
+
+ end tell
+ else
+ return "stopped"
+ end if
+ '''.format(status_delimiter)
+
+ spotify = asrun(pl, ascript)
+ if not asrun:
+ return None
+
+ spotify_status = spotify.split(status_delimiter)
+ state = _convert_state(spotify_status[0])
+ if state == 'stop':
+ return None
+ return {
+ 'state': state,
+ 'album': spotify_status[1],
+ 'artist': spotify_status[2],
+ 'title': spotify_status[3],
+ 'total': _convert_seconds(int(spotify_status[4])/1000),
+ 'elapsed': _convert_seconds(spotify_status[5]),
+ }
+
+
+spotify_apple_script = with_docstring(SpotifyAppleScriptPlayerSegment(),
+('''Return spotify player information
+
+Requires ``osascript`` available in $PATH.
+
+{0}
+''').format(_common_args.format('spotify_apple_script')))
+
+
+if not sys.platform.startswith('darwin'):
+ spotify = spotify_dbus
+ _old_name = 'spotify_dbus'
+else:
+ spotify = spotify_apple_script
+ _old_name = 'spotify_apple_script'
+
+
+spotify = with_docstring(spotify, spotify.__doc__.replace(_old_name, 'spotify'))
+
+
+class ClementinePlayerSegment(PlayerSegment):
+ def get_player_status(self, pl):
+ return _get_dbus_player_status(
+ pl=pl,
+ player_name='Clementine',
+ bus_name='org.mpris.MediaPlayer2.clementine',
+ player_path='/org/mpris/MediaPlayer2',
+ iface_prop='org.freedesktop.DBus.Properties',
+ iface_player='org.mpris.MediaPlayer2.Player',
+ )
+
+
+clementine = with_docstring(ClementinePlayerSegment(),
+('''Return clementine player information
+
+Requires ``dbus`` python module.
+
+{0}
+''').format(_common_args.format('clementine')))
+
+
+class RhythmboxPlayerSegment(PlayerSegment):
+ def get_player_status(self, pl):
+ now_playing = run_cmd(pl, [
+ 'rhythmbox-client',
+ '--no-start', '--no-present',
+ '--print-playing-format', '%at\n%aa\n%tt\n%te\n%td'
+ ], strip=False)
+ if not now_playing:
+ return
+ now_playing = now_playing.split('\n')
+ return {
+ 'album': now_playing[0],
+ 'artist': now_playing[1],
+ 'title': now_playing[2],
+ 'elapsed': now_playing[3],
+ 'total': now_playing[4],
+ }
+
+
+rhythmbox = with_docstring(RhythmboxPlayerSegment(),
+('''Return rhythmbox player information
+
+Requires ``rhythmbox-client`` available in $PATH.
+
+{0}
+''').format(_common_args.format('rhythmbox')))
+
+
+class RDIOPlayerSegment(PlayerSegment):
+ def get_player_status(self, pl):
+ status_delimiter = '-~`/='
+ ascript = '''
+ tell application "System Events"
+ set rdio_active to the count(every process whose name is "Rdio")
+ if rdio_active is 0 then
+ return
+ end if
+ end tell
+ tell application "Rdio"
+ set rdio_name to the name of the current track
+ set rdio_artist to the artist of the current track
+ set rdio_album to the album of the current track
+ set rdio_duration to the duration of the current track
+ set rdio_state to the player state
+ set rdio_elapsed to the player position
+ return rdio_name & "{0}" & rdio_artist & "{0}" & rdio_album & "{0}" & rdio_elapsed & "{0}" & rdio_duration & "{0}" & rdio_state
+ end tell
+ '''.format(status_delimiter)
+ now_playing = asrun(pl, ascript)
+ if not now_playing:
+ return
+ now_playing = now_playing.split(status_delimiter)
+ if len(now_playing) != 6:
+ return
+ state = _convert_state(now_playing[5])
+ total = _convert_seconds(now_playing[4])
+ elapsed = _convert_seconds(float(now_playing[3]) * float(now_playing[4]) / 100)
+ return {
+ 'title': now_playing[0],
+ 'artist': now_playing[1],
+ 'album': now_playing[2],
+ 'elapsed': elapsed,
+ 'total': total,
+ 'state': state,
+ }
+
+
+rdio = with_docstring(RDIOPlayerSegment(),
+('''Return rdio player information
+
+Requires ``osascript`` available in $PATH.
+
+{0}
+''').format(_common_args.format('rdio')))
+
+
+class ITunesPlayerSegment(PlayerSegment):
+ def get_player_status(self, pl):
+ status_delimiter = '-~`/='
+ ascript = '''
+ tell application "System Events"
+ set process_list to (name of every process)
+ end tell
+
+ if process_list contains "iTunes" then
+ tell application "iTunes"
+ if player state is playing then
+ set t_title to name of current track
+ set t_artist to artist of current track
+ set t_album to album of current track
+ set t_duration to duration of current track
+ set t_elapsed to player position
+ set t_state to player state
+ return t_title & "{0}" & t_artist & "{0}" & t_album & "{0}" & t_elapsed & "{0}" & t_duration & "{0}" & t_state
+ end if
+ end tell
+ end if
+ '''.format(status_delimiter)
+ now_playing = asrun(pl, ascript)
+ if not now_playing:
+ return
+ now_playing = now_playing.split(status_delimiter)
+ if len(now_playing) != 6:
+ return
+ title, artist, album = now_playing[0], now_playing[1], now_playing[2]
+ state = _convert_state(now_playing[5])
+ total = _convert_seconds(now_playing[4])
+ elapsed = _convert_seconds(now_playing[3])
+ return {
+ 'title': title,
+ 'artist': artist,
+ 'album': album,
+ 'total': total,
+ 'elapsed': elapsed,
+ 'state': state
+ }
+
+
+itunes = with_docstring(ITunesPlayerSegment(),
+('''Return iTunes now playing information
+
+Requires ``osascript``.
+
+{0}
+''').format(_common_args.format('itunes')))
+
+
+class MocPlayerSegment(PlayerSegment):
+ def get_player_status(self, pl):
+ '''Return Music On Console (mocp) player information.
+
+ ``mocp -i`` returns current information i.e.
+
+ .. code-block::
+
+ File: filename.format
+ Title: full title
+ Artist: artist name
+ SongTitle: song title
+ Album: album name
+ TotalTime: 00:00
+ TimeLeft: 00:00
+ TotalSec: 000
+ CurrentTime: 00:00
+ CurrentSec: 000
+ Bitrate: 000kbps
+ AvgBitrate: 000kbps
+ Rate: 00kHz
+
+ For the information we are looking for we don’t really care if we have
+ extra-timing information or bit rate level. The dictionary comprehension
+ in this method takes anything in ignore_info and brings the key inside
+ that to the right info of the dictionary.
+ '''
+ now_playing_str = run_cmd(pl, ['mocp', '-i'])
+ if not now_playing_str:
+ return
+
+ now_playing = dict((
+ line.split(': ', 1)
+ for line in now_playing_str.split('\n')[:-1]
+ ))
+ state = _convert_state(now_playing.get('State', 'stop'))
+ return {
+ 'state': state,
+ 'album': now_playing.get('Album', ''),
+ 'artist': now_playing.get('Artist', ''),
+ 'title': now_playing.get('SongTitle', ''),
+ 'elapsed': _convert_seconds(now_playing.get('CurrentSec', 0)),
+ 'total': _convert_seconds(now_playing.get('TotalSec', 0)),
+ }
+
+
+mocp = with_docstring(MocPlayerSegment(),
+('''Return MOC (Music On Console) player information
+
+Requires version >= 2.3.0 and ``mocp`` executable in ``$PATH``.
+
+{0}
+''').format(_common_args.format('mocp')))
diff --git a/powerline/segments/common/sys.py b/powerline/segments/common/sys.py
new file mode 100644
index 0000000..29a2459
--- /dev/null
+++ b/powerline/segments/common/sys.py
@@ -0,0 +1,184 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import os
+
+from multiprocessing import cpu_count as _cpu_count
+
+from powerline.lib.threaded import ThreadedSegment
+from powerline.lib import add_divider_highlight_group
+from powerline.segments import with_docstring
+
+
+cpu_count = None
+
+
+def system_load(pl, format='{avg:.1f}', threshold_good=1, threshold_bad=2,
+ track_cpu_count=False, short=False):
+ '''Return system load average.
+
+ Highlights using ``system_load_good``, ``system_load_bad`` and
+ ``system_load_ugly`` highlighting groups, depending on the thresholds
+ passed to the function.
+
+ :param str format:
+ format string, receives ``avg`` as an argument
+ :param float threshold_good:
+ threshold for gradient level 0: any normalized load average below this
+ value will have this gradient level.
+ :param float threshold_bad:
+ threshold for gradient level 100: any normalized load average above this
+ value will have this gradient level. Load averages between
+ ``threshold_good`` and ``threshold_bad`` receive gradient level that
+ indicates relative position in this interval:
+ (``100 * (cur-good) / (bad-good)``).
+ Note: both parameters are checked against normalized load averages.
+ :param bool track_cpu_count:
+ if True powerline will continuously poll the system to detect changes
+ in the number of CPUs.
+ :param bool short:
+ if True only the sys load over last 1 minute will be displayed.
+
+ Divider highlight group used: ``background:divider``.
+
+ Highlight groups used: ``system_load_gradient`` (gradient) or ``system_load``.
+ '''
+ global cpu_count
+ try:
+ cpu_num = cpu_count = _cpu_count() if cpu_count is None or track_cpu_count else cpu_count
+ except NotImplementedError:
+ pl.warn('Unable to get CPU count: method is not implemented')
+ return None
+ ret = []
+ for avg in os.getloadavg():
+ normalized = avg / cpu_num
+ if normalized < threshold_good:
+ gradient_level = 0
+ elif normalized < threshold_bad:
+ gradient_level = (normalized - threshold_good) * 100.0 / (threshold_bad - threshold_good)
+ else:
+ gradient_level = 100
+ ret.append({
+ 'contents': format.format(avg=avg),
+ 'highlight_groups': ['system_load_gradient', 'system_load'],
+ 'divider_highlight_group': 'background:divider',
+ 'gradient_level': gradient_level,
+ })
+
+ if short:
+ return ret
+
+ ret[0]['contents'] += ' '
+ ret[1]['contents'] += ' '
+ return ret
+
+
+try:
+ import psutil
+
+ class CPULoadPercentSegment(ThreadedSegment):
+ interval = 1
+
+ def update(self, old_cpu):
+ return psutil.cpu_percent(interval=None)
+
+ def run(self):
+ while not self.shutdown_event.is_set():
+ try:
+ self.update_value = psutil.cpu_percent(interval=self.interval)
+ except Exception as e:
+ self.exception('Exception while calculating cpu_percent: {0}', str(e))
+
+ def render(self, cpu_percent, format='{0:.0f}%', **kwargs):
+ return [{
+ 'contents': format.format(cpu_percent),
+ 'gradient_level': cpu_percent,
+ 'highlight_groups': ['cpu_load_percent_gradient', 'cpu_load_percent'],
+ }]
+except ImportError:
+ class CPULoadPercentSegment(ThreadedSegment):
+ interval = 1
+
+ @staticmethod
+ def startup(**kwargs):
+ pass
+
+ @staticmethod
+ def start():
+ pass
+
+ @staticmethod
+ def shutdown():
+ pass
+
+ @staticmethod
+ def render(cpu_percent, pl, format='{0:.0f}%', **kwargs):
+ pl.warn('Module “psutil” is not installed, thus CPU load is not available')
+ return None
+
+
+cpu_load_percent = with_docstring(CPULoadPercentSegment(),
+'''Return the average CPU load as a percentage.
+
+Requires the ``psutil`` module.
+
+:param str format:
+ Output format. Accepts measured CPU load as the first argument.
+
+Highlight groups used: ``cpu_load_percent_gradient`` (gradient) or ``cpu_load_percent``.
+''')
+
+
+if os.path.exists('/proc/uptime'):
+ def _get_uptime():
+ with open('/proc/uptime', 'r') as f:
+ return int(float(f.readline().split()[0]))
+elif 'psutil' in globals():
+ from time import time
+
+ if hasattr(psutil, 'boot_time'):
+ def _get_uptime():
+ return int(time() - psutil.boot_time())
+ else:
+ def _get_uptime():
+ return int(time() - psutil.BOOT_TIME)
+else:
+ def _get_uptime():
+ raise NotImplementedError
+
+
+@add_divider_highlight_group('background:divider')
+def uptime(pl, days_format='{days:d}d', hours_format=' {hours:d}h', minutes_format=' {minutes:02d}m',
+ seconds_format=' {seconds:02d}s', shorten_len=3):
+ '''Return system uptime.
+
+ :param str days_format:
+ day format string, will be passed ``days`` as the argument
+ :param str hours_format:
+ hour format string, will be passed ``hours`` as the argument
+ :param str minutes_format:
+ minute format string, will be passed ``minutes`` as the argument
+ :param str seconds_format:
+ second format string, will be passed ``seconds`` as the argument
+ :param int shorten_len:
+ shorten the amount of units (days, hours, etc.) displayed
+
+ Divider highlight group used: ``background:divider``.
+ '''
+ try:
+ seconds = _get_uptime()
+ except NotImplementedError:
+ pl.warn('Unable to get uptime. You should install psutil module')
+ return None
+ minutes, seconds = divmod(seconds, 60)
+ hours, minutes = divmod(minutes, 60)
+ days, hours = divmod(hours, 24)
+ time_formatted = list(filter(None, [
+ days_format.format(days=days) if days_format else None,
+ hours_format.format(hours=hours) if hours_format else None,
+ minutes_format.format(minutes=minutes) if minutes_format else None,
+ seconds_format.format(seconds=seconds) if seconds_format else None,
+ ]))
+ first_non_zero = next((i for i, x in enumerate([days, hours, minutes, seconds]) if x != 0))
+ time_formatted = time_formatted[first_non_zero:first_non_zero + shorten_len]
+ return ''.join(time_formatted).strip()
diff --git a/powerline/segments/common/time.py b/powerline/segments/common/time.py
new file mode 100644
index 0000000..be727c9
--- /dev/null
+++ b/powerline/segments/common/time.py
@@ -0,0 +1,123 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+from datetime import datetime
+
+
+def date(pl, format='%Y-%m-%d', istime=False, timezone=None):
+ '''Return the current date.
+
+ :param str format:
+ strftime-style date format string
+ :param bool istime:
+ If true then segment uses ``time`` highlight group.
+ :param string timezone:
+ Specify a timezone to use as ``+HHMM`` or ``-HHMM``.
+ (Defaults to system defaults.)
+
+ Divider highlight group used: ``time:divider``.
+
+ Highlight groups used: ``time`` or ``date``.
+ '''
+
+ try:
+ tz = datetime.strptime(timezone, '%z').tzinfo if timezone else None
+ except ValueError:
+ tz = None
+
+ nw = datetime.now(tz)
+
+ try:
+ contents = nw.strftime(format)
+ except UnicodeEncodeError:
+ contents = nw.strftime(format.encode('utf-8')).decode('utf-8')
+
+ return [{
+ 'contents': contents,
+ 'highlight_groups': (['time'] if istime else []) + ['date'],
+ 'divider_highlight_group': 'time:divider' if istime else None,
+ }]
+
+
+UNICODE_TEXT_TRANSLATION = {
+ ord('\''): '’',
+ ord('-'): '‐',
+}
+
+
+def fuzzy_time(pl, format='{minute_str} {hour_str}', unicode_text=False, timezone=None, hour_str=['twelve', 'one', 'two', 'three', 'four',
+ 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven'], minute_str = {
+ '0': 'o\'clock', '5': 'five past', '10': 'ten past','15': 'quarter past',
+ '20': 'twenty past', '25': 'twenty-five past', '30': 'half past', '35': 'twenty-five to',
+ '40': 'twenty to', '45': 'quarter to', '50': 'ten to', '55': 'five to'
+ }, special_case_str = {
+ '(23, 58)': 'round about midnight',
+ '(23, 59)': 'round about midnight',
+ '(0, 0)': 'midnight',
+ '(0, 1)': 'round about midnight',
+ '(0, 2)': 'round about midnight',
+ '(12, 0)': 'noon',
+ }):
+
+ '''Display the current time as fuzzy time, e.g. "quarter past six".
+
+ :param string format:
+ Format used to display the fuzzy time. (Ignored when a special time
+ is displayed.)
+ :param bool unicode_text:
+ If true then hyphenminuses (regular ASCII ``-``) and single quotes are
+ replaced with unicode dashes and apostrophes.
+ :param string timezone:
+ Specify a timezone to use as ``+HHMM`` or ``-HHMM``.
+ (Defaults to system defaults.)
+ :param string list hour_str:
+ Strings to be used to display the hour, starting with midnight.
+ (This list may contain 12 or 24 entries.)
+ :param dict minute_str:
+ Dictionary mapping minutes to strings to be used to display them.
+ :param dict special_case_str:
+ Special strings for special times.
+
+ Highlight groups used: ``fuzzy_time``.
+ '''
+
+ try:
+ tz = datetime.strptime(timezone, '%z').tzinfo if timezone else None
+ except ValueError:
+ tz = None
+
+ now = datetime.now(tz)
+
+ try:
+ # We don't want to enforce a special type of spaces/ alignment in the input
+ from ast import literal_eval
+ special_case_str = {literal_eval(x):special_case_str[x] for x in special_case_str}
+ result = special_case_str[(now.hour, now.minute)]
+ if unicode_text:
+ result = result.translate(UNICODE_TEXT_TRANSLATION)
+ return result
+ except KeyError:
+ pass
+
+ hour = now.hour
+ if now.minute >= 30:
+ hour = hour + 1
+ hour = hour % len(hour_str)
+
+ min_dis = 100
+ min_pos = 0
+
+ for mn in minute_str:
+ mn = int(mn)
+ if now.minute >= mn and now.minute - mn < min_dis:
+ min_dis = now.minute - mn
+ min_pos = mn
+ elif now.minute < mn and mn - now.minute < min_dis:
+ min_dis = mn - now.minute
+ min_pos = mn
+ result = format.format(minute_str=minute_str[str(min_pos)], hour_str=hour_str[hour])
+
+ if unicode_text:
+ result = result.translate(UNICODE_TEXT_TRANSLATION)
+
+ return result
diff --git a/powerline/segments/common/vcs.py b/powerline/segments/common/vcs.py
new file mode 100644
index 0000000..07679ae
--- /dev/null
+++ b/powerline/segments/common/vcs.py
@@ -0,0 +1,89 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+from powerline.lib.vcs import guess, tree_status
+from powerline.segments import Segment, with_docstring
+from powerline.theme import requires_segment_info, requires_filesystem_watcher
+
+
+@requires_filesystem_watcher
+@requires_segment_info
+class BranchSegment(Segment):
+ divider_highlight_group = None
+
+ @staticmethod
+ def get_directory(segment_info):
+ return segment_info['getcwd']()
+
+ def __call__(self, pl, segment_info, create_watcher, status_colors=False, ignore_statuses=()):
+ name = self.get_directory(segment_info)
+ if name:
+ repo = guess(path=name, create_watcher=create_watcher)
+ if repo is not None:
+ branch = repo.branch()
+ scol = ['branch']
+ if status_colors:
+ try:
+ status = tree_status(repo, pl)
+ except Exception as e:
+ pl.exception('Failed to compute tree status: {0}', str(e))
+ status = '?'
+ else:
+ status = status and status.strip()
+ if status in ignore_statuses:
+ status = None
+ scol.insert(0, 'branch_dirty' if status else 'branch_clean')
+ return [{
+ 'contents': branch,
+ 'highlight_groups': scol,
+ 'divider_highlight_group': self.divider_highlight_group,
+ }]
+
+
+branch = with_docstring(BranchSegment(),
+'''Return the current VCS branch.
+
+:param bool status_colors:
+ Determines whether repository status will be used to determine highlighting.
+ Default: False.
+:param list ignore_statuses:
+ List of statuses which will not result in repo being marked as dirty. Most
+ useful is setting this option to ``["U"]``: this will ignore repository
+ which has just untracked files (i.e. repository with modified, deleted or
+ removed files will be marked as dirty, while just untracked files will make
+ segment show clean repository). Only applicable if ``status_colors`` option
+ is True.
+
+Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``.
+''')
+
+
+@requires_filesystem_watcher
+@requires_segment_info
+class StashSegment(Segment):
+ divider_highlight_group = None
+
+ @staticmethod
+ def get_directory(segment_info):
+ return segment_info['getcwd']()
+
+ def __call__(self, pl, segment_info, create_watcher):
+ name = self.get_directory(segment_info)
+ if name:
+ repo = guess(path=name, create_watcher=create_watcher)
+ if repo is not None:
+ stash = getattr(repo, 'stash', None)
+ if stash:
+ stashes = stash()
+ if stashes:
+ return [{
+ 'contents': str(stashes),
+ 'highlight_groups': ['stash'],
+ 'divider_highlight_group': self.divider_highlight_group
+ }]
+
+stash = with_docstring(StashSegment(),
+'''Return the number of current VCS stash entries, if any.
+
+Highlight groups used: ``stash``.
+''')
diff --git a/powerline/segments/common/wthr.py b/powerline/segments/common/wthr.py
new file mode 100644
index 0000000..2c54cca
--- /dev/null
+++ b/powerline/segments/common/wthr.py
@@ -0,0 +1,234 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import json
+from collections import namedtuple
+
+from powerline.lib.url import urllib_read, urllib_urlencode
+from powerline.lib.threaded import KwThreadedSegment
+from powerline.segments import with_docstring
+
+
+_WeatherKey = namedtuple('Key', 'location_query weather_api_key')
+
+
+# XXX Warning: module name must not be equal to the segment name as long as this
+# segment is imported into powerline.segments.common module.
+
+
+# Weather condition code descriptions available at
+# https://openweathermap.org/weather-conditions
+weather_conditions_codes = {
+ 200: ('stormy',),
+ 201: ('stormy',),
+ 202: ('stormy',),
+ 210: ('stormy',),
+ 211: ('stormy',),
+ 212: ('stormy',),
+ 221: ('stormy',),
+ 230: ('stormy',),
+ 231: ('stormy',),
+ 232: ('stormy',),
+ 300: ('rainy',),
+ 301: ('rainy',),
+ 302: ('rainy',),
+ 310: ('rainy',),
+ 311: ('rainy',),
+ 312: ('rainy',),
+ 313: ('rainy',),
+ 314: ('rainy',),
+ 321: ('rainy',),
+ 500: ('rainy',),
+ 501: ('rainy',),
+ 502: ('rainy',),
+ 503: ('rainy',),
+ 504: ('rainy',),
+ 511: ('snowy',),
+ 520: ('rainy',),
+ 521: ('rainy',),
+ 522: ('rainy',),
+ 531: ('rainy',),
+ 600: ('snowy',),
+ 601: ('snowy',),
+ 602: ('snowy',),
+ 611: ('snowy',),
+ 612: ('snowy',),
+ 613: ('snowy',),
+ 615: ('snowy',),
+ 616: ('snowy',),
+ 620: ('snowy',),
+ 621: ('snowy',),
+ 622: ('snowy',),
+ 701: ('foggy',),
+ 711: ('foggy',),
+ 721: ('foggy',),
+ 731: ('foggy',),
+ 741: ('foggy',),
+ 751: ('foggy',),
+ 761: ('foggy',),
+ 762: ('foggy',),
+ 771: ('foggy',),
+ 781: ('foggy',),
+ 800: ('sunny',),
+ 801: ('cloudy',),
+ 802: ('cloudy',),
+ 803: ('cloudy',),
+ 804: ('cloudy',),
+}
+
+weather_conditions_icons = {
+ 'day': 'DAY',
+ 'blustery': 'WIND',
+ 'rainy': 'RAIN',
+ 'cloudy': 'CLOUDS',
+ 'snowy': 'SNOW',
+ 'stormy': 'STORM',
+ 'foggy': 'FOG',
+ 'sunny': 'SUN',
+ 'night': 'NIGHT',
+ 'windy': 'WINDY',
+ 'not_available': 'NA',
+ 'unknown': 'UKN',
+}
+
+temp_conversions = {
+ 'C': lambda temp: temp - 273.15,
+ 'F': lambda temp: (temp * 9 / 5) - 459.67,
+ 'K': lambda temp: temp,
+}
+
+# Note: there are also unicode characters for units: ℃, ℉ and K
+temp_units = {
+ 'C': '°C',
+ 'F': '°F',
+ 'K': 'K',
+}
+
+
+class WeatherSegment(KwThreadedSegment):
+ interval = 600
+ default_location = None
+ location_urls = {}
+ weather_api_key = "fbc9549d91a5e4b26c15be0dbdac3460"
+
+ @staticmethod
+ def key(location_query=None, **kwargs):
+ try:
+ weather_api_key = kwargs["weather_api_key"]
+ except KeyError:
+ weather_api_key = WeatherSegment.weather_api_key
+ return _WeatherKey(location_query, weather_api_key)
+
+ def get_request_url(self, weather_key):
+ try:
+ return self.location_urls[weather_key]
+ except KeyError:
+ query_data = {
+ "appid": weather_key.weather_api_key
+ }
+ location_query = weather_key.location_query
+ if location_query is None:
+ location_data = json.loads(urllib_read('https://freegeoip.app/json/'))
+ query_data["lat"] = location_data["latitude"]
+ query_data["lon"] = location_data["longitude"]
+ else:
+ query_data["q"] = location_query
+ self.location_urls[location_query] = url = (
+ "https://api.openweathermap.org/data/2.5/weather?" +
+ urllib_urlencode(query_data))
+ return url
+
+ def compute_state(self, weather_key):
+ url = self.get_request_url(weather_key)
+ raw_response = urllib_read(url)
+ if not raw_response:
+ self.error('Failed to get response')
+ return None
+
+ response = json.loads(raw_response)
+ try:
+ condition = response['weather'][0]
+ condition_code = int(condition['id'])
+ temp = float(response['main']['temp'])
+ except (KeyError, ValueError):
+ self.exception('OpenWeatherMap returned malformed or unexpected response: {0}', repr(raw_response))
+ return None
+
+ try:
+ icon_names = weather_conditions_codes[condition_code]
+ except IndexError:
+ icon_names = ('unknown',)
+ self.error('Unknown condition code: {0}', condition_code)
+
+ return (temp, icon_names)
+
+ def render_one(self, weather, icons=None, unit='C', temp_format=None, temp_coldest=-30, temp_hottest=40, **kwargs):
+ if not weather:
+ return None
+
+ temp, icon_names = weather
+
+ for icon_name in icon_names:
+ if icons:
+ if icon_name in icons:
+ icon = icons[icon_name]
+ break
+ else:
+ icon = weather_conditions_icons[icon_names[-1]]
+
+ temp_format = temp_format or ('{temp:.0f}' + temp_units[unit])
+ converted_temp = temp_conversions[unit](temp)
+ if converted_temp <= temp_coldest:
+ gradient_level = 0
+ elif converted_temp >= temp_hottest:
+ gradient_level = 100
+ else:
+ gradient_level = (converted_temp - temp_coldest) * 100.0 / (temp_hottest - temp_coldest)
+ groups = ['weather_condition_' + icon_name for icon_name in icon_names] + ['weather_conditions', 'weather']
+ return [
+ {
+ 'contents': icon + ' ',
+ 'highlight_groups': groups,
+ 'divider_highlight_group': 'background:divider',
+ },
+ {
+ 'contents': temp_format.format(temp=converted_temp),
+ 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'],
+ 'divider_highlight_group': 'background:divider',
+ 'gradient_level': gradient_level,
+ },
+ ]
+
+
+weather = with_docstring(WeatherSegment(),
+'''Return weather from OpenWeatherMaps.
+
+Uses GeoIP lookup from https://freegeoip.app to automatically determine
+your current location. This should be changed if you’re in a VPN or if your
+IP address is registered at another location.
+
+Returns a list of colorized icon and temperature segments depending on
+weather conditions.
+
+:param str unit:
+ temperature unit, can be one of ``F``, ``C`` or ``K``
+:param str location_query:
+ location query for your current location, e.g. ``oslo, norway``
+:param dict icons:
+ dict for overriding default icons, e.g. ``{'heavy_snow' : u'❆'}``
+:param str temp_format:
+ format string, receives ``temp`` as an argument. Should also hold unit.
+:param float temp_coldest:
+ coldest temperature. Any temperature below it will have gradient level equal
+ to zero.
+:param float temp_hottest:
+ hottest temperature. Any temperature above it will have gradient level equal
+ to 100. Temperatures between ``temp_coldest`` and ``temp_hottest`` receive
+ gradient level that indicates relative position in this interval
+ (``100 * (cur-coldest) / (hottest-coldest)``).
+
+Divider highlight group used: ``background:divider``.
+
+Highlight groups used: ``weather_conditions`` or ``weather``, ``weather_temp_gradient`` (gradient) or ``weather``.
+Also uses ``weather_conditions_{condition}`` for all weather conditions supported by OpenWeatherMap.
+''')