diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 16:40:16 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 16:40:16 +0000 |
commit | 3f25952c13d5847d510c0cae22a8ba876638d570 (patch) | |
tree | 02f505f016ed5a1029277dcae520d5e2a75906fb /powerline/segments | |
parent | Initial commit. (diff) | |
download | powerline-3f25952c13d5847d510c0cae22a8ba876638d570.tar.xz powerline-3f25952c13d5847d510c0cae22a8ba876638d570.zip |
Adding upstream version 2.8.3.upstream/2.8.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'powerline/segments')
25 files changed, 3982 insertions, 0 deletions
diff --git a/powerline/segments/__init__.py b/powerline/segments/__init__.py new file mode 100644 index 0000000..fa09e58 --- /dev/null +++ b/powerline/segments/__init__.py @@ -0,0 +1,63 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from pkgutil import extend_path +from types import MethodType + + +__path__ = extend_path(__path__, __name__) + + +class Segment(object): + '''Base class for any segment that is not a function + + Required for powerline.lint.inspect to work properly: it defines methods for + omitting existing or adding new arguments. + + .. note:: + Until python-3.4 ``inspect.getargspec`` does not support querying + callable classes for arguments of their ``__call__`` method, requiring + to use this method directly (i.e. before 3.4 you should write + ``getargspec(obj.__call__)`` in place of ``getargspec(obj)``). + ''' + if sys.version_info < (3, 4): + def argspecobjs(self): + yield '__call__', self.__call__ + else: + def argspecobjs(self): + yield '__call__', self + + argspecobjs.__doc__ = ( + '''Return a list of valid arguments for inspect.getargspec + + Used to determine function arguments. + ''' + ) + + def omitted_args(self, name, method): + '''List arguments which should be omitted + + Returns a tuple with indexes of omitted arguments. + + .. note::``segment_info``, ``create_watcher`` and ``pl`` will be omitted + regardless of the below return (for ``segment_info`` and + ``create_watcher``: only if object was marked to require segment + info or filesystem watcher). + ''' + if isinstance(self.__call__, MethodType): + return (0,) + else: + return () + + @staticmethod + def additional_args(): + '''Returns a list of (additional argument name[, default value]) tuples. + ''' + return () + + +def with_docstring(instance, doc): + instance.__doc__ = doc + return instance 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. +''') diff --git a/powerline/segments/i3wm.py b/powerline/segments/i3wm.py new file mode 100644 index 0000000..57ab377 --- /dev/null +++ b/powerline/segments/i3wm.py @@ -0,0 +1,309 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re + +from powerline.theme import requires_segment_info +from powerline.bindings.wm import get_i3_connection + +WORKSPACE_REGEX = re.compile(r'^[0-9]+: ?') + +def workspace_groups(w): + group = [] + if w.focused: + group.append('workspace:focused') + group.append('w_focused') + if w.urgent: + group.append('workspace:urgent') + group.append('w_urgent') + if w.visible: + group.append('workspace:visible') + group.append('w_visible') + group.append('workspace') + return group + + +def format_name(name, strip=False): + if strip: + return WORKSPACE_REGEX.sub('', name, count=1) + return name + + +def is_empty_workspace(workspace, containers): + if workspace.focused or workspace.visible: + return False + wins = [win for win in containers[workspace.name].leaves()] + return False if len(wins) > 0 else True + +WS_ICONS = {"multiple": "M"} + +def get_icon(workspace, separator, icons, show_multiple_icons, ws_containers): + icons_tmp = WS_ICONS + icons_tmp.update(icons) + icons = icons_tmp + + wins = [win for win in ws_containers[workspace.name].leaves() \ + if win.parent.scratchpad_state == 'none'] + if len(wins) == 0: + return '' + + result = '' + cnt = 0 + for key in icons: + if not icons[key] or len(icons[key]) < 1: + continue + if any(key in win.window_class for win in wins if win.window_class): + result += (separator if cnt > 0 else '') + icons[key] + cnt += 1 + if not show_multiple_icons and cnt > 1: + if 'multiple' in icons: + return icons['multiple'] + else: + return '' + return result + +@requires_segment_info +def workspaces(pl, segment_info, only_show=None, output=None, strip=0, format='{name}', + icons=WS_ICONS, sort_workspaces=False, show_output=False, priority_workspaces=[], + hide_empty_workspaces=False): + '''Return list of used workspaces + + :param list only_show: + Specifies which workspaces to show. Valid entries are ``"visible"``, + ``"urgent"`` and ``"focused"``. If omitted or ``null`` all workspaces + are shown. + :param str output: + May be set to the name of an X output. If specified, only workspaces + on that output are shown. Overrides automatic output detection by + the lemonbar renderer and bindings. + Use "__all__" to show workspaces on all outputs. + :param int strip: + Specifies how many characters from the front of each workspace name + should be stripped (e.g. to remove workspace numbers). Defaults to zero. + :param str format: + Specifies the format used to display workspaces; defaults to ``{name}``. + Valid fields are: ``name`` (workspace name), ``number`` (workspace number + if present), `stipped_name`` (workspace name stripped of leading number), + ``icon`` (if available, icon for application running in the workspace, + uses the ``multiple`` icon instead of multiple different icons), ``multi_icon`` + (similar to ``icon``, but does not use ``multiple``, instead joins all icons + with a single space) + :param dict icons: + A dictionary mapping a substring of window classes to strings to be used as an + icon for that window class. + Further, there is a ``multiple`` icon for workspaces containing more than one + window. + :param bool sort_workspaces: + Sort the workspaces displayed by their name according to the natural ordering. + :param bool show_output: + Shows the name of the output if more than one output is connected. + :param list priority_workspaces: + A list of workspace names to be sorted before any other workspaces in the given + order. + :param bool hide_empty_workspaces: + Hides all workspaces without any open window. + Also hides non-focussed workspaces containing only an open scratchpad. + + + Highlight groups used: ``workspace`` or ``w_visible``, ``workspace:visible``, ``workspace`` or ``w_focused``, ``workspace:focused``, ``workspace`` or ``w_urgent``, ``workspace:urgent``, ``workspace`` or ``output``. + ''' + conn = get_i3_connection() + + if not output == "__all__": + output = output or segment_info.get('output') + else: + output = None + + if output: + output = [output] + else: + output = [o.name for o in conn.get_outputs() if o.active] + + + def sort_ws(ws): + if sort_workspaces: + def natural_key(ws): + str = ws.name + return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', str)] + ws = sorted(ws, key=natural_key) + result = [] + for n in priority_workspaces: + result += [w for w in ws if w.name == n] + return result + [w for w in ws if not w.name in priority_workspaces] + + ws_containers = {w_con.name : w_con for w_con in conn.get_tree().workspaces()} + + if len(output) <= 1: + res = [] + if show_output: + res += [{ + 'contents': output[0], + 'highlight_groups': ['output'] + }] + res += [{ + 'contents': format.format(name = w.name[min(len(w.name), strip):], + stripped_name = format_name(w.name, strip=True), + number = w.num, + icon = get_icon(w, '', icons, False, ws_containers), + multi_icon = get_icon(w, ' ', icons, True, ws_containers)), + 'highlight_groups': workspace_groups(w) + } for w in sort_ws(conn.get_workspaces()) \ + if (not only_show or any(getattr(w, tp) for tp in only_show)) \ + if w.output == output[0] \ + if not (hide_empty_workspaces and is_empty_workspace(w, ws_containers))] + return res + else: + res = [] + for n in output: + if show_output: + res += [{ + 'contents': n, + 'highlight_groups': ['output'] + }] + res += [{ + 'contents': format.format(name = w.name[min(len(w.name), strip):], + stripped_name = format_name(w.name, strip=True), + number = w.num, + icon = get_icon(w, '', icons, False, ws_containers), + multi_icon = get_icon(w, ' ', icons, True, ws_containers)), + 'highlight_groups': workspace_groups(w) + } for w in sort_ws(conn.get_workspaces()) \ + if (not only_show or any(getattr(w, tp) for tp in only_show)) \ + if w.output == n \ + if not (hide_empty_workspaces and is_empty_workspace(w, ws_containers))] + return res + +@requires_segment_info +def workspace(pl, segment_info, workspace=None, strip=False, format=None, icons=WS_ICONS): + '''Return the specified workspace name + + :param str workspace: + Specifies which workspace to show. If unspecified, may be set by the + ``list_workspaces`` lister if used, otherwise falls back to + currently focused workspace. + + :param bool strip: + Specifies whether workspace numbers (in the ``1: name`` format) should + be stripped from workspace names before being displayed. Defaults to false. + Deprecated: Use {name} or {stripped_name} of format instead. + + :param str format: + Specifies the format used to display workspaces; defaults to ``{name}``. + Valid fields are: ``name`` (workspace name), ``number`` (workspace number + if present), `stipped_name`` (workspace name stripped of leading number), + ``icon`` (if available, icon for application running in the workspace, + uses the ``multiple`` icon instead of multiple different icons), ``multi_icon`` + (similar to ``icon``, but does not use ``multiple``, instead joins all icons + with a single space) + + :param dict icons: + A dictionary mapping a substring of window classes to strings to be used as an + icon for that window class. + Further, there is a ``multiple`` icon for workspaces containing more than one + window. + + Highlight groups used: ``workspace`` or ``w_visible``, ``workspace:visible``, ``workspace`` or ``w_focused``, ``workspace:focused``, ``workspace`` or ``w_urgent``, ``workspace:urgent``, ``workspace``. + ''' + if format == None: + format = '{stripped_name}' if strip else '{name}' + + conn = get_i3_connection() + ws_containers = {w_con.name : w_con for w_con in conn.get_tree().workspaces()} + + if workspace: + try: + w = next(( + w for w in conn.get_workspaces() + if w.name == workspace + )) + except StopIteration: + return None + elif segment_info.get('workspace'): + w = segment_info['workspace'] + else: + try: + w = next(( + w for w in conn.get_workspaces() + if w.focused + )) + except StopIteration: + return None + + return [{ + 'contents': format.format(name = w.name, + stripped_name = format_name(w.name, strip=True), + number = w.num, + icon = get_icon(w, '', icons, False, ws_containers), + multi_icon = get_icon(w, ' ', icons, True, ws_containers)), + 'highlight_groups': workspace_groups(w) + }] + + +@requires_segment_info +def mode(pl, segment_info, names={'default': None}): + '''Returns current i3 mode + + :param dict names: + Specifies the string to show for various modes. + Use ``null`` to hide a mode (``default`` is hidden by default). + + Highligh groups used: ``mode`` + ''' + mode = segment_info['mode'] + if mode in names: + return names[mode] + return mode + + +def scratchpad_groups(w): + group = [] + if w.urgent: + group.append('scratchpad:urgent') + if w.nodes[0].focused: + group.append('scratchpad:focused') + if w.workspace().name != '__i3_scratch': + group.append('scratchpad:visible') + group.append('scratchpad') + return group + + +SCRATCHPAD_ICONS = { + 'fresh': 'O', + 'changed': 'X', +} + + +def scratchpad(pl, icons=SCRATCHPAD_ICONS): + '''Returns the windows currently on the scratchpad + + :param dict icons: + Specifies the strings to show for the different scratchpad window states. Must + contain the keys ``fresh`` and ``changed``. + + Highlight groups used: ``scratchpad`` or ``scratchpad:visible``, ``scratchpad`` or ``scratchpad:focused``, ``scratchpad`` or ``scratchpad:urgent``. + ''' + + return [ + { + 'contents': icons.get(w.scratchpad_state, icons['changed']), + 'highlight_groups': scratchpad_groups(w) + } + for w in get_i3_connection().get_tree().descendants() + if w.scratchpad_state != 'none' + ] + +def active_window(pl, cutoff=100): + '''Returns the title of the currently active window. + + :param int cutoff: + Maximum title length. If the title is longer, the window_class is used instead. + + Highlight groups used: ``active_window_title``. + ''' + + focused = get_i3_connection().get_tree().find_focused() + cont = focused.name + if len(cont) > cutoff: + cont = focused.window_class + + return [{'contents': cont, 'highlight_groups': ['active_window_title']}] if focused.name != focused.workspace().name else [] diff --git a/powerline/segments/ipython.py b/powerline/segments/ipython.py new file mode 100644 index 0000000..622e0a5 --- /dev/null +++ b/powerline/segments/ipython.py @@ -0,0 +1,9 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info + + +@requires_segment_info +def prompt_count(pl, segment_info): + return str(segment_info['ipython'].prompt_count) diff --git a/powerline/segments/pdb.py b/powerline/segments/pdb.py new file mode 100644 index 0000000..bd6a38b --- /dev/null +++ b/powerline/segments/pdb.py @@ -0,0 +1,61 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from powerline.theme import requires_segment_info + + +@requires_segment_info +def current_line(pl, segment_info): + '''Displays line number that is next to be run + ''' + return str(segment_info['curframe'].f_lineno) + + +@requires_segment_info +def current_file(pl, segment_info, basename=True): + '''Displays current file name + + :param bool basename: + If true only basename is displayed. + ''' + filename = segment_info['curframe'].f_code.co_filename + if basename: + filename = os.path.basename(filename) + return filename + + +@requires_segment_info +def current_code_name(pl, segment_info): + '''Displays name of the code object of the current frame + ''' + return segment_info['curframe'].f_code.co_name + + +@requires_segment_info +def current_context(pl, segment_info): + '''Displays currently executed context name + + This is similar to :py:func:`current_code_name`, but gives more details. + + Currently it only gives module file name if code_name happens to be + ``<module>``. + ''' + name = segment_info['curframe'].f_code.co_name + if name == '<module>': + name = os.path.basename(segment_info['curframe'].f_code.co_filename) + return name + + +@requires_segment_info +def stack_depth(pl, segment_info, full_stack=False): + '''Displays current stack depth + + Result is relative to the stack depth at the time prompt was first run. + + :param bool full_stack: + If true then absolute depth is used. + ''' + return str(len(segment_info['pdb'].stack) - ( + 0 if full_stack else segment_info['initial_stack_length'])) diff --git a/powerline/segments/shell.py b/powerline/segments/shell.py new file mode 100644 index 0000000..66991c7 --- /dev/null +++ b/powerline/segments/shell.py @@ -0,0 +1,196 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info +from powerline.segments import with_docstring +from powerline.segments.common.env import CwdSegment +from powerline.lib.unicode import out_u + + +@requires_segment_info +def jobnum(pl, segment_info, show_zero=False): + '''Return the number of jobs. + + :param bool show_zero: + If False (default) shows nothing if there are no jobs. Otherwise shows + zero for no jobs. + ''' + jobnum = segment_info['args'].jobnum + if jobnum is None or (not show_zero and jobnum == 0): + return None + else: + return str(jobnum) + +try: + import signal + exit_codes = dict((k, v) for v, k in reversed(sorted(signal.__dict__.items())) \ + if v.startswith('SIG') and not v.startswith('SIG_')) +except ImportError: + exit_codes = dict() + +@requires_segment_info +def last_status(pl, segment_info, signal_names=True): + '''Return last exit code. + + :param bool signal_names: + If True (default), translate signal numbers to human-readable names. + + Highlight groups used: ``exit_fail`` + ''' + if not segment_info['args'].last_exit_code: + return None + + try: + if signal_names and segment_info['args'].last_exit_code - 128 in exit_codes: + return [{'contents': exit_codes[segment_info['args'].last_exit_code - 128], 'highlight_groups': ['exit_fail']}] + except TypeError: + pass + return [{'contents': str(segment_info['args'].last_exit_code), 'highlight_groups': ['exit_fail']}] + + +@requires_segment_info +def last_pipe_status(pl, segment_info, signal_names=True): + '''Return last pipe status. + + :param bool signal_names: + If True (default), translate signal numbers to human-readable names. + + Highlight groups used: ``exit_fail``, ``exit_success`` + ''' + last_pipe_status = ( + segment_info['args'].last_pipe_status + or (segment_info['args'].last_exit_code,) + ) + if any(last_pipe_status): + try: + return [{ + 'contents': exit_codes[status - 128] if signal_names and \ + status - 128 in exit_codes else str(status), + 'highlight_groups': ['exit_fail' if status else 'exit_success'], + 'draw_inner_divider': True + } for status in last_pipe_status] + except TypeError: + return [{ + 'contents': str(status), + 'highlight_groups': ['exit_fail' if status else 'exit_success'], + 'draw_inner_divider': True + } for status in last_pipe_status] + else: + return None + +@requires_segment_info +def mode(pl, segment_info, override={'vicmd': 'COMMND', 'viins': 'INSERT'}, default=None): + '''Return the current mode. + + :param dict override: + dict for overriding mode strings. + :param str default: + If current mode is equal to this string then this segment will not get + displayed. If not specified the value is taken from + ``$POWERLINE_DEFAULT_MODE`` variable. This variable is set by zsh + bindings for any mode that does not start from ``vi``. + ''' + mode = segment_info.get('mode', None) + if not mode: + pl.debug('No mode specified') + return None + default = default or segment_info.get('default_mode', None) + if mode == default: + return None + try: + return override[mode] + except KeyError: + # Note: with zsh line editor you can emulate as much modes as you wish. + # Thus having unknown mode is not an error: maybe just some developer + # added support for his own zle widgets. As there is no built-in mode() + # function like in VimL and mode is likely be defined by our code or by + # somebody knowing what he is doing there is absolutely no need in + # keeping translations dictionary. + return mode.upper() + + +@requires_segment_info +def continuation(pl, segment_info, omit_cmdsubst=True, right_align=False, renames={}): + '''Display parser state. + + :param bool omit_cmdsubst: + Do not display cmdsubst parser state if it is the last one. + :param bool right_align: + Align to the right. + :param dict renames: + Rename states: ``{old_name : new_name}``. If ``new_name`` is ``None`` + then given state is not displayed. + + Highlight groups used: ``continuation``, ``continuation:current``. + ''' + if not segment_info.get('parser_state'): + return [{ + 'contents': '', + 'width': 'auto', + 'highlight_groups': ['continuation:current', 'continuation'], + }] + ret = [] + + for state in segment_info['parser_state'].split(): + state = renames.get(state, state) + if state: + ret.append({ + 'contents': state, + 'highlight_groups': ['continuation'], + 'draw_inner_divider': True, + }) + + if omit_cmdsubst and ret[-1]['contents'] == 'cmdsubst': + ret.pop(-1) + + if not ret: + ret.append({ + 'contents': '' + }) + + if right_align: + ret[0].update(width='auto', align='r') + ret[-1]['highlight_groups'] = ['continuation:current', 'continuation'] + else: + ret[-1].update(width='auto', align='l', highlight_groups=['continuation:current', 'continuation']) + + return ret + + +@requires_segment_info +class ShellCwdSegment(CwdSegment): + def get_shortened_path(self, pl, segment_info, use_shortened_path=True, **kwargs): + if use_shortened_path: + try: + return out_u(segment_info['shortened_path']) + except KeyError: + pass + return super(ShellCwdSegment, self).get_shortened_path(pl, segment_info, **kwargs) + + +cwd = with_docstring(ShellCwdSegment(), +'''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 use_shortened_path: + Use path from shortened_path ``--renderer-arg`` argument. If this argument + is present ``shorten_home`` argument is ignored. +: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. +''') diff --git a/powerline/segments/tmux.py b/powerline/segments/tmux.py new file mode 100644 index 0000000..1f34389 --- /dev/null +++ b/powerline/segments/tmux.py @@ -0,0 +1,22 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.bindings.tmux import get_tmux_output + + +def attached_clients(pl, minimum=1): + '''Return the number of tmux clients attached to the currently active session + + :param int minimum: + The minimum number of attached clients that must be present for this + segment to be visible. + ''' + session_output = get_tmux_output(pl, 'list-panes', '-F', '#{session_name}') + if not session_output: + return None + session_name = session_output.rstrip().split('\n')[0] + + attached_clients_output = get_tmux_output(pl, 'list-clients', '-t', session_name) + attached_count = len(attached_clients_output.rstrip().split('\n')) + + return None if attached_count < minimum else str(attached_count) diff --git a/powerline/segments/vim/__init__.py b/powerline/segments/vim/__init__.py new file mode 100644 index 0000000..d999d07 --- /dev/null +++ b/powerline/segments/vim/__init__.py @@ -0,0 +1,805 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re +import csv +import sys + +from collections import defaultdict + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import (vim_get_func, getbufvar, vim_getbufoption, + buffer_name, vim_getwinvar, + register_buffer_cache, current_tabpage, + list_tabpage_buffers_segment_info) +from powerline.theme import requires_segment_info, requires_filesystem_watcher +from powerline.lib import add_divider_highlight_group +from powerline.lib.vcs import guess +from powerline.lib.humanize_bytes import humanize_bytes +from powerline.lib import wraps_saveargs as wraps +from powerline.segments.common.vcs import BranchSegment, StashSegment +from powerline.segments import with_docstring +from powerline.lib.unicode import string, unicode + +try: + from __builtin__ import xrange as range +except ImportError: + pass + + +vim_funcs = { + 'virtcol': vim_get_func('virtcol', rettype='int'), + 'getpos': vim_get_func('getpos'), + 'fnamemodify': vim_get_func('fnamemodify', rettype='bytes'), + 'line2byte': vim_get_func('line2byte', rettype='int'), + 'line': vim_get_func('line', rettype='int'), +} + +vim_modes = { + 'n': 'NORMAL', + 'no': 'N-OPER', + 'v': 'VISUAL', + 'V': 'V-LINE', + '^V': 'V-BLCK', + 's': 'SELECT', + 'S': 'S-LINE', + '^S': 'S-BLCK', + 'i': 'INSERT', + 'ic': 'I-COMP', + 'ix': 'I-C_X ', + 'R': 'RPLACE', + 'Rv': 'V-RPLC', + 'Rc': 'R-COMP', + 'Rx': 'R-C_X ', + 'c': 'COMMND', + 'cv': 'VIM-EX', + 'ce': 'NRM-EX', + 'r': 'PROMPT', + 'rm': '-MORE-', + 'r?': 'CNFIRM', + '!': '!SHELL', + 't': 'TERM ', +} + + +# TODO Remove cache when needed +def window_cached(func): + cache = {} + + @requires_segment_info + @wraps(func) + def ret(segment_info, **kwargs): + window_id = segment_info['window_id'] + if segment_info['mode'] == 'nc': + return cache.get(window_id) + else: + if getattr(func, 'powerline_requires_segment_info', False): + r = func(segment_info=segment_info, **kwargs) + else: + r = func(**kwargs) + cache[window_id] = r + return r + + return ret + + +@requires_segment_info +def mode(pl, segment_info, override=None): + '''Return the current vim mode. + + If mode (returned by ``mode()`` VimL function, see ``:h mode()`` in Vim) + consists of multiple characters and necessary mode is not known to powerline + then it will fall back to mode with last character(s) ignored. + + :param dict override: + dict for overriding default mode strings, e.g. ``{ 'n': 'NORM' }`` + ''' + mode = segment_info['mode'] + if mode == 'nc': + return None + while mode: + try: + if not override: + return vim_modes[mode] + try: + return override[mode] + except KeyError: + return vim_modes[mode] + except KeyError: + mode = mode[:-1] + return 'BUG' + + +@window_cached +@requires_segment_info +def visual_range(pl, segment_info, CTRL_V_text='{rows} x {vcols}', v_text_oneline='C:{vcols}', v_text_multiline='L:{rows}', V_text='L:{rows}'): + '''Return the current visual selection range. + + :param str CTRL_V_text: + Text to display when in block visual or select mode. + :param str v_text_oneline: + Text to display when in charaterwise visual or select mode, assuming + selection occupies only one line. + :param str v_text_multiline: + Text to display when in charaterwise visual or select mode, assuming + selection occupies more then one line. + :param str V_text: + Text to display when in linewise visual or select mode. + + All texts are format strings which are passed the following parameters: + + ========= ============================================================= + Parameter Description + ========= ============================================================= + sline Line number of the first line of the selection + eline Line number of the last line of the selection + scol Column number of the first character of the selection + ecol Column number of the last character of the selection + svcol Virtual column number of the first character of the selection + secol Virtual column number of the last character of the selection + rows Number of lines in the selection + cols Number of columns in the selection + vcols Number of virtual columns in the selection + ========= ============================================================= + ''' + sline, scol, soff = [int(v) for v in vim_funcs['getpos']('v')[1:]] + eline, ecol, eoff = [int(v) for v in vim_funcs['getpos']('.')[1:]] + svcol = vim_funcs['virtcol']([sline, scol, soff]) + evcol = vim_funcs['virtcol']([eline, ecol, eoff]) + rows = abs(eline - sline) + 1 + cols = abs(ecol - scol) + 1 + vcols = abs(evcol - svcol) + 1 + return { + '^': CTRL_V_text, + 's': v_text_oneline if rows == 1 else v_text_multiline, + 'S': V_text, + 'v': v_text_oneline if rows == 1 else v_text_multiline, + 'V': V_text, + }.get(segment_info['mode'][0], '').format( + sline=sline, eline=eline, + scol=scol, ecol=ecol, + svcol=svcol, evcol=evcol, + rows=rows, cols=cols, vcols=vcols, + ) + + +@requires_segment_info +def modified_indicator(pl, segment_info, text='+'): + '''Return a file modified indicator. + + :param string text: + text to display if the current buffer is modified + ''' + return text if int(vim_getbufoption(segment_info, 'modified')) else None + + +@requires_segment_info +def tab_modified_indicator(pl, segment_info, text='+'): + '''Return a file modified indicator for tabpages. + + :param string text: + text to display if any buffer in the current tab is modified + + Highlight groups used: ``tab_modified_indicator`` or ``modified_indicator``. + ''' + for buf_segment_info in list_tabpage_buffers_segment_info(segment_info): + if int(vim_getbufoption(buf_segment_info, 'modified')): + return [{ + 'contents': text, + 'highlight_groups': ['tab_modified_indicator', 'modified_indicator'], + }] + return None + + +@requires_segment_info +def paste_indicator(pl, segment_info, text='PASTE'): + '''Return a paste mode indicator. + + :param string text: + text to display if paste mode is enabled + ''' + return text if int(vim.eval('&paste')) else None + + +@requires_segment_info +def readonly_indicator(pl, segment_info, text='RO'): + '''Return a read-only indicator. + + :param string text: + text to display if the current buffer is read-only + ''' + return text if int(vim_getbufoption(segment_info, 'readonly')) else None + + +SCHEME_RE = re.compile(b'^\\w[\\w\\d+\\-.]*(?=:)') + + +@requires_segment_info +def file_scheme(pl, segment_info): + '''Return the protocol part of the file. + + Protocol is the part of the full filename just before the colon which + starts with a latin letter and contains only latin letters, digits, plus, + period or hyphen (refer to `RFC3986 + <http://tools.ietf.org/html/rfc3986#section-3.1>`_ for the description of + URI scheme). If there is no such a thing ``None`` is returned, effectively + removing segment. + + .. note:: + Segment will not check whether there is ``//`` just after the + colon or if there is at least one slash after the scheme. Reason: it is + not always present. E.g. when opening file inside a zip archive file + name will look like :file:`zipfile:/path/to/archive.zip::file.txt`. + ``file_scheme`` segment will catch ``zipfile`` part here. + ''' + name = buffer_name(segment_info) + if not name: + return None + match = SCHEME_RE.match(name) + if match: + return match.group(0).decode('ascii') + + +@requires_segment_info +def file_directory(pl, segment_info, remove_scheme=True, shorten_user=True, shorten_cwd=True, shorten_home=False): + '''Return file directory (head component of the file path). + + :param bool remove_scheme: + Remove scheme part from the segment name, if present. See documentation + of file_scheme segment for the description of what scheme is. Also + removes the colon. + + :param bool shorten_user: + Shorten ``$HOME`` directory to :file:`~/`. Does not work for files with + scheme. + + :param bool shorten_cwd: + Shorten current directory to :file:`./`. Does not work for files with + scheme present. + + :param bool shorten_home: + Shorten all directories in :file:`/home/` to :file:`~user/` instead of + :file:`/home/user/`. Does not work for files with scheme present. + ''' + name = buffer_name(segment_info) + if not name: + return None + match = SCHEME_RE.match(name) + if match: + if remove_scheme: + name = name[len(match.group(0)) + 1:] # Remove scheme and colon + file_directory = vim_funcs['fnamemodify'](name, ':h') + else: + file_directory = vim_funcs['fnamemodify']( + name, + (':~' if shorten_user else '') + (':.' if shorten_cwd else '') + ':h' + ) + if not file_directory: + return None + if shorten_home and file_directory.startswith('/home/'): + file_directory = b'~' + file_directory[6:] + file_directory = file_directory.decode(segment_info['encoding'], 'powerline_vim_strtrans_error') + return file_directory + os.sep + + +@requires_segment_info +def file_name(pl, segment_info, display_no_file=False, no_file_text='[No file]'): + '''Return file name (tail component of the file path). + + :param bool display_no_file: + display a string if the buffer is missing a file name + :param str no_file_text: + the string to display if the buffer is missing a file name + + Highlight groups used: ``file_name_no_file`` or ``file_name``, ``file_name``. + ''' + name = buffer_name(segment_info) + if not name: + if display_no_file: + return [{ + 'contents': no_file_text, + 'highlight_groups': ['file_name_no_file', 'file_name'], + }] + else: + return None + return os.path.basename(name).decode(segment_info['encoding'], 'powerline_vim_strtrans_error') + + +@window_cached +def file_size(pl, suffix='B', si_prefix=False): + '''Return file size in &encoding. + + :param str suffix: + string appended to the file size + :param bool si_prefix: + use SI prefix, e.g. MB instead of MiB + :return: file size or None if the file isn’t saved or if the size is too big to fit in a number + ''' + # Note: returns file size in &encoding, not in &fileencoding. But returned + # size is updated immediately; and it is valid for any buffer + file_size = vim_funcs['line2byte'](len(vim.current.buffer) + 1) - 1 + if file_size < 0: + file_size = 0 + return humanize_bytes(file_size, suffix, si_prefix) + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_format(pl, segment_info): + '''Return file format (i.e. line ending type). + + :return: file format or None if unknown or missing file format + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'fileformat') or None + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_encoding(pl, segment_info): + '''Return file encoding/character set. + + :return: file encoding/character set or None if unknown or missing file encoding + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'fileencoding') or None + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_bom(pl, segment_info): + '''Return BOM of the current file + + :return: Byte order mark or None if unknown or missing BOM + + Divider highlight group used: ``background:divider``. + ''' + return 'bom' if vim_getbufoption(segment_info, 'bomb') else None + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_type(pl, segment_info): + '''Return file type. + + :return: file type or None if unknown file type + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'filetype') or None + + +@requires_segment_info +def window_title(pl, segment_info): + '''Return the window title. + + This currently looks at the ``quickfix_title`` window variable, + which is used by Syntastic and Vim itself. + + It is used in the quickfix theme.''' + try: + return vim_getwinvar(segment_info, 'quickfix_title') + except KeyError: + return None + + +@requires_segment_info +def line_percent(pl, segment_info, gradient=False): + '''Return the cursor position in the file as a percentage. + + :param bool gradient: + highlight the percentage with a color gradient (by default a green to red gradient) + + Highlight groups used: ``line_percent_gradient`` (gradient), ``line_percent``. + ''' + line_current = segment_info['window'].cursor[0] + line_last = len(segment_info['buffer']) + percentage = line_current * 100.0 / line_last + if not gradient: + return str(int(round(percentage))) + return [{ + 'contents': str(int(round(percentage))), + 'highlight_groups': ['line_percent_gradient', 'line_percent'], + 'gradient_level': percentage, + }] + + +@window_cached +def position(pl, position_strings={'top': 'Top', 'bottom': 'Bot', 'all': 'All'}, gradient=False): + '''Return the position of the current view in the file as a percentage. + + :param dict position_strings: + dict for translation of the position strings, e.g. ``{"top":"Oben", "bottom":"Unten", "all":"Alles"}`` + + :param bool gradient: + highlight the percentage with a color gradient (by default a green to red gradient) + + Highlight groups used: ``position_gradient`` (gradient), ``position``. + ''' + line_last = len(vim.current.buffer) + + winline_first = vim_funcs['line']('w0') + winline_last = vim_funcs['line']('w$') + if winline_first == 1 and winline_last == line_last: + percentage = 0.0 + content = position_strings['all'] + elif winline_first == 1: + percentage = 0.0 + content = position_strings['top'] + elif winline_last == line_last: + percentage = 100.0 + content = position_strings['bottom'] + else: + percentage = winline_first * 100.0 / (line_last - winline_last + winline_first) + content = str(int(round(percentage))) + '%' + + if not gradient: + return content + return [{ + 'contents': content, + 'highlight_groups': ['position_gradient', 'position'], + 'gradient_level': percentage, + }] + + +@requires_segment_info +def line_current(pl, segment_info): + '''Return the current cursor line.''' + return str(segment_info['window'].cursor[0]) + + +@requires_segment_info +def line_count(pl, segment_info): + '''Return the line count of the current buffer.''' + return str(len(segment_info['buffer'])) + + +@requires_segment_info +def col_current(pl, segment_info): + '''Return the current cursor column. + ''' + return str(segment_info['window'].cursor[1] + 1) + + +@window_cached +def virtcol_current(pl, gradient=True): + '''Return current visual column with concealed characters ignored + + :param bool gradient: + Determines whether it should show textwidth-based gradient (gradient level is ``virtcol * 100 / textwidth``). + + Highlight groups used: ``virtcol_current_gradient`` (gradient), ``virtcol_current`` or ``col_current``. + ''' + col = vim_funcs['virtcol']('.') + r = [{'contents': str(col), 'highlight_groups': ['virtcol_current', 'col_current']}] + if gradient: + textwidth = int(getbufvar('%', '&textwidth')) + r[-1]['gradient_level'] = min(col * 100 / textwidth, 100) if textwidth else 0 + r[-1]['highlight_groups'].insert(0, 'virtcol_current_gradient') + return r + + +def modified_buffers(pl, text='+ ', join_str=','): + '''Return a comma-separated list of modified buffers. + + :param str text: + text to display before the modified buffer list + :param str join_str: + string to use for joining the modified buffer list + ''' + buffer_mod_text = join_str.join(( + str(buffer.number) + for buffer in vim.buffers + if int(vim_getbufoption({'buffer': buffer, 'bufnr': buffer.number}, 'modified')) + )) + if buffer_mod_text: + return text + buffer_mod_text + return None + + +@requires_filesystem_watcher +@requires_segment_info +class VimBranchSegment(BranchSegment): + divider_highlight_group = 'branch:divider' + + @staticmethod + def get_directory(segment_info): + if vim_getbufoption(segment_info, 'buftype'): + return None + return buffer_name(segment_info) + + +branch = with_docstring(VimBranchSegment(), +'''Return the current working branch. + +:param bool status_colors: + Determines whether repository status will be used to determine highlighting. + Default: False. +:param bool 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``. + +Divider highlight group used: ``branch:divider``. +''') + + +@requires_filesystem_watcher +@requires_segment_info +class VimStashSegment(StashSegment): + divider_highlight_group = 'stash:divider' + + @staticmethod + def get_directory(segment_info): + if vim_getbufoption(segment_info, 'buftype'): + return None + return buffer_name(segment_info) + + +stash = with_docstring(VimStashSegment(), +'''Return the number of stashes in the current working branch. + +Highlight groups used: ``stash``. +''') + + +@requires_filesystem_watcher +@requires_segment_info +def file_vcs_status(pl, segment_info, create_watcher): + '''Return the VCS status for this buffer. + + Highlight groups used: ``file_vcs_status``. + ''' + name = buffer_name(segment_info) + skip = not (name and (not vim_getbufoption(segment_info, 'buftype'))) + if not skip: + repo = guess(path=name, create_watcher=create_watcher) + if repo is not None: + status = repo.status(os.path.relpath(name, repo.directory)) + if not status: + return None + status = status.strip() + ret = [] + for status in status: + ret.append({ + 'contents': status, + 'highlight_groups': ['file_vcs_status_' + status, 'file_vcs_status'], + }) + return ret + + +trailing_whitespace_cache = None + + +@requires_segment_info +def trailing_whitespace(pl, segment_info): + '''Return the line number for trailing whitespaces + + It is advised not to use this segment in insert mode: in Insert mode it will + iterate over all lines in buffer each time you happen to type a character + which may cause lags. It will also show you whitespace warning each time you + happen to type space. + + Highlight groups used: ``trailing_whitespace`` or ``warning``. + ''' + global trailing_whitespace_cache + if trailing_whitespace_cache is None: + trailing_whitespace_cache = register_buffer_cache(defaultdict(lambda: (0, None))) + bufnr = segment_info['bufnr'] + changedtick = getbufvar(bufnr, 'changedtick') + if trailing_whitespace_cache[bufnr][0] == changedtick: + return trailing_whitespace_cache[bufnr][1] + else: + buf = segment_info['buffer'] + bws = b' \t' + sws = str(' \t') # Ignore unicode_literals and use native str. + for i in range(len(buf)): + try: + line = buf[i] + except UnicodeDecodeError: # May happen in Python 3 + if hasattr(vim, 'bindeval'): + line = vim.bindeval('getbufline({0}, {1})'.format( + bufnr, i + 1)) + has_trailing_ws = (line[-1] in bws) + else: + line = vim.eval('strtrans(getbufline({0}, {1}))'.format( + bufnr, i + 1)) + has_trailing_ws = (line[-1] in bws) + else: + has_trailing_ws = (line and line[-1] in sws) + if has_trailing_ws: + break + if has_trailing_ws: + ret = [{ + 'contents': str(i + 1), + 'highlight_groups': ['trailing_whitespace', 'warning'], + }] + else: + ret = None + trailing_whitespace_cache[bufnr] = (changedtick, ret) + return ret + + +@requires_segment_info +def tabnr(pl, segment_info, show_current=True): + '''Show tabpage number + + :param bool show_current: + If False do not show current tabpage number. This is default because + tabnr is by default only present in tabline. + ''' + try: + tabnr = segment_info['tabnr'] + except KeyError: + return None + if show_current or tabnr != current_tabpage().number: + return str(tabnr) + + +@requires_segment_info +def bufnr(pl, segment_info, show_current=True): + '''Show buffer number + + :param bool show_current: + If False do not show current window number. + ''' + bufnr = segment_info['bufnr'] + if show_current or bufnr != vim.current.buffer.number: + return str(bufnr) + + +@requires_segment_info +def winnr(pl, segment_info, show_current=True): + '''Show window number + + :param bool show_current: + If False do not show current window number. + ''' + winnr = segment_info['winnr'] + if show_current or winnr != vim.current.window.number: + return str(winnr) + + +csv_cache = None +sniffer = csv.Sniffer() + + +def detect_text_csv_dialect(text, display_name, header_text=None): + return ( + sniffer.sniff(string(text)), + sniffer.has_header(string(header_text or text)) if display_name == 'auto' else display_name, + ) + + +CSV_SNIFF_LINES = 100 +CSV_PARSE_LINES = 10 + + +if sys.version_info < (2, 7): + def read_csv(l, dialect, fin=next): + try: + return fin(csv.reader(l, dialect)) + except csv.Error as e: + if str(e) == 'newline inside string' and dialect.quotechar: + # Maybe we are inside an unfinished quoted string. Python-2.6 + # does not handle this fine + return fin(csv.reader(l[:-1] + [l[-1] + dialect.quotechar])) + else: + raise +else: + def read_csv(l, dialect, fin=next): + return fin(csv.reader(l, dialect)) + + +def process_csv_buffer(pl, buffer, line, col, display_name): + global csv_cache + if csv_cache is None: + csv_cache = register_buffer_cache(defaultdict(lambda: (None, None, None))) + try: + cur_first_line = buffer[0] + except UnicodeDecodeError: + cur_first_line = vim.eval('strtrans(getline(1))') + dialect, has_header, first_line = csv_cache[buffer.number] + if dialect is None or (cur_first_line != first_line and display_name == 'auto'): + try: + text = '\n'.join(buffer[:CSV_SNIFF_LINES]) + except UnicodeDecodeError: # May happen in Python 3 + text = vim.eval('join(map(getline(1, {0}), "strtrans(v:val)"), "\\n")'.format(CSV_SNIFF_LINES)) + try: + dialect, has_header = detect_text_csv_dialect(text, display_name) + except csv.Error as e: + pl.warn('Failed to detect csv format: {0}', str(e)) + # Try detecting using three lines only: + if line == 1: + rng = (0, line + 2) + elif line == len(buffer): + rng = (line - 3, line) + else: + rng = (line - 2, line + 1) + try: + dialect, has_header = detect_text_csv_dialect( + '\n'.join(buffer[rng[0]:rng[1]]), + display_name, + header_text='\n'.join(buffer[:4]), + ) + except csv.Error as e: + pl.error('Failed to detect csv format: {0}', str(e)) + return None, None + if len(buffer) > 2: + csv_cache[buffer.number] = dialect, has_header, cur_first_line + column_number = len(read_csv( + buffer[max(0, line - CSV_PARSE_LINES):line - 1] + [buffer[line - 1][:col]], + dialect=dialect, + fin=list, + )[-1]) or 1 + if has_header: + try: + header = read_csv(buffer[0:1], dialect=dialect) + except UnicodeDecodeError: + header = read_csv([vim.eval('strtrans(getline(1))')], dialect=dialect) + column_name = header[column_number - 1] + else: + column_name = None + return unicode(column_number), column_name + + +@requires_segment_info +def csv_col_current(pl, segment_info, display_name='auto', name_format=' ({column_name:.15})'): + '''Display CSV column number and column name + + Requires filetype to be set to ``csv``. + + :param bool or str name: + May be ``True``, ``False`` and ``"auto"``. In the first case value from + the first raw will always be displayed. In the second case it will never + be displayed. In the last case ``csv.Sniffer().has_header()`` will be + used to detect whether current file contains header in the first column. + :param str name_format: + String used to format column name (in case ``display_name`` is set to + ``True`` or ``"auto"``). Accepts ``column_name`` keyword argument. + + Highlight groups used: ``csv:column_number`` or ``csv``, ``csv:column_name`` or ``csv``. + ''' + if vim_getbufoption(segment_info, 'filetype') != 'csv': + return None + line, col = segment_info['window'].cursor + column_number, column_name = process_csv_buffer(pl, segment_info['buffer'], line, col, display_name) + if not column_number: + return None + return [{ + 'contents': column_number, + 'highlight_groups': ['csv:column_number', 'csv'], + }] + ([{ + 'contents': name_format.format(column_name=column_name), + 'highlight_groups': ['csv:column_name', 'csv'], + }] if column_name else []) + + +@requires_segment_info +def tab(pl, segment_info, end=False): + '''Mark start of the clickable region for tabpage + + :param bool end: + In place of starting region for the current tab end it. + + No highlight groups are used (literal segment). + ''' + try: + return [{ + 'contents': None, + 'literal_contents': (0, '%{tabnr}T'.format(tabnr=('' if end else segment_info['tabnr']))), + }] + except KeyError: + return None diff --git a/powerline/segments/vim/plugin/__init__.py b/powerline/segments/vim/plugin/__init__.py new file mode 100644 index 0000000..b2b9f10 --- /dev/null +++ b/powerline/segments/vim/plugin/__init__.py @@ -0,0 +1,6 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) +from pkgutil import extend_path + + +__path__ = extend_path(__path__, __name__) diff --git a/powerline/segments/vim/plugin/ale.py b/powerline/segments/vim/plugin/ale.py new file mode 100644 index 0000000..4f4bdee --- /dev/null +++ b/powerline/segments/vim/plugin/ale.py @@ -0,0 +1,52 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_global_exists +from powerline.theme import requires_segment_info + + +@requires_segment_info +def ale(segment_info, pl, err_format='ERR: ln {first_line} ({num}) ', warn_format='WARN: ln {first_line} ({num}) '): + '''Show whether ALE has found any errors or warnings + + :param str err_format: + Format string for errors. + + :param str warn_format: + Format string for warnings. + + Highlight groups used: ``ale:warning`` or ``warning``, ``ale:error`` or ``error``. + ''' + if not (vim_global_exists('ale_enabled') and int(vim.eval('g:ale_enabled'))): + return None + has_errors = int(vim.eval('ale#statusline#Count(' + str(segment_info['bufnr']) + ').total')) + if not has_errors: + return + error = None + warning = None + errors_count = 0 + warnings_count = 0 + for issue in vim.eval('ale#engine#GetLoclist(' + str(segment_info['bufnr']) + ')'): + if issue['type'] == 'E': + error = error or issue + errors_count += 1 + elif issue['type'] == 'W': + warning = warning or issue + warnings_count += 1 + segments = [] + if error: + segments.append({ + 'contents': err_format.format(first_line=error['lnum'], num=errors_count), + 'highlight_groups': ['ale:error', 'error'], + }) + if warning: + segments.append({ + 'contents': warn_format.format(first_line=warning['lnum'], num=warnings_count), + 'highlight_groups': ['ale:warning', 'warning'], + }) + return segments diff --git a/powerline/segments/vim/plugin/capslock.py b/powerline/segments/vim/plugin/capslock.py new file mode 100644 index 0000000..d2c474d --- /dev/null +++ b/powerline/segments/vim/plugin/capslock.py @@ -0,0 +1,30 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_func_exists +from powerline.theme import requires_segment_info + + +@requires_segment_info +def capslock_indicator(pl, segment_info, text='CAPS'): + '''Shows the indicator if tpope/vim-capslock plugin is enabled + + .. note:: + In the current state plugin automatically disables itself when leaving + insert mode. So trying to use this segment not in insert or replace + modes is useless. + + :param str text: + String to show when software capslock presented by this plugin is + active. + ''' + if not vim_func_exists('CapsLockStatusline'): + return None + # CapsLockStatusline() function returns an empty string when plugin is + # disabled. If it is not then string is non-empty. + return text if vim.eval('CapsLockStatusline()') else None diff --git a/powerline/segments/vim/plugin/coc.py b/powerline/segments/vim/plugin/coc.py new file mode 100644 index 0000000..290faec --- /dev/null +++ b/powerline/segments/vim/plugin/coc.py @@ -0,0 +1,51 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_command_exists +from powerline.theme import requires_segment_info + +# coc_status's format: E1 W2 +def parse_coc_status(coc_status): + # type(coc_status) is tuple + errors_count = 0 + warnings_count = 0 + if len(coc_status) <= 0: + return errors_count, warnings_count + status_str = coc_status[0] + if len(status_str) <= 0: + return errors_count, warnings_count + status_list = status_str.split(' ') + for item in status_list: + if len(item) > 0 and item[0] == 'E': + errors_count = int(item[1:]) + if len(item) > 0 and item[0] == 'W': + warnings_count = int(item[1:]) + return errors_count, warnings_count + +@requires_segment_info +def coc(segment_info, pl): + '''Show whether coc.nvim has found any errors or warnings + + Highlight groups used: ``coc:warning`` or ``warning``, ``coc:error`` or ``error``. + ''' + segments = [] + if not vim_command_exists('CocCommand'): + return segments + coc_status = vim.eval('coc#status()'), + errors_count, warnings_count = parse_coc_status(coc_status) + if errors_count > 0: + segments.append({ + 'contents': 'E:' + str(errors_count), + 'highlight_groups': ['coc:error', 'error'], + }) + if warnings_count > 0: + segments.append({ + 'contents': 'W:' + str(warnings_count), + 'highlight_groups': ['coc:warning', 'warning'], + }) + return segments diff --git a/powerline/segments/vim/plugin/commandt.py b/powerline/segments/vim/plugin/commandt.py new file mode 100644 index 0000000..7e5262e --- /dev/null +++ b/powerline/segments/vim/plugin/commandt.py @@ -0,0 +1,97 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import create_ruby_dpowerline + + +def initialize(): + global initialized + if initialized: + return + initialized = True + create_ruby_dpowerline() + vim.command(( + # When using :execute (vim.command uses the same code) one should not + # use << EOF. + ''' + ruby + if (not ($command_t.respond_to? 'active_finder')) + def $command_t.active_finder + @active_finder and @active_finder.class.name or '' + end + end + if (not ($command_t.respond_to? 'path')) + def $command_t.path + @path or '' + end + end + def $powerline.commandt_set_active_finder + ::VIM::command "let g:powerline_commandt_reply = '#{$command_t.active_finder}'" + end + def $powerline.commandt_set_path + ::VIM::command "let g:powerline_commandt_reply = '#{($command_t.path or '').gsub(/'/, "''")}'" + end + ''' + )) + + +initialized = False + + +def finder(pl): + '''Display Command-T finder name + + Requires $command_t.active_finder and methods (code above may monkey-patch + $command_t to add them). All Command-T finders have ``CommandT::`` module + prefix, but it is stripped out (actually, any ``CommandT::`` substring will + be stripped out). + + Highlight groups used: ``commandt:finder``. + ''' + initialize() + vim.command('ruby $powerline.commandt_set_active_finder') + return [{ + 'highlight_groups': ['commandt:finder'], + 'contents': vim.eval('g:powerline_commandt_reply').replace('CommandT::', '').replace('Finder::', '') + }] + + +FINDERS_WITHOUT_PATH = set(( + 'CommandT::MRUBufferFinder', + 'CommandT::BufferFinder', + 'CommandT::TagFinder', + 'CommandT::JumpFinder', + 'CommandT::Finder::MRUBufferFinder', + 'CommandT::Finder::BufferFinder', + 'CommandT::Finder::TagFinder', + 'CommandT::Finder::JumpFinder', +)) + + +def path(pl): + '''Display path used by Command-T + + Requires $command_t.active_finder and .path methods (code above may + monkey-patch $command_t to add them). + + $command_t.active_finder is required in order to omit displaying path for + finders ``MRUBufferFinder``, ``BufferFinder``, ``TagFinder`` and + ``JumpFinder`` (pretty much any finder, except ``FileFinder``). + + Highlight groups used: ``commandt:path``. + ''' + initialize() + vim.command('ruby $powerline.commandt_set_active_finder') + finder = vim.eval('g:powerline_commandt_reply') + if finder in FINDERS_WITHOUT_PATH: + return None + vim.command('ruby $powerline.commandt_set_path') + return [{ + 'highlight_groups': ['commandt:path'], + 'contents': vim.eval('g:powerline_commandt_reply') + }] diff --git a/powerline/segments/vim/plugin/nerdtree.py b/powerline/segments/vim/plugin/nerdtree.py new file mode 100644 index 0000000..f11be14 --- /dev/null +++ b/powerline/segments/vim/plugin/nerdtree.py @@ -0,0 +1,25 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import bufvar_exists +from powerline.segments.vim import window_cached + + +@window_cached +def nerdtree(pl): + '''Return directory that is shown by the current buffer. + + Highlight groups used: ``nerdtree:path`` or ``file_name``. + ''' + if not bufvar_exists(None, 'NERDTreeRoot'): + return None + path_str = vim.eval('getbufvar("%", "NERDTreeRoot").path.str()') + return [{ + 'contents': path_str, + 'highlight_groups': ['nerdtree:path', 'file_name'], + }] diff --git a/powerline/segments/vim/plugin/syntastic.py b/powerline/segments/vim/plugin/syntastic.py new file mode 100644 index 0000000..5bef3c7 --- /dev/null +++ b/powerline/segments/vim/plugin/syntastic.py @@ -0,0 +1,43 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.segments.vim import window_cached +from powerline.bindings.vim import vim_global_exists + + +@window_cached +def syntastic(pl, err_format='ERR: {first_line} ({num}) ', warn_format='WARN: {first_line} ({num}) '): + '''Show whether syntastic has found any errors or warnings + + :param str err_format: + Format string for errors. + + :param str warn_format: + Format string for warnings. + + Highlight groups used: ``syntastic:warning`` or ``warning``, ``syntastic:error`` or ``error``. + ''' + if not vim_global_exists('SyntasticLoclist'): + return None + has_errors = int(vim.eval('g:SyntasticLoclist.current().hasErrorsOrWarningsToDisplay()')) + if not has_errors: + return + errors = vim.eval('g:SyntasticLoclist.current().errors()') + warnings = vim.eval('g:SyntasticLoclist.current().warnings()') + segments = [] + if errors: + segments.append({ + 'contents': err_format.format(first_line=errors[0]['lnum'], num=len(errors)), + 'highlight_groups': ['syntastic:error', 'error'], + }) + if warnings: + segments.append({ + 'contents': warn_format.format(first_line=warnings[0]['lnum'], num=len(warnings)), + 'highlight_groups': ['syntastic:warning', 'warning'], + }) + return segments diff --git a/powerline/segments/vim/plugin/tagbar.py b/powerline/segments/vim/plugin/tagbar.py new file mode 100644 index 0000000..e683758 --- /dev/null +++ b/powerline/segments/vim/plugin/tagbar.py @@ -0,0 +1,51 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_command_exists, vim_get_autoload_func +from powerline.theme import requires_segment_info + + +currenttag = None +tag_cache = {} + + +@requires_segment_info +def current_tag(segment_info, pl, flags='s'): + '''Return tag that is near the cursor. + + :param str flags: + Specifies additional properties of the displayed tag. Supported values: + + * s - display complete signature + * f - display the full hierarchy of the tag + * p - display the raw prototype + + More info in the `official documentation`_ (search for + “tagbar#currenttag”). + + .. _`official documentation`: https://github.com/majutsushi/tagbar/blob/master/doc/tagbar.txt + ''' + global currenttag + global tag_cache + window_id = segment_info['window_id'] + if segment_info['mode'] == 'nc': + return tag_cache.get(window_id, (None,))[-1] + if not currenttag: + if vim_command_exists('Tagbar'): + currenttag = vim_get_autoload_func('tagbar#currenttag') + if not currenttag: + return None + else: + return None + prev_key, r = tag_cache.get(window_id, (None, None)) + key = (int(vim.eval('b:changedtick')), segment_info['window'].cursor[0]) + if prev_key and key == prev_key: + return r + r = currenttag('%s', '', flags) + tag_cache[window_id] = (key, r) + return r |