diff --git a/.travis.yml b/.travis.yml index 01429651..7546af90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,12 +24,9 @@ matrix: - python: "3.2" - python: "3.3" - python: "3.4" + - python: "3.5" - python: "pypy" - python: "pypy3" - - python: "2.6" - env: >- - USE_UCS2_PYTHON=1 - UCS2_PYTHON_VARIANT="2.6" - python: "2.7" env: >- USE_UCS2_PYTHON=1 diff --git a/MANIFEST.in b/MANIFEST.in index e3e42f79..14acc932 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ recursive-include powerline *.json *.vim recursive-include powerline/bindings *.* recursive-exclude powerline/bindings *.pyc *.pyo +recursive-include powerline/dist *.* recursive-include client *.* recursive-include docs/source *.rst *.py include docs/Makefile diff --git a/docs/source/configuration/listers.rst b/docs/source/configuration/listers.rst index 04e5371e..e6ba06e7 100644 --- a/docs/source/configuration/listers.rst +++ b/docs/source/configuration/listers.rst @@ -16,8 +16,6 @@ their type and ``segments`` key with a list of segments (a bit more details in More information in :ref:`Writing listers ` section. -Currently only Vim listers are available. - Vim listers ----------- @@ -29,3 +27,9 @@ Pdb listers .. automodule:: powerline.listers.pdb :members: + +i3wm listers +---------- + +.. automodule:: powerline.listers.i3wm + :members: diff --git a/docs/source/configuration/local.rst b/docs/source/configuration/local.rst index 5ea233f4..0f3d1104 100644 --- a/docs/source/configuration/local.rst +++ b/docs/source/configuration/local.rst @@ -12,6 +12,8 @@ Vim overrides Vim configuration can be overridden using the following options: +.. _local-configuration-overrides-vim-config: + ``g:powerline_config_overrides`` Dictionary, recursively merged with contents of :file:`powerline/config.json`. @@ -37,6 +39,17 @@ Vim configuration can be overridden using the following options: was configured in :ref:`log_* options `. Level is always :ref:`log_level `, same for format. + .. warning:: + This variable is deprecated. Use :ref:`log_file option + ` in conjunction with + :py:class:`powerline.vim.VimVarHandler` class and :ref:`Vim config + overrides variable `. Using + this is also the only variant to make saving into the environment + variable the *only* place where log is saved or save into different + variable. + + .. autoclass:: powerline.vim.VimVarHandler + .. _local-configuration-overrides-script: Powerline script overrides diff --git a/docs/source/configuration/reference.rst b/docs/source/configuration/reference.rst index 55841415..99b32be1 100644 --- a/docs/source/configuration/reference.rst +++ b/docs/source/configuration/reference.rst @@ -94,14 +94,38 @@ Common configuration is a subdictionary that is a value of ``common`` key in .. _config-common-log: ``log_file`` - Defines path which will hold powerline logs. If not present, logging will be - done to stderr. + Defines how logs will be handled. There are three variants here: + + #. Absent. In this case logging will be done to stderr: equivalent to + ``[["logging.StreamHandler", []]]`` or ``[null]``. + #. Plain string. In this case logging will be done to the given file: + ``"/file/name"`` is equivalent to ``[["logging.FileHandler", + [["/file/name"]]]]`` or ``["/file/name"]``. Leading ``~/`` is expanded in + the file name, so using ``"~/.log/foo"`` is permitted. If directory + pointed by the option is absent, it will be created, but not its parent. + #. List of handler definitions. Handler definition may either be ``null``, + a string or a list with two or three elements: + + #. Logging class name and module. If module name is absent, it is + equivalent to ``logging.handlers``. + #. Class constructor arguments in a form ``[[args[, kwargs]]]``: accepted + variants are ``[]`` (no arguments), ``[args]`` (e.g. + ``[["/file/name"]]``: only positional arguments) or ``[args, kwargs]`` + (e.g. ``[[], {"host": "localhost", "port": 6666}]``: positional and + keyword arguments, but no positional arguments in the example). + #. Optional logging level. Overrides :ref:`log_level key + ` and has the same format. + #. Optional format string. Partially overrides :ref:`log_format key + ` and has the same format. “Partially” here + means that it may only specify more critical level. .. _config-common-log_level: ``log_level`` String, determines logging level. Defaults to ``WARNING``. +.. _config-common-log_format: + ``log_format`` String, determines format of the log messages. Defaults to ``'%(asctime)s:%(level)s:%(message)s'``. @@ -163,6 +187,10 @@ Common configuration is a subdictionary that is a value of ``ext`` key in ``out`` and ``rewrite`` prompts (refer to IPython documentation for more details) while ``in`` prompt is the default. + For wm (:ref:`lemonbar ` only) it is a dictionary + ``{output : theme_name}`` that maps the ``xrandr`` output names to the + local themes to use on that output. + ``components`` Determines which extension components should be enabled. This key is highly extension-specific, here is the table of extensions and corresponding diff --git a/docs/source/develop/segments.rst b/docs/source/develop/segments.rst index d96bb1cb..543ddd57 100644 --- a/docs/source/develop/segments.rst +++ b/docs/source/develop/segments.rst @@ -305,7 +305,7 @@ Segment dictionary contains the following keys: ``side`` Segment side: ``right`` or ``left``. - ``display_condition``` + ``display_condition`` Contains function that takes three position parameters: :py:class:`powerline.PowerlineLogger` instance, :ref:`segment_info ` dictionary and current mode and returns either ``True`` @@ -517,6 +517,22 @@ Pdb Equal to the length of :py:attr:`pdb.Pdb.stack` at the first invocation of the prompt decremented by one. +i3wm +---- + +``mode`` + Currently active i3 mode (as a string). + +``output`` + ``xrandr`` output name currently drawing to. Currently only available + in lemonbar bindings. + +``workspace`` + dictionary containing the workspace name under the key ``"name"`` and + boolean values for the ``"visible"``, ``"urgent"`` and ``"focused"`` + keys, indicating the state of the workspace. Currently only provided by + the :py:func:`powerline.listers.i3wm.workspace_lister` lister. + Segment class ============= diff --git a/docs/source/installation.rst b/docs/source/installation.rst index be786480..507b432b 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -30,8 +30,9 @@ Generic requirements with bazaar repositories. * ``pyuv`` python package. Required for :ref:`libuv-based watcher ` to work. -* ``i3-py``, `available on github `_. Required - for i3wm bindings and segments. +* ``i3-ipc`` python package. Required for i3wm bindings and segments. +* ``xrandr`` program. Required for the multi-monitor lemonbar binding and the + :py:func:`powerline.listers.i3wm.output_lister`. .. note:: Until mercurial and bazaar support Python-3 or PyPy powerline will not diff --git a/docs/source/installation/osx.rst b/docs/source/installation/osx.rst index 5a85e692..e520348d 100644 --- a/docs/source/installation/osx.rst +++ b/docs/source/installation/osx.rst @@ -11,12 +11,10 @@ Python package sudo port select python python27-apple - . Homebrew may be used here:: + Homebrew may be used here:: brew install python - . - .. note:: In case :file:`powerline.sh` as a client ``socat`` and ``coreutils`` need to be installed. ``coreutils`` may be installed using ``brew install @@ -45,7 +43,7 @@ Python package ``powerline-status`` in PyPI. .. note:: - Powerline developers should be aware that``pip install --editable`` does + Powerline developers should be aware that ``pip install --editable`` does not currently fully work. Installation performed this way are missing ``powerline`` executable that needs to be symlinked. It will be located in ``scripts/powerline``. @@ -56,13 +54,13 @@ Vim installation Any terminal vim version with Python 3.2+ or Python 2.6+ support should work, but MacVim users need to install it using the following command:: - brew install macvim --env-std --override-system-vim + brew install macvim --env-std --with-override-system-vim Fonts installation ================== -Install downloaded patched font by double-clicking the font file in Finder, then -clicking :guilabel:`Install this font` in the preview window. +To install patched font double-click the font file in Finder, then click +:guilabel:`Install this font` in the preview window. After installing the patched font MacVim or terminal emulator (whatever application powerline should work with) need to be configured to use the patched diff --git a/docs/source/powerline_automan.py b/docs/source/powerline_automan.py index 1f79001a..85d241cb 100644 --- a/docs/source/powerline_automan.py +++ b/docs/source/powerline_automan.py @@ -6,6 +6,7 @@ import re import codecs from collections import namedtuple +from argparse import REMAINDER from functools import reduce @@ -61,9 +62,9 @@ def parse_argument(*args, **kwargs): is_option = args[0].startswith('-') is_long_option = args[0].startswith('--') is_short_option = is_option and not is_long_option - action = kwargs.get('action', 'store_true') - multi = kwargs.get('action') in ('append',) - nargs = kwargs.get('nargs') or (1 if kwargs.get('metavar') or action in ('append',) else 0) + action = kwargs.get('action', 'store') + multi = kwargs.get('action') in ('append',) or kwargs.get('nargs') is REMAINDER + nargs = kwargs.get('nargs', (1 if action in ('append', 'store') else 0)) return Argument( names=args, help=u(kwargs.get('help', '')), @@ -165,9 +166,20 @@ def format_usage_arguments(arguments, base_length=None): # `--` is automatically transformed into – (EN DASH) # when parsing into HTML. We do not need this. line[-1] += [nodes.Text(char) for char in name] + elif argument.nargs is REMAINDER: + line.append(nodes.Text('[')) + line.append(nodes.strong()) + line[-1] += [nodes.Text(char) for char in '--'] + line.append(nodes.Text('] ')) if argument.nargs: - assert(argument.nargs in (1, '?')) - with SurroundWith(line, argument.nargs == '?' and argument.is_option): + assert(argument.nargs in (1, '?', REMAINDER)) + with SurroundWith( + line, ( + True + if argument.nargs is REMAINDER + else (argument.nargs == '?' and argument.is_option) + ) + ): if argument.is_long_option: line.append(nodes.Text('=')) line.append(nodes.emphasis(text=argument.metavar)) @@ -337,15 +349,21 @@ class AutoManParser(object): class AutoMan(Directive): required_arguments = 1 optional_arguments = 0 - option_spec = dict(prog=unchanged_required) + option_spec = dict(prog=unchanged_required, minimal=bool) has_content = False def run(self): + minimal = self.options.get('minimal') module = self.arguments[0] template_args = {} template_args.update(get_authors()) get_argparser = __import__(str(module), fromlist=[str('get_argparser')]).get_argparser parser = get_argparser(AutoManParser) + if minimal: + container = nodes.container() + container += parser.automan_usage(self.options['prog']) + container += parser.automan_description() + return [container] synopsis_section = nodes.section( '', nodes.title(text='Synopsis'), diff --git a/docs/source/troubleshooting/osx.rst b/docs/source/troubleshooting/osx.rst index cb7e56b8..c2251e66 100644 --- a/docs/source/troubleshooting/osx.rst +++ b/docs/source/troubleshooting/osx.rst @@ -7,7 +7,8 @@ I can’t see any fancy symbols, what’s wrong? * If you’re using iTerm2, please update to `this revision `_ - or newer. + or newer. Also make sure that Preferences>Profiles>Text>Non-ASCII Font is the same as + your main Font. * You need to set your ``LANG`` and ``LC_*`` environment variables to a UTF-8 locale (e.g. ``LANG=en_US.utf8``). Consult your Linux distro’s documentation for information about setting these variables correctly. diff --git a/docs/source/usage/other.rst b/docs/source/usage/other.rst index e08c0632..72bff6d1 100644 --- a/docs/source/usage/other.rst +++ b/docs/source/usage/other.rst @@ -89,8 +89,8 @@ root `):: .. note:: The availability of the ``powerline-config`` command is required for - powerline support. DLlocation of this script may be specified via - ``$POWERLINE_CONFIG_COMMAND`` environment variable. + powerline support. The location of this script may be specified via + the ``$POWERLINE_CONFIG_COMMAND`` environment variable. .. note:: It is advised to run ``powerline-daemon`` before adding the above line to @@ -114,11 +114,12 @@ For IPython<0.11 add the following lines to :file:`.ipython/ipy_user_conf.py`: # create skeleton ipy_user_conf.py file): powerline_setup() -For IPython>=0.11 add the following line to :file:`ipython_config.py` file in -the used profile: +For IPython>=0.11 add the following line to +:file:`~/.ipython/profile_default/ipython_config.py` file in the used profile: .. code-block:: Python + c = get_config() c.InteractiveShellApp.extensions = [ 'powerline.bindings.ipython.post_0_11' ] diff --git a/docs/source/usage/wm-widgets.rst b/docs/source/usage/wm-widgets.rst index 6c0806e2..1aa1a2de 100644 --- a/docs/source/usage/wm-widgets.rst +++ b/docs/source/usage/wm-widgets.rst @@ -50,26 +50,35 @@ Add the following to :file:`~/.config/qtile/config.py`: ), ] -.. _bar-usage: +.. _lemonbar-usage: -bar-aint-recursive -================== +lemonbar (formerly bar-aint-recursive) +====================================== -To run the bar simply pipe the output of the binding script into ``bar`` and -specify appropriate options, for example like this:: +To run the bar simply start the binding script: - python /path/to/powerline/bindings/bar/powerline-bar.py | bar + python /path/to/powerline/bindings/lemonbar/powerline-lemonbar.py -to run with i3, simply ``exec`` this in i3 config file:: +You can specify options to be passed to ``lemonbar`` after ``--``, like so: - exec python /path/to/powerline/bindings/bar/powerline-bar.py --i3 | bar + python /path/to/powerline/bindings/lemonbar/powerline-lemonbar.py --height 16 -- -f "Source Code Pro for Powerline-9" + +to run with i3, simply ``exec`` this in the i3 config file and set the ``--i3`` switch: + + exec python /path/to/powerline/bindings/lemonbar/powerline-lemonbar.py --i3 Running the binding in i3-mode will require `i3ipc `_ (or the outdated `i3-py `_). -See the `bar documentation `_ for more +See the `lemonbar documentation `_ for more information and options. +All ``powerline-lemonbar.py`` arguments: + +.. automan:: powerline.commands.lemonbar + :prog: powerline-lemonbar.py + :minimal: true + I3 bar ====== diff --git a/powerline/__init__.py b/powerline/__init__.py index 9d2bb689..cd9e47d9 100644 --- a/powerline/__init__.py +++ b/powerline/__init__.py @@ -9,7 +9,7 @@ from threading import Lock, Event from powerline.colorscheme import Colorscheme from powerline.lib.config import ConfigLoader -from powerline.lib.unicode import safe_unicode, FailedUnicode +from powerline.lib.unicode import unicode, safe_unicode, FailedUnicode from powerline.config import DEFAULT_SYSTEM_CONFIG_DIR from powerline.lib.dict import mergedicts from powerline.lib.encoding import get_preferred_output_encoding @@ -121,7 +121,7 @@ def get_fallback_logger(stream=None): handler.setLevel(level) handler.setFormatter(formatter) - logger = logging.getLogger('powerline') + logger = logging.Logger('powerline') logger.setLevel(level) logger.addHandler(handler) _fallback_logger = PowerlineLogger(None, logger, '_fallback_') @@ -200,40 +200,102 @@ def load_config(cfg_path, find_config_files, config_loader, loader_callback=None return ret -def _get_log_handler(common_config, stream=None): - '''Get log handler. +def _set_log_handlers(common_config, logger, get_module_attr, stream=None): + '''Set log handlers :param dict common_config: Configuration dictionary used to create handler. - - :return: logging.Handler subclass. + :param logging.Logger logger: + Logger to which handlers will be attached. + :param func get_module_attr: + :py:func:`gen_module_attr_getter` output. + :param file stream: + Stream to use by default for :py:class:`logging.StreamHandler` in place + of :py:attr:`sys.stderr`. May be ``None``. ''' - log_file = common_config['log_file'] - if log_file: - log_file = os.path.expanduser(log_file) - log_dir = os.path.dirname(log_file) - if not os.path.isdir(log_dir): - os.mkdir(log_dir) - return logging.FileHandler(log_file) - else: - return logging.StreamHandler(stream) + log_targets = common_config['log_file'] + num_handlers = 0 + for log_target in log_targets: + if log_target is None: + log_target = ['logging.StreamHandler', []] + elif isinstance(log_target, unicode): + log_target = os.path.expanduser(log_target) + log_dir = os.path.dirname(log_target) + if log_dir and not os.path.isdir(log_dir): + os.mkdir(log_dir) + log_target = ['logging.FileHandler', [[log_target]]] + module, handler_class_name = log_target[0].rpartition('.')[::2] + module = module or 'logging.handlers' + try: + handler_class_args = log_target[1][0] + except IndexError: + if module == 'logging' and handler_class_name == 'StreamHandler': + handler_class_args = [stream] + else: + handler_class_args = () + try: + handler_class_kwargs = log_target[1][1] + except IndexError: + handler_class_kwargs = {} + module = str(module) + handler_class_name = str(handler_class_name) + handler_class = get_module_attr(module, handler_class_name) + if not handler_class: + continue + handler = handler_class(*handler_class_args, **handler_class_kwargs) + try: + handler_level_name = log_target[2] + except IndexError: + handler_level_name = common_config['log_level'] + try: + handler_format = log_target[3] + except IndexError: + handler_format = common_config['log_format'] + handler.setLevel(getattr(logging, handler_level_name)) + handler.setFormatter(logging.Formatter(handler_format)) + logger.addHandler(handler) + num_handlers += 1 + if num_handlers == 0 and log_targets: + raise ValueError('Failed to set up any handlers') -def create_logger(common_config, stream=None): +def create_logger(common_config, use_daemon_threads=True, ext='__unknown__', + import_paths=None, imported_modules=None, stream=None): '''Create logger according to provided configuration + + :param dict common_config: + Common configuration, from :py:func:`finish_common_config`. + :param bool use_daemon_threads: + Whether daemon threads should be used. Argument to + :py:class:`PowerlineLogger` constructor. + :param str ext: + Used extension. Argument to :py:class:`PowerlineLogger` constructor. + :param set imported_modules: + Set where imported modules are saved. Argument to + :py:func:`gen_module_attr_getter`. May be ``None``, in this case new + empty set is used. + :param file stream: + Stream to use by default for :py:class:`logging.StreamHandler` in place + of :py:attr:`sys.stderr`. May be ``None``. + + :return: Three objects: + + #. :py:class:`logging.Logger` instance. + #. :py:class:`PowerlineLogger` instance. + #. Function, output of :py:func:`gen_module_attr_getter`. ''' - log_format = common_config['log_format'] - formatter = logging.Formatter(log_format) - + logger = logging.Logger('powerline') level = getattr(logging, common_config['log_level']) - handler = _get_log_handler(common_config, stream) - handler.setLevel(level) - handler.setFormatter(formatter) - - logger = logging.getLogger('powerline') logger.setLevel(level) - logger.addHandler(handler) - return logger + + pl = PowerlineLogger(use_daemon_threads, logger, ext) + get_module_attr = gen_module_attr_getter( + pl, common_config['paths'], + set() if imported_modules is None else imported_modules) + + _set_log_handlers(common_config, logger, get_module_attr, stream) + + return logger, pl, get_module_attr def finish_common_config(encoding, common_config): @@ -264,7 +326,10 @@ def finish_common_config(encoding, common_config): common_config.setdefault('additional_escapes', None) common_config.setdefault('reload_config', True) common_config.setdefault('interval', None) - common_config.setdefault('log_file', None) + common_config.setdefault('log_file', [None]) + + if not isinstance(common_config['log_file'], list): + common_config['log_file'] = [common_config['log_file']] common_config['paths'] = [ os.path.expanduser(path) for path in common_config['paths'] @@ -324,6 +389,26 @@ def gen_module_attr_getter(pl, import_paths, imported_modules): return get_module_attr +LOG_KEYS = set(('log_format', 'log_level', 'log_file', 'paths')) +'''List of keys related to logging +''' + + +def _get_log_keys(common_config): + '''Return a common configuration copy with only log-related config left + + :param dict common_config: + Common configuration. + + :return: + :py:class:`dict` instance which has only keys from + :py:attr:`powerline.LOG_KEYS` left. + ''' + return dict(( + (k, v) for k, v in common_config.items() if k in LOG_KEYS + )) + + class Powerline(object): '''Main powerline class, entrance point for all powerline uses. Sets powerline up and loads the configuration. @@ -380,6 +465,7 @@ class Powerline(object): self.ext = ext self.run_once = run_once self.logger = logger + self.had_logger = bool(self.logger) self.use_daemon_threads = use_daemon_threads if not renderer_module: @@ -430,8 +516,20 @@ class Powerline(object): This function is used to create logger unless it was already specified at initialization. + + :return: Three objects: + + #. :py:class:`logging.Logger` instance. + #. :py:class:`PowerlineLogger` instance. + #. Function, output of :py:func:`gen_module_attr_getter`. ''' - return create_logger(self.common_config, self.default_log_stream) + return create_logger( + common_config=self.common_config, + use_daemon_threads=self.use_daemon_threads, + ext=self.ext, + imported_modules=self.imported_modules, + stream=self.default_log_stream, + ) def create_renderer(self, load_main=False, load_colors=False, load_colorscheme=False, load_theme=False): '''(Re)create renderer object. Can be used after Powerline object was @@ -465,22 +563,24 @@ class Powerline(object): or not self.prev_common_config or self.prev_common_config['default_top_theme'] != self.common_config['default_top_theme']) + log_keys_differ = (not self.prev_common_config or ( + _get_log_keys(self.prev_common_config) != _get_log_keys(self.common_config) + )) + self.prev_common_config = self.common_config - self.import_paths = self.common_config['paths'] - - if not self.logger: - self.logger = self.create_logger() - - if not self.pl: - self.pl = PowerlineLogger(self.use_daemon_threads, self.logger, self.ext) + if log_keys_differ: + if self.had_logger: + self.pl = PowerlineLogger(self.use_daemon_threads, self.logger, self.ext) + self.get_module_attr = gen_module_attr_getter( + self.pl, self.common_config['paths'], self.imported_modules) + else: + self.logger, self.pl, self.get_module_attr = self.create_logger() self.config_loader.pl = self.pl if not self.run_once: self.config_loader.set_watcher(self.common_config['watcher']) - self.get_module_attr = gen_module_attr_getter(self.pl, self.import_paths, self.imported_modules) - mergedicts(self.renderer_options, dict( pl=self.pl, term_truecolor=self.common_config['term_truecolor'], diff --git a/powerline/bindings/bar/powerline-bar.py b/powerline/bindings/bar/powerline-bar.py index 35496f6f..05ef7685 100755 --- a/powerline/bindings/bar/powerline-bar.py +++ b/powerline/bindings/bar/powerline-bar.py @@ -2,31 +2,27 @@ # vim:fileencoding=utf-8:noet from __future__ import (unicode_literals, division, absolute_import, print_function) +import os import sys import time from threading import Lock, Timer from argparse import ArgumentParser -from powerline import Powerline +from powerline.lemonbar import LemonbarPowerline from powerline.lib.encoding import get_unicode_writer -class BarPowerline(Powerline): - get_encoding = staticmethod(lambda: 'utf-8') - - def init(self): - super(BarPowerline, self).init(ext='wm', renderer_module='bar') - - if __name__ == '__main__': - parser = ArgumentParser(description='Powerline BAR bindings.') + parser = ArgumentParser(description='Powerline lemonbar bindings.') parser.add_argument( '--i3', action='store_true', help='Subscribe for i3 events.' ) args = parser.parse_args() - powerline = BarPowerline() + powerline = LemonbarPowerline() + powerline.update_renderer() + powerline.pl.warn("The 'bar' bindings are deprecated, please switch to 'lemonbar'") lock = Lock() modes = ['default'] write = get_unicode_writer(encoding='utf-8') @@ -60,4 +56,4 @@ if __name__ == '__main__': conn.main() while True: - time.sleep(1e10) + time.sleep(1e8) diff --git a/powerline/bindings/config.py b/powerline/bindings/config.py index aedee89e..0db902be 100644 --- a/powerline/bindings/config.py +++ b/powerline/bindings/config.py @@ -9,7 +9,7 @@ import shlex from powerline.config import POWERLINE_ROOT, TMUX_CONFIG_DIRECTORY from powerline.lib.config import ConfigLoader -from powerline import generate_config_finder, load_config, create_logger, PowerlineLogger, finish_common_config +from powerline import generate_config_finder, load_config, create_logger, finish_common_config from powerline.shell import ShellPowerline from powerline.lib.shell import which from powerline.bindings.tmux import (TmuxVersionInfo, run_tmux_command, set_tmux_environment, get_tmux_version, @@ -221,8 +221,8 @@ def get_main_config(args): def create_powerline_logger(args): config = get_main_config(args) common_config = finish_common_config(get_preferred_output_encoding(), config['common']) - logger = create_logger(common_config) - return PowerlineLogger(use_daemon_threads=True, logger=logger, ext='config') + logger, pl, get_module_attr = create_logger(common_config) + return pl def check_command(cmd): diff --git a/powerline/bindings/lemonbar/powerline-lemonbar.py b/powerline/bindings/lemonbar/powerline-lemonbar.py new file mode 100755 index 00000000..76372541 --- /dev/null +++ b/powerline/bindings/lemonbar/powerline-lemonbar.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import time +import re +import subprocess + +from threading import Lock, Timer + +from powerline.lemonbar import LemonbarPowerline +from powerline.commands.lemonbar import get_argparser +from powerline.bindings.wm import get_connected_xrandr_outputs + + +if __name__ == '__main__': + parser = get_argparser() + args = parser.parse_args() + + powerline = LemonbarPowerline() + powerline.update_renderer() + bars = [] + + for screen in get_connected_xrandr_outputs(powerline.pl): + command = [args.bar_command, '-g', '{0}x{1}+{2}'.format(screen['width'], args.height, screen['x'])] + args.args[1:] + process = subprocess.Popen(command, stdin=subprocess.PIPE) + bars.append((screen['name'], process, int(screen['width']) / 5)) + + lock = Lock() + modes = ['default'] + + def render(reschedule=False): + if reschedule: + Timer(args.interval, render, kwargs={'reschedule': True}).start() + + global lock + with lock: + for output, process, width in bars: + process.stdin.write(powerline.render(mode=modes[0], width=width, matcher_info=output).encode('utf-8') + b'\n') + process.stdin.flush() + + def update(evt): + modes[0] = evt.change + render() + + render(reschedule=True) + + if args.i3: + try: + import i3ipc + except ImportError: + import i3 + i3.Subscription(lambda evt, data, sub: render(), 'workspace') + else: + conn = i3ipc.Connection() + conn.on('workspace::focus', lambda conn, evt: render()) + conn.on('mode', lambda conn, evt: update(evt)) + conn.main() + + while True: + time.sleep(1e8) diff --git a/powerline/bindings/tmux/powerline-base.conf b/powerline/bindings/tmux/powerline-base.conf index a17f0c41..77791581 100644 --- a/powerline/bindings/tmux/powerline-base.conf +++ b/powerline/bindings/tmux/powerline-base.conf @@ -1,5 +1,4 @@ set -g status on -set -g status-utf8 on set -g status-interval 2 set -g status-left-length 20 set -g status-right '#(env "$POWERLINE_COMMAND" $POWERLINE_COMMAND_ARGS tmux right -R pane_id=\"`tmux display -p "#D"`\")' diff --git a/powerline/bindings/vim/__init__.py b/powerline/bindings/vim/__init__.py index e617943a..b754c1f3 100644 --- a/powerline/bindings/vim/__init__.py +++ b/powerline/bindings/vim/__init__.py @@ -273,7 +273,7 @@ def _vim_to_python(value): if hasattr(vim, 'options'): def vim_getbufoption(info, option): - return info['buffer'].options[str(option)] + return _vim_to_python(info['buffer'].options[str(option)]) def vim_getoption(option): return vim.options[str(option)] diff --git a/powerline/bindings/wm/__init__.py b/powerline/bindings/wm/__init__.py new file mode 100644 index 00000000..0f2cc9d6 --- /dev/null +++ b/powerline/bindings/wm/__init__.py @@ -0,0 +1,38 @@ +# 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.lib.shell import run_cmd + + +conn = None + + +def get_i3_connection(): + '''Return a valid, cached i3 Connection instance + ''' + global conn + if not conn: + try: + import i3ipc + except ImportError: + import i3 as conn + else: + conn = i3ipc.Connection() + return conn + + +XRANDR_OUTPUT_RE = re.compile(r'^(?P[0-9A-Za-z-]+) connected(?P primary)? (?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+)', re.MULTILINE) + + +def get_connected_xrandr_outputs(pl): + '''Iterate over xrandr outputs + + Outputs are represented by a dictionary with ``name``, ``width``, + ``height``, ``primary``, ``x`` and ``y`` keys. + ''' + return (match.groupdict() for match in XRANDR_OUTPUT_RE.finditer( + run_cmd(pl, ['xrandr', '-q']) + )) diff --git a/powerline/commands/lemonbar.py b/powerline/commands/lemonbar.py new file mode 100644 index 00000000..547c52cc --- /dev/null +++ b/powerline/commands/lemonbar.py @@ -0,0 +1,35 @@ +# vim:fileencoding=utf-8:noet +# WARNING: using unicode_literals causes errors in argparse +from __future__ import (division, absolute_import, print_function) + +import argparse + + +def get_argparser(ArgumentParser=argparse.ArgumentParser): + parser = ArgumentParser( + description='Powerline BAR bindings.' + ) + parser.add_argument( + '--i3', action='store_true', + help='Subscribe for i3 events.' + ) + parser.add_argument( + '--height', default='', + metavar='PIXELS', help='Bar height.' + ) + parser.add_argument( + '--interval', '-i', + type=float, default=0.5, + metavar='SECONDS', help='Refresh interval.' + ) + parser.add_argument( + '--bar-command', '-C', + default='lemonbar', + metavar='CMD', help='Name of the lemonbar executable to use.' + ) + parser.add_argument( + 'args', nargs=argparse.REMAINDER, + help='Extra arguments for lemonbar. Should be preceded with ``--`` ' + 'argument in order not to be confused with script own arguments.' + ) + return parser diff --git a/powerline/config_files/colorschemes/vim/__main__.json b/powerline/config_files/colorschemes/vim/__main__.json index 0d460b59..1ce2e7b3 100644 --- a/powerline/config_files/colorschemes/vim/__main__.json +++ b/powerline/config_files/colorschemes/vim/__main__.json @@ -23,16 +23,25 @@ "csv:column_number": "line_current", "csv:column_name": "line_current_symbol", + "tab:background": "background", + "tab:divider": "background:divider", + "tab_nc:modified_indicator": "modified_indicator", "tab_nc:file_directory": "information:unimportant", "tab_nc:file_name": "tab_nc:file_directory", "tab_nc:tabnr": "tab_nc:file_directory", "buf_nc:file_directory": "tab_nc:file_directory", - "buf_nc:file_name": "tab_nc:file_name", - "buf_nc:bufnr": "tab_nc:tabnr", + "buf_nc:file_name": "buf_nc:file_directory", + "buf_nc:bufnr": "buf_nc:file_directory", "buf_nc:modified_indicator": "tab_nc:modified_indicator", + "buf_nc_mod:file_directory": "tab_nc:file_directory", + "buf_nc_mod:file_name": "buf_nc_mod:file_directory", + "buf_nc_mod:bufnr": "buf_nc_mod:file_directory", + "buf_nc_mod:modified_indicator": "tab_nc:modified_indicator", + + "commandt:label": "file_name", "commandt:background": "background", "commandt:finder": "file_name", diff --git a/powerline/config_files/themes/ascii.json b/powerline/config_files/themes/ascii.json index 9e876734..7b12d446 100644 --- a/powerline/config_files/themes/ascii.json +++ b/powerline/config_files/themes/ascii.json @@ -131,6 +131,15 @@ "args": { "text": "+" } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "O", + "changed": "X" + } + } } } } diff --git a/powerline/config_files/themes/powerline.json b/powerline/config_files/themes/powerline.json index c33b5c1f..4ca9f0ee 100644 --- a/powerline/config_files/themes/powerline.json +++ b/powerline/config_files/themes/powerline.json @@ -129,6 +129,15 @@ "args": { "text": "+" } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } } } } diff --git a/powerline/config_files/themes/powerline_unicode7.json b/powerline/config_files/themes/powerline_unicode7.json index bfa86fe0..d470d3a9 100644 --- a/powerline/config_files/themes/powerline_unicode7.json +++ b/powerline/config_files/themes/powerline_unicode7.json @@ -143,6 +143,15 @@ "args": { "text": "🖫⃥" } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } } } } diff --git a/powerline/config_files/themes/unicode.json b/powerline/config_files/themes/unicode.json index eadfc870..4b52fd3b 100644 --- a/powerline/config_files/themes/unicode.json +++ b/powerline/config_files/themes/unicode.json @@ -129,6 +129,15 @@ "args": { "text": "+" } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } } } } diff --git a/powerline/config_files/themes/unicode_terminus.json b/powerline/config_files/themes/unicode_terminus.json index 8c1e045a..b7b005e4 100644 --- a/powerline/config_files/themes/unicode_terminus.json +++ b/powerline/config_files/themes/unicode_terminus.json @@ -129,6 +129,15 @@ "args": { "text": "+" } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } } } } diff --git a/powerline/config_files/themes/unicode_terminus_condensed.json b/powerline/config_files/themes/unicode_terminus_condensed.json index 1c567dc7..fc9e90aa 100644 --- a/powerline/config_files/themes/unicode_terminus_condensed.json +++ b/powerline/config_files/themes/unicode_terminus_condensed.json @@ -130,6 +130,15 @@ "args": { "text": "+" } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } } } } diff --git a/powerline/config_files/themes/vim/tabline.json b/powerline/config_files/themes/vim/tabline.json index 48bae6b8..1e3130ec 100644 --- a/powerline/config_files/themes/vim/tabline.json +++ b/powerline/config_files/themes/vim/tabline.json @@ -67,7 +67,7 @@ }, { "type": "string", - "highlight_groups": ["background"], + "highlight_groups": ["tab:background"], "draw_soft_divider": false, "draw_hard_divider": false, "width": "auto" diff --git a/powerline/lemonbar.py b/powerline/lemonbar.py new file mode 100644 index 00000000..b49f86b5 --- /dev/null +++ b/powerline/lemonbar.py @@ -0,0 +1,21 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline import Powerline +from powerline.lib.dict import mergedicts + + +class LemonbarPowerline(Powerline): + def init(self): + super(LemonbarPowerline, self).init(ext='wm', renderer_module='lemonbar') + + get_encoding = staticmethod(lambda: 'utf-8') + + def get_local_themes(self, local_themes): + if not local_themes: + return {} + + return dict(( + (key, {'config': self.load_theme_config(val)}) + for key, val in local_themes.items() + )) diff --git a/powerline/lib/dict.py b/powerline/lib/dict.py index d8d2088b..c06ab30c 100644 --- a/powerline/lib/dict.py +++ b/powerline/lib/dict.py @@ -78,3 +78,11 @@ def mergedicts_copy(d1, d2): else: ret[k] = d2[k] return ret + + +def updated(d, *args, **kwargs): + '''Copy dictionary and update it with provided arguments + ''' + d = d.copy() + d.update(*args, **kwargs) + return d diff --git a/powerline/lib/encoding.py b/powerline/lib/encoding.py index 798fc396..76a51d81 100644 --- a/powerline/lib/encoding.py +++ b/powerline/lib/encoding.py @@ -43,9 +43,15 @@ def get_preferred_output_encoding(): Falls back to ASCII, so that output is most likely to be displayed correctly. ''' + if hasattr(locale, 'LC_MESSAGES'): + return ( + locale.getlocale(locale.LC_MESSAGES)[1] + or locale.getdefaultlocale()[1] + or 'ascii' + ) + return ( - locale.getlocale(locale.LC_MESSAGES)[1] - or locale.getdefaultlocale()[1] + locale.getdefaultlocale()[1] or 'ascii' ) @@ -57,9 +63,15 @@ def get_preferred_input_encoding(): Falls back to latin1 so that function is less likely to throw as decoded output is primary searched for ASCII values. ''' + if hasattr(locale, 'LC_MESSAGES'): + return ( + locale.getlocale(locale.LC_MESSAGES)[1] + or locale.getdefaultlocale()[1] + or 'latin1' + ) + return ( - locale.getlocale(locale.LC_MESSAGES)[1] - or locale.getdefaultlocale()[1] + locale.getdefaultlocale()[1] or 'latin1' ) diff --git a/powerline/lib/shell.py b/powerline/lib/shell.py index fcdc35ce..2082e82e 100644 --- a/powerline/lib/shell.py +++ b/powerline/lib/shell.py @@ -7,7 +7,7 @@ import os from subprocess import Popen, PIPE from functools import partial -from powerline.lib.encoding import get_preferred_input_encoding +from powerline.lib.encoding import get_preferred_input_encoding, get_preferred_output_encoding if sys.platform.startswith('win32'): @@ -36,7 +36,8 @@ def run_cmd(pl, cmd, stdin=None, strip=True): pl.exception('Could not execute command ({0}): {1}', e, cmd) return None else: - stdout, err = p.communicate(stdin) + stdout, err = p.communicate( + stdin if stdin is None else stdin.encode(get_preferred_output_encoding())) stdout = stdout.decode(get_preferred_input_encoding()) return stdout.strip() if strip else stdout diff --git a/powerline/lint/__init__.py b/powerline/lint/__init__.py index 72d9da5e..6f7fa4b2 100644 --- a/powerline/lint/__init__.py +++ b/powerline/lint/__init__.py @@ -22,7 +22,7 @@ from powerline.lint.checks import (check_matcher_func, check_ext, check_config, check_segment_function, check_args, get_one_segment_function, check_highlight_groups, check_highlight_group, check_full_segment_data, get_all_possible_functions, check_segment_data_key, register_common_name, - highlight_group_spec) + highlight_group_spec, check_log_file_level, check_logging_handler) from powerline.lint.spec import Spec from powerline.lint.context import Context @@ -56,6 +56,11 @@ ext_spec = Spec( top_theme=top_theme_spec().optional(), ).copy gen_components_spec = (lambda *components: Spec().list(Spec().type(unicode).oneof(set(components)))) +log_level_spec = Spec().re('^[A-Z]+$').func( + (lambda value, *args: (True, True, not hasattr(logging, value))), + (lambda value: 'unknown debugging level {0}'.format(value)) +).copy +log_format_spec = Spec().type(unicode).copy main_spec = (Spec( common=Spec( default_top_theme=top_theme_spec().optional(), @@ -67,21 +72,32 @@ main_spec = (Spec( (lambda value, *args: (True, True, not os.path.exists(os.path.expanduser(value.value)))), (lambda value: 'path does not exist: {0}'.format(value)) ).optional(), - log_file=Spec().type(unicode).func( - ( - lambda value, *args: ( - True, - True, - not os.path.isdir(os.path.dirname(os.path.expanduser(value))) - ) + log_file=Spec().either( + Spec().type(unicode).func( + ( + lambda value, *args: ( + True, + True, + not os.path.isdir(os.path.dirname(os.path.expanduser(value))) + ) + ), + (lambda value: 'directory does not exist: {0}'.format(os.path.dirname(value))) ), - (lambda value: 'directory does not exist: {0}'.format(os.path.dirname(value))) + Spec().list(Spec().either( + Spec().type(unicode, type(None)), + Spec().tuple( + Spec().re(function_name_re).func(check_logging_handler), + Spec().tuple( + Spec().type(list).optional(), + Spec().type(dict).optional(), + ), + log_level_spec().func(check_log_file_level).optional(), + log_format_spec().optional(), + ), + )) ).optional(), - log_level=Spec().re('^[A-Z]+$').func( - (lambda value, *args: (True, True, not hasattr(logging, value))), - (lambda value: 'unknown debugging level {0}'.format(value)) - ).optional(), - log_format=Spec().type(unicode).optional(), + log_level=log_level_spec().optional(), + log_format=log_format_spec().optional(), interval=Spec().either(Spec().cmp('gt', 0.0), Spec().type(type(None))).optional(), reload_config=Spec().type(bool).optional(), watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(), @@ -110,6 +126,12 @@ main_spec = (Spec( select=ext_theme_spec(), ), ).optional(), + wm=ext_spec().update( + local_themes=Spec().unknown_spec( + Spec().re('^[0-9A-Za-z-]+$'), + ext_theme_spec() + ).optional() + ).optional(), ).unknown_spec( check_ext, ext_spec(), diff --git a/powerline/lint/checks.py b/powerline/lint/checks.py index 4c746bf6..c2fcfc45 100644 --- a/powerline/lint/checks.py +++ b/powerline/lint/checks.py @@ -800,3 +800,67 @@ def check_exinclude_function(name, data, context, echoerr): if not func: return True, False, True return True, False, False + + +def check_log_file_level(this_level, data, context, echoerr): + '''Check handler level specified in :ref:`log_file key ` + + This level must be greater or equal to the level in :ref:`log_level key + `. + ''' + havemarks(this_level) + hadproblem = False + top_level = context[0][1].get('common', {}).get('log_level', 'WARNING') + top_level_str = top_level + top_level_mark = getattr(top_level, 'mark', None) + if ( + not isinstance(top_level, unicode) or not hasattr(logging, top_level) + or not isinstance(this_level, unicode) or not hasattr(logging, this_level) + ): + return True, False, hadproblem + top_level = getattr(logging, top_level) + this_level_str = this_level + this_level_mark = this_level.mark + this_level = getattr(logging, this_level) + if this_level < top_level: + echoerr( + context='Error while checking log level index (key {key})'.format( + key=context.key), + context_mark=this_level_mark, + problem='found level that is less critical then top level ({0} < {0})'.format( + this_level_str, top_level_str), + problem_mark=top_level_mark, + ) + hadproblem = True + return True, False, hadproblem + + +def check_logging_handler(handler_name, data, context, echoerr): + havemarks(handler_name) + import_paths = [os.path.expanduser(path) for path in context[0][1].get('common', {}).get('paths', [])] + + handler_module, separator, handler_class = handler_name.rpartition('.') + if not separator: + handler_module = 'logging.handlers' + handler_class = handler_name + with WithPath(import_paths): + try: + handler = getattr(__import__(str(handler_module), fromlist=[str(handler_class)]), str(handler_class)) + except ImportError: + echoerr(context='Error while loading logger class (key {key})'.format(key=context.key), + problem='failed to load module {0}'.format(handler_module), + problem_mark=handler_name.mark) + return True, False, True + except AttributeError: + echoerr(context='Error while loading logger class (key {key})'.format(key=context.key), + problem='failed to load handler class {0}'.format(handler_class), + problem_mark=handler_name.mark) + return True, False, True + + if not issubclass(handler, logging.Handler): + echoerr(context='Error while loading logger class (key {key})'.format(key=context.key), + problem='loaded class {0} is not a logging.Handler subclass'.format(handler_class), + problem_mark=handler_name.mark) + return True, False, True + + return True, False, False diff --git a/powerline/lint/imp.py b/powerline/lint/imp.py index 6e402132..399654e1 100644 --- a/powerline/lint/imp.py +++ b/powerline/lint/imp.py @@ -21,6 +21,12 @@ class WithPath(object): def import_function(function_type, name, data, context, echoerr, module): havemarks(name, module) + if module == 'powerline.segments.i3wm' and name == 'workspaces': + echoerr(context='Warning while checking segments (key {key})'.format(key=context.key), + context_mark=name.mark, + problem='segment {0} from {1} is deprecated'.format(name, module), + problem_mark=module.mark) + with WithPath(data['import_paths']): try: func = getattr(__import__(str(module), fromlist=[str(name)]), str(name)) diff --git a/powerline/lint/spec.py b/powerline/lint/spec.py index e3d072a4..f7b9155c 100644 --- a/powerline/lint/spec.py +++ b/powerline/lint/spec.py @@ -531,7 +531,8 @@ class Spec(object): if max_len == min_len: self.len('eq', len(specs)) else: - self.len('ge', min_len) + if min_len > 0: + self.len('ge', min_len) self.len('le', max_len) start = len(self.specs) diff --git a/powerline/listers/i3wm.py b/powerline/listers/i3wm.py new file mode 100644 index 00000000..0bbcfdc3 --- /dev/null +++ b/powerline/listers/i3wm.py @@ -0,0 +1,68 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info +from powerline.lib.dict import updated +from powerline.bindings.wm import get_i3_connection, get_connected_xrandr_outputs + + +@requires_segment_info +def output_lister(pl, segment_info): + '''List all outputs in segment_info format + ''' + + return ( + ( + updated(segment_info, output=output['name']), + { + 'draw_inner_divider': None + } + ) + for output in get_connected_xrandr_outputs(pl) + ) + + +@requires_segment_info +def workspace_lister(pl, segment_info, only_show=None, output=None): + '''List all workspaces in segment_info format + + Sets the segment info values of ``workspace`` and ``output`` to the name of + the i3 workspace and the ``xrandr`` output respectively and the keys + ``"visible"``, ``"urgent"`` and ``"focused"`` to a boolean indicating these + states. + + :param list only_show: + Specifies which workspaces to list. Valid entries are ``"visible"``, + ``"urgent"`` and ``"focused"``. If omitted or ``null`` all workspaces + are listed. + + :param str output: + May be set to the name of an X output. If specified, only workspaces + on that output are listed. Overrides automatic output detection by + the lemonbar renderer and bindings. Set to ``false`` to force + all workspaces to be shown. + ''' + + if output == None: + output = output or segment_info.get('output') + + return ( + ( + updated( + segment_info, + output=w['output'], + workspace={ + 'name': w['name'], + 'visible': w['visible'], + 'urgent': w['urgent'], + 'focused': w['focused'], + }, + ), + { + 'draw_inner_divider': None + } + ) + for w in get_i3_connection().get_workspaces() + if (((not only_show or any(w[typ] for typ in only_show)) + and (not output or w['output'] == output))) + ) diff --git a/powerline/listers/vim.py b/powerline/listers/vim.py index c1995957..583e0d3a 100644 --- a/powerline/listers/vim.py +++ b/powerline/listers/vim.py @@ -2,12 +2,12 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) from powerline.theme import requires_segment_info -from powerline.bindings.vim import (current_tabpage, list_tabpages, vim_getbufoption) +from powerline.bindings.vim import (current_tabpage, list_tabpages) try: import vim except ImportError: - vim = {} + vim = object() def tabpage_updated_segment_info(segment_info, tabpage): @@ -49,7 +49,10 @@ def tablister(pl, segment_info, **kwargs): return ( (lambda tabpage, prefix: ( tabpage_updated_segment_info(segment_info, tabpage), - add_multiplier(tabpage, {'highlight_group_prefix': prefix}) + add_multiplier(tabpage, { + 'highlight_group_prefix': prefix, + 'divider_highlight_group': 'tab:divider' + }) ))(tabpage, 'tab' if tabpage == cur_tabpage else 'tab_nc') for tabpage in list_tabpages() ) @@ -75,7 +78,8 @@ def bufferlister(pl, segment_info, show_unlisted=False, **kwargs): and ``bufnr`` keys set to buffer-specific ones, ``window``, ``winnr`` and ``window_id`` keys set to None. - Adds either ``buf:`` or ``buf_nc:`` prefix to all segment highlight groups. + Adds one of ``buf:``, ``buf_nc:``, ``buf_mod:``, or ``buf_nc_mod`` + prefix to all segment highlight groups. :param bool show_unlisted: True if unlisted buffers should be shown as well. Current buffer is @@ -89,22 +93,31 @@ def bufferlister(pl, segment_info, show_unlisted=False, **kwargs): return dct return ( - ( - buf_segment_info, - add_multiplier(buf_segment_info['buffer'], {'highlight_group_prefix': prefix}) + (lambda buffer, current, modified: ( + buffer_updated_segment_info(segment_info, buffer), + add_multiplier(buffer, { + 'highlight_group_prefix': '{0}{1}'.format(current, modified), + 'divider_highlight_group': 'tab:divider' + }) + ))( + buffer, + 'buf' if buffer is cur_buffer else 'buf_nc', + '_mod' if int(vim.eval('getbufvar({0}, \'&modified\')'.format(buffer.number))) > 0 else '' ) - for buf_segment_info, prefix in ( - ( - buffer_updated_segment_info( - segment_info, - buffer - ), - ('buf' if buffer is cur_buffer else 'buf_nc') - ) - for buffer in vim.buffers - ) if ( - buf_segment_info['buffer'] is cur_buffer - or show_unlisted - or int(vim_getbufoption(buf_segment_info, 'buflisted')) + for buffer in vim.buffers if ( + buffer is cur_buffer + or show_unlisted + # We can't use vim_getbufoption(segment_info, 'buflisted') + # here for performance reasons. Querying the buffer options + # through the vim python module's option attribute caused + # vim to think it needed to update the tabline for every + # keystroke after any event that changed the buffer's + # options. + # + # Using the vim module's eval method to directly use the + # buflisted(nr) vim method instead does not cause vim to + # update the tabline after every keystroke, but rather after + # events that would change that status. Fixes #1281 + or int(vim.eval('buflisted(%s)' % buffer.number)) > 0 ) ) diff --git a/powerline/matchers/vim/__init__.py b/powerline/matchers/vim/__init__.py index b2bad4cc..f6de45ed 100644 --- a/powerline/matchers/vim/__init__.py +++ b/powerline/matchers/vim/__init__.py @@ -7,7 +7,7 @@ from powerline.bindings.vim import vim_getbufoption, buffer_name def help(matcher_info): - return str(vim_getbufoption(matcher_info, 'buftype')) == 'help' + return vim_getbufoption(matcher_info, 'buftype') == 'help' def cmdwin(matcher_info): @@ -16,4 +16,4 @@ def cmdwin(matcher_info): def quickfix(matcher_info): - return str(vim_getbufoption(matcher_info, 'buftype')) == 'quickfix' + return vim_getbufoption(matcher_info, 'buftype') == 'quickfix' diff --git a/powerline/matchers/vim/plugin/commandt.py b/powerline/matchers/vim/plugin/commandt.py index e4236f3e..7eefe9b7 100644 --- a/powerline/matchers/vim/plugin/commandt.py +++ b/powerline/matchers/vim/plugin/commandt.py @@ -3,9 +3,12 @@ from __future__ import (unicode_literals, division, absolute_import, print_funct import os -from powerline.bindings.vim import buffer_name +from powerline.bindings.vim import vim_getbufoption, buffer_name def commandt(matcher_info): name = buffer_name(matcher_info) - return name and os.path.basename(name) == b'GoToFile' + return ( + vim_getbufoption(matcher_info, 'filetype') == 'command-t' + or (name and os.path.basename(name) == b'GoToFile') + ) diff --git a/powerline/renderers/bar.py b/powerline/renderers/bar.py deleted file mode 100644 index 1a0684aa..00000000 --- a/powerline/renderers/bar.py +++ /dev/null @@ -1,45 +0,0 @@ -# vim:fileencoding=utf-8:noet -from __future__ import (unicode_literals, division, absolute_import, print_function) - -from powerline.renderer import Renderer -from powerline.colorscheme import ATTR_UNDERLINE - - -class BarRenderer(Renderer): - '''bar (bar ain't recursive) renderer - - - See documentation of `bar `_ and :ref:`the usage instructions ` - ''' - - character_translations = Renderer.character_translations.copy() - character_translations[ord('%')] = '%%' - - @staticmethod - def hlstyle(*args, **kwargs): - # We don’t need to explicitly reset attributes, so skip those calls - return '' - - def hl(self, contents, fg=None, bg=None, attrs=None): - text = '' - - if fg is not None: - if fg is not False and fg[1] is not False: - text += '%{{F#ff{0:06x}}}'.format(fg[1]) - if bg is not None: - if bg is not False and bg[1] is not False: - text += '%{{B#ff{0:06x}}}'.format(bg[1]) - - if attrs & ATTR_UNDERLINE: - text += '%{+u}' - - return text + contents + '%{F-B--u}' - - def render(self, *args, **kwargs): - return '%{{l}}{0}%{{r}}{1}'.format( - super(BarRenderer, self).render(side='left', *args, **kwargs), - super(BarRenderer, self).render(side='right', *args, **kwargs), - ) - - -renderer = BarRenderer diff --git a/powerline/renderers/lemonbar.py b/powerline/renderers/lemonbar.py new file mode 100644 index 00000000..f378f235 --- /dev/null +++ b/powerline/renderers/lemonbar.py @@ -0,0 +1,61 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderer import Renderer +from powerline.theme import Theme +from powerline.colorscheme import ATTR_UNDERLINE + + +class LemonbarRenderer(Renderer): + '''lemonbar (formerly bar/bar ain't recursive) renderer + + + See documentation of `lemonbar `_ and :ref:`the usage instructions ` + ''' + + character_translations = Renderer.character_translations.copy() + character_translations[ord('%')] = '%%{}' + + @staticmethod + def hlstyle(*args, **kwargs): + # We don’t need to explicitly reset attributes, so skip those calls + return '' + + def hl(self, contents, fg=None, bg=None, attrs=None): + text = '' + + if fg is not None: + if fg is not False and fg[1] is not False: + text += '%{{F#ff{0:06x}}}'.format(fg[1]) + if bg is not None: + if bg is not False and bg[1] is not False: + text += '%{{B#ff{0:06x}}}'.format(bg[1]) + + if attrs & ATTR_UNDERLINE: + text += '%{+u}' + + return text + contents + '%{F-B--u}' + + def render(self, *args, **kwargs): + return '%{{l}}{0}%{{r}}{1}'.format( + super(LemonbarRenderer, self).render(side='left', segment_info={'output': kwargs.get('matcher_info')}, *args, **kwargs), + super(LemonbarRenderer, self).render(side='right', segment_info={'output': kwargs.get('matcher_info')}, *args, **kwargs), + ) + + def get_theme(self, matcher_info): + if not matcher_info or matcher_info not in self.local_themes: + return self.theme + match = self.local_themes[matcher_info] + + try: + return match['theme'] + except KeyError: + match['theme'] = Theme( + theme_config=match['config'], + main_theme_config=self.theme_config, + **self.theme_kwargs + ) + return match['theme'] + + +renderer = LemonbarRenderer diff --git a/powerline/segments/common/bat.py b/powerline/segments/common/bat.py index 26e8b0dc..a0b89245 100644 --- a/powerline/segments/common/bat.py +++ b/powerline/segments/common/bat.py @@ -30,6 +30,7 @@ def _fetch_battery_info(pl): 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( @@ -46,37 +47,63 @@ def _fetch_battery_info(pl): 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) - return lambda pl: ( - float( - dbus.Interface(dev, dbus_interface=devinterface).Get( + 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, - 'Percentage' - ), - ), - dbus.Interface(dev, dbus_interface=devinterface).Get( - devtype_name, - 'State' - ) == 1 - ) + 'State' + ) != 2 + return (energy * 100.0 / energy_full), state + return _flatten_battery pl.debug('Not using DBUS+UPower as no batteries were found') if os.path.isdir('/sys/class/power_supply'): - linux_bat_fmt = '/sys/class/power_supply/{0}/capacity' - linux_ac_fmt = '/sys/class/power_supply/{0}/online' - for linux_bat in os.listdir('/sys/class/power_supply'): - cap_path = linux_bat_fmt.format(linux_bat) - online_path = linux_ac_fmt.format(linux_bat) - if linux_bat.startswith('BAT') and os.path.exists(cap_path): - pl.debug('Using /sys/class/power_supply with battery {0}', linux_bat) - - def _get_battery_status(pl): - with open(cap_path, 'r') as f: - _capacity = int(float(f.readline().split()[0])) - with open(online_path, 'r') as f: - _ac_powered = f.readline() == 1 - return _capacity, _ac_powered - return _get_battery_status + linux_energy_full_fmt = '/sys/class/power_supply/{0}/energy_full' + linux_energy_fmt = '/sys/class/power_supply/{0}/energy_now' + linux_status_fmt = '/sys/class/power_supply/{0}/status' + devices = [] + for linux_supplier in os.listdir('/sys/class/power_supply'): + energy_path = linux_energy_fmt.format(linux_supplier) + if not os.path.exists(energy_path): + continue + pl.debug('Using /sys/class/power_supply with battery {0}', linux_supplier) + devices.append(linux_supplier) + if devices: + def _get_battery_status(pl): + energy = 0.0 + energy_full = 0.0 + state = True + for device in devices: + with open(linux_energy_full_fmt.format(device), 'r') as f: + energy_full += int(float(f.readline().split()[0])) + with open(linux_energy_fmt.format(device), '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('Not using /sys/class/power_supply: no directory') diff --git a/powerline/segments/common/net.py b/powerline/segments/common/net.py index eb5b4bcf..47c2ce71 100644 --- a/powerline/segments/common/net.py +++ b/powerline/segments/common/net.py @@ -74,6 +74,7 @@ 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 diff --git a/powerline/segments/common/players.py b/powerline/segments/common/players.py index 8ae6cb1a..333612cb 100644 --- a/powerline/segments/common/players.py +++ b/powerline/segments/common/players.py @@ -189,7 +189,11 @@ class MpdPlayerSegment(PlayerSegment): 'total': now_playing[3], } else: - client = mpd.MPDClient() + 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) @@ -204,7 +208,7 @@ class MpdPlayerSegment(PlayerSegment): 'album': now_playing.get('album'), 'artist': now_playing.get('artist'), 'title': now_playing.get('title'), - 'elapsed': _convert_seconds(now_playing.get('elapsed', 0)), + 'elapsed': _convert_seconds(status.get('elapsed', 0)), 'total': _convert_seconds(now_playing.get('time', 0)), } diff --git a/powerline/segments/common/sys.py b/powerline/segments/common/sys.py index f53e2c7d..a83025bf 100644 --- a/powerline/segments/common/sys.py +++ b/powerline/segments/common/sys.py @@ -13,7 +13,8 @@ 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): +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 @@ -35,6 +36,8 @@ def system_load(pl, format='{avg:.1f}', threshold_good=1, threshold_bad=2, track :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``. @@ -61,6 +64,10 @@ def system_load(pl, format='{avg:.1f}', threshold_good=1, threshold_bad=2, track 'divider_highlight_group': 'background:divider', 'gradient_level': gradient_level, }) + + if short: + return ret + ret[0]['contents'] += ' ' ret[1]['contents'] += ' ' return ret diff --git a/powerline/segments/i3wm.py b/powerline/segments/i3wm.py index a5a82774..3f508ee2 100644 --- a/powerline/segments/i3wm.py +++ b/powerline/segments/i3wm.py @@ -1,13 +1,16 @@ # 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 -conn = None +WORKSPACE_REGEX = re.compile(r'^[0-9]+: ?') -def calcgrp(w): +def workspace_groups(w): group = [] if w['focused']: group.append('w_focused') @@ -19,7 +22,14 @@ def calcgrp(w): return group -def workspaces(pl, only_show=None, output=None, strip=0): +def format_name(name, strip=False): + if strip: + return WORKSPACE_REGEX.sub('', name, count=1) + return name + + +@requires_segment_info +def workspaces(pl, segment_info, only_show=None, output=None, strip=0): '''Return list of used workspaces :param list only_show: @@ -28,7 +38,9 @@ def workspaces(pl, only_show=None, output=None, strip=0): are shown. :param str output: - If specified, only workspaces on this output are shown. + 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. :param int strip: Specifies how many characters from the front of each workspace name @@ -36,22 +48,57 @@ def workspaces(pl, only_show=None, output=None, strip=0): Highlight groups used: ``workspace`` or ``w_visible``, ``workspace`` or ``w_focused``, ``workspace`` or ``w_urgent``. ''' - global conn - if not conn: + output = output or segment_info.get('output') + + return [ + { + 'contents': w['name'][strip:], + 'highlight_groups': workspace_groups(w) + } + for w in get_i3_connection().get_workspaces() + if ((not only_show or any(w[typ] for typ in only_show)) + and (not output or w['output'] == output)) + ] + + +@requires_segment_info +def workspace(pl, segment_info, workspace=None, strip=False): + '''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. + + Highlight groups used: ``workspace`` or ``w_visible``, ``workspace`` or ``w_focused``, ``workspace`` or ``w_urgent``. + ''' + if workspace: try: - import i3ipc - except ImportError: - import i3 as conn - else: - conn = i3ipc.Connection() + w = next(( + w for w in get_i3_connection().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 get_i3_connection().get_workspaces() + if w['focused'] + )) + except StopIteration: + return None return [{ - 'contents': w['name'][min(len(w['name']), strip):], - 'highlight_groups': calcgrp(w) - } for w in conn.get_workspaces() - if (not only_show or any(w[typ] for typ in only_show)) - and (not output or w['output'] == output) - ] + 'contents': format_name(w['name'], strip=strip), + 'highlight_groups': workspace_groups(w) + }] @requires_segment_info @@ -68,3 +115,41 @@ def mode(pl, segment_info, names={'default': None}): 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().descendents() + if w.scratchpad_state != 'none' + ] diff --git a/powerline/segments/vim/__init__.py b/powerline/segments/vim/__init__.py index 16ad0194..8c740c95 100644 --- a/powerline/segments/vim/__init__.py +++ b/powerline/segments/vim/__init__.py @@ -11,7 +11,7 @@ from collections import defaultdict try: import vim except ImportError: - vim = {} + vim = object() from powerline.bindings.vim import (vim_get_func, getbufvar, vim_getbufoption, buffer_name, vim_getwinvar, diff --git a/powerline/vim.py b/powerline/vim.py index 53e23147..603a6a50 100644 --- a/powerline/vim.py +++ b/powerline/vim.py @@ -7,10 +7,13 @@ import logging from itertools import count -import vim +try: + import vim +except ImportError: + vim = object() from powerline.bindings.vim import vim_get_func, vim_getvar, get_vim_encoding, python_to_vim -from powerline import Powerline, FailedUnicode +from powerline import Powerline, FailedUnicode, finish_common_config from powerline.lib.dict import mergedicts from powerline.lib.unicode import u @@ -32,19 +35,21 @@ def _override_from(config, override_varname, key=None): class VimVarHandler(logging.Handler, object): '''Vim-specific handler which emits messages to Vim global variables - Used variable: ``g:powerline_log_messages``. + :param str varname: + Variable where ''' - def __init__(self, *args, **kwargs): - super(VimVarHandler, self).__init__(*args, **kwargs) - vim.command('unlet! g:powerline_log_messages') - vim.command('let g:powerline_log_messages = []') + def __init__(self, varname): + super(VimVarHandler, self).__init__() + utf_varname = u(varname) + self.vim_varname = utf_varname.encode('ascii') + vim.command('unlet! g:' + utf_varname) + vim.command('let g:' + utf_varname + ' = []') - @staticmethod - def emit(record): + def emit(self, record): message = u(record.message) if record.exc_text: message += '\n' + u(record.exc_text) - vim.eval(b'add(g:powerline_log_messages, ' + python_to_vim(message) + b')') + vim.eval(b'add(g:' + self.vim_varname + b', ' + python_to_vim(message) + b')') class VimPowerline(Powerline): @@ -53,6 +58,12 @@ class VimPowerline(Powerline): self.last_window_id = 1 self.pyeval = pyeval self.construct_window_statusline = self.create_window_statusline_constructor() + if all((hasattr(vim.current.window, attr) for attr in ('options', 'vars', 'number'))): + self.win_idx = self.new_win_idx + else: + self.win_idx = self.old_win_idx + self._vim_getwinvar = vim_get_func('getwinvar', 'bytes') + self._vim_setwinvar = vim_get_func('setwinvar') if sys.version_info < (3,): def create_window_statusline_constructor(self): @@ -80,18 +91,6 @@ class VimPowerline(Powerline): default_log_stream = sys.stdout - def create_logger(self): - logger = super(VimPowerline, self).create_logger() - try: - if int(vim_getvar('powerline_use_var_handler')): - formatter = logging.Formatter(self.common_config['log_format']) - handler = VimVarHandler(getattr(logging, self.common_config['log_level'])) - handler.setFormatter(formatter) - logger.addHandler(handler) - except KeyError: - pass - return logger - def add_local_theme(self, key, config): '''Add local themes at runtime (during vim session). @@ -137,7 +136,16 @@ class VimPowerline(Powerline): get_encoding = staticmethod(get_vim_encoding) def load_main_config(self): - return _override_from(super(VimPowerline, self).load_main_config(), 'powerline_config_overrides') + main_config = _override_from(super(VimPowerline, self).load_main_config(), 'powerline_config_overrides') + try: + use_var_handler = bool(int(vim_getvar('powerline_use_var_handler'))) + except KeyError: + use_var_handler = False + if use_var_handler: + main_config.setdefault('common', {}) + main_config['common'] = finish_common_config(self.get_encoding(), main_config['common']) + main_config['common']['log_file'].append(['powerline.vim.VimVarHandler', [['powerline_log_messages']]]) + return main_config def load_theme_config(self, name): return _override_from( @@ -252,44 +260,40 @@ class VimPowerline(Powerline): # do anything. pass - if all((hasattr(vim.current.window, attr) for attr in ('options', 'vars', 'number'))): - def win_idx(self, window_id): - r = None - for window in vim.windows: - try: - curwindow_id = window.vars['powerline_window_id'] - if r is not None and curwindow_id == window_id: - raise KeyError - except KeyError: - curwindow_id = self.last_window_id - self.last_window_id += 1 - window.vars['powerline_window_id'] = curwindow_id - statusline = self.construct_window_statusline(curwindow_id) - if window.options['statusline'] != statusline: - window.options['statusline'] = statusline - if curwindow_id == window_id if window_id else window is vim.current.window: - r = (window, curwindow_id, window.number) - return r - else: - _vim_getwinvar = staticmethod(vim_get_func('getwinvar', 'bytes')) - _vim_setwinvar = staticmethod(vim_get_func('setwinvar')) + def new_win_idx(self, window_id): + r = None + for window in vim.windows: + try: + curwindow_id = window.vars['powerline_window_id'] + if r is not None and curwindow_id == window_id: + raise KeyError + except KeyError: + curwindow_id = self.last_window_id + self.last_window_id += 1 + window.vars['powerline_window_id'] = curwindow_id + statusline = self.construct_window_statusline(curwindow_id) + if window.options['statusline'] != statusline: + window.options['statusline'] = statusline + if curwindow_id == window_id if window_id else window is vim.current.window: + r = (window, curwindow_id, window.number) + return r - def win_idx(self, window_id): - r = None - for winnr, window in zip(count(1), vim.windows): - curwindow_id = self._vim_getwinvar(winnr, 'powerline_window_id') - if curwindow_id and not (r is not None and curwindow_id == window_id): - curwindow_id = int(curwindow_id) - else: - curwindow_id = self.last_window_id - self.last_window_id += 1 - self._vim_setwinvar(winnr, 'powerline_window_id', curwindow_id) - statusline = self.construct_window_statusline(curwindow_id) - if self._vim_getwinvar(winnr, '&statusline') != statusline: - self._vim_setwinvar(winnr, '&statusline', statusline) - if curwindow_id == window_id if window_id else window is vim.current.window: - r = (window, curwindow_id, winnr) - return r + def old_win_idx(self, window_id): + r = None + for winnr, window in zip(count(1), vim.windows): + curwindow_id = self._vim_getwinvar(winnr, 'powerline_window_id') + if curwindow_id and not (r is not None and curwindow_id == window_id): + curwindow_id = int(curwindow_id) + else: + curwindow_id = self.last_window_id + self.last_window_id += 1 + self._vim_setwinvar(winnr, 'powerline_window_id', curwindow_id) + statusline = self.construct_window_statusline(curwindow_id) + if self._vim_getwinvar(winnr, '&statusline') != statusline: + self._vim_setwinvar(winnr, '&statusline', statusline) + if curwindow_id == window_id if window_id else window is vim.current.window: + r = (window, curwindow_id, winnr) + return r def statusline(self, window_id): window, window_id, winnr = self.win_idx(window_id) or (None, None, None) diff --git a/setup.py b/setup.py index ea6a1f3d..b8818a87 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ else: def get_version(): - base_version = '2.3' + base_version = '2.4' base_version += '.dev9999' try: return base_version + '+git.' + str(subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip()) @@ -70,7 +70,7 @@ def get_version(): setup( name='powerline-status', - version='2.3', + version='2.4', description='The ultimate statusline/prompt utility.', long_description=README, classifiers=[ diff --git a/tests/common.sh b/tests/common.sh index 0b555162..14da0e19 100644 --- a/tests/common.sh +++ b/tests/common.sh @@ -18,7 +18,9 @@ exit_suite() { echo "${FAIL_SUMMARY}" fi export POWERLINE_CURRENT_SUITE="${POWERLINE_CURRENT_SUITE%/*}" - exit $FAILED + if test "x$1" != "x--continue" ; then + exit $FAILED + fi } fail() { diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index b9c0d3ad..38287c82 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -16,11 +16,16 @@ class Pl(object): self.use_daemon_threads = True for meth in ('error', 'warn', 'debug', 'exception', 'info'): - exec (( + exec(( 'def {0}(self, msg, *args, **kwargs):\n' ' self.{0}s.append((kwargs.get("prefix") or self.prefix, msg, args, kwargs))\n' ).format(meth)) + def __nonzero__(self): + return bool(self.exceptions or self.errors or self.warns) + + __bool__ = __nonzero__ + class Args(object): theme_override = {} diff --git a/tests/run_bar_tests.sh b/tests/run_bar_tests.sh new file mode 100755 index 00000000..7d658ff1 --- /dev/null +++ b/tests/run_bar_tests.sh @@ -0,0 +1,201 @@ +#!/bin/sh +. tests/common.sh + +enter_suite bar + +TEST_ROOT="$ROOT/tests/bar" +TEST_PATH="$TEST_ROOT/path" +TEST_STATIC_ROOT="$ROOT/tests/test_bar" + +test -d "$TEST_ROOT" && rm -r "$TEST_ROOT" +mkdir "$TEST_ROOT" +cp -r "$TEST_STATIC_ROOT/path" "$TEST_ROOT" +cp -r "$TEST_STATIC_ROOT/powerline" "$TEST_ROOT" + +export PYTHONPATH="$ROOT${PYTHONPATH:+:}$PYTHONPATH" + +ln -s "$(which "${PYTHON}")" "$TEST_PATH"/python +ln -s "$(which sed)" "$TEST_PATH" +ln -s "$(which cat)" "$TEST_PATH" +ln -s "$(which mkdir)" "$TEST_PATH" +ln -s "$(which basename)" "$TEST_PATH" +ln -s "$TEST_PATH/lemonbar" "$TEST_PATH/bar-aint-recursive" + +DEPRECATED_SCRIPT="$ROOT/powerline/bindings/bar/powerline-bar.py" + +run() { + env -i \ + LANG=C \ + PATH="$TEST_PATH" \ + XDG_CONFIG_HOME="$TEST_ROOT" \ + XDG_CONFIG_DIRS="$TEST_ROOT/dummy" \ + PYTHONPATH="$PYTHONPATH" \ + TEST_ROOT="$TEST_ROOT" \ + LD_LIBRARY_PATH="$LD_LIBRARY_PATH" \ + "$@" || true +} + +display_log() { + local log_file="$1" + echo "$log_file:" + echo '============================================================' + cat -v "$log_file" + echo + echo '____________________________________________________________' +} + +check_log() { + local log_file="$1" + local text="$2" + local warns="$3" + if test "$warns" = "warns" ; then + local warning="$(head -n1 "$log_file" | sed 's/.*://')" + local expwarning="The 'bar' bindings are deprecated, please switch to 'lemonbar'" + if test "x$warning" != "x$expwarning" ; then + echo "Got: $warning" + echo "Exp: $expwarning" + fail "warn" F "Expected warning" + fi + sed -r -i -e '1d' "$log_file" + fi + local line="$(head -n1 "$log_file")" + local linenum="$(cat "$log_file" | wc -l)" + if test $linenum -lt 5 ; then + fail "log:lt" F "Script was run not enough times" + return 1 + elif test $linenum -gt 15 ; then + fail "log:gt" E "Script was run too many times" + return 1 + fi + local expline="%{l}%{F#ffd0d0d0}%{B#ff303030} $text-left %{F-B--u}%{F#ff303030} %{F-B--u}%{r}%{F#ff303030} %{F-B--u}%{F#ffd0d0d0}%{B#ff303030} $text-right %{F-B--u}" + if test "x$expline" != "x$line" ; then + echo "Line: '$line'" + echo "Expected: '$expline'" + fail "log:line" F "Unexpected line" + return 1 + fi + local ret=0 + while test $linenum -gt 0 ; do + echo "$line" >> "$TEST_ROOT/ok" + linenum=$(( linenum - 1 )) + done + if ! diff "$TEST_ROOT/ok" "$log_file" ; then + fail "log:diff" F "Unexpected output" + ret=1 + fi + rm "$TEST_ROOT/ok" + return $ret +} + +killscript() { + kill -KILL $1 || true +} + +if ! test -e "$DEPRECATED_SCRIPT" ; then + # TODO: uncomment when skip is available + # skip "deprecated" "Missing deprecated bar bindings script" + : +else + enter_suite "deprecated" + run python "$DEPRECATED_SCRIPT" $args > "$TEST_ROOT/deprecated.log" 2>&1 & + SPID=$! + sleep 5 + killscript $SPID + if ! check_log "$TEST_ROOT/deprecated.log" "default" warns ; then + display_log "$TEST_ROOT/deprecated.log" + fail "log" F "Checking log failed" + fi + rm "$TEST_ROOT/deprecated.log" + exit_suite --continue +fi + +LEMONBAR_SCRIPT="$ROOT/powerline/bindings/lemonbar/powerline-lemonbar.py" + +if ! test -e "$LEMONBAR_SCRIPT" ; then + # TODO: uncomment when skip is available + # skip "lemonbar" "Missing lemonbar bindings script" + : +else + enter_suite "lemonbar" + for args in "" "-i0.5" "--interval=0.5" "-- test args" "--bar-command bar-aint-recursive" "--height=10"; do + rm -rf "$TEST_ROOT/results" + run python "$LEMONBAR_SCRIPT" $args > "$TEST_ROOT/lemonbar.log" 2>&1 & + SPID=$! + sleep 5 + killscript $SPID + sleep 0.5 + enter_suite "args($args)" + fnum=0 + for file in "$TEST_ROOT/results"/*.log ; do + if ! test -e "$file" ; then + fail "log" E "Log file is missing" + break + fi + fnum=$(( fnum + 1 )) + args_file="${file%.log}.args" + if ! test -e "$args_file" ; then + fail "args" E "$args_file is missing" + else + cat "$args_file" >> "$TEST_ROOT/args.log" + fi + text="dvi" + if cat "$args_file" | grep -q +1 ; then + text="default" + fi + if ! check_log "$file" "$text" ; then + display_log "$file" + fail "log" F "Checking log failed" + fi + rm "$file" + done + if test "$fnum" -ne 2 ; then + fail "fnum" F "Expected two output files" + fi + if test "x${args#--height}" != "x$args" ; then + height="${args#--height}" + height="${height# }" + height="${height#=}" + height="${height%% *}" + fi + command="lemonbar" + if test "x${args#--bar-command}" != "x$args" ; then + command="${args#--bar-command}" + command="${command# }" + command="${command#=}" + command="${command%% *}" + fi + received_args="$(cat "$TEST_ROOT/args.log" | sort)" + rm "$TEST_ROOT/args.log" + script_args="${args#*-- }" + script_args="${script_args# }" + if test "x${script_args}" '=' "x$args" ; then + script_args= + fi + expected_args="$command -g 1920x$height+0${script_args:+ }$script_args${NL}$command -g 1920x$height+1${script_args:+ }$script_args" + if test "x$expected_args" != "x$received_args" ; then + echo "args:${NL}<$received_args>" + echo "expected:${NL}<$expected_args>" + fail "args" F "Expected different args" + fi + if ! test -z "$(cat "$TEST_ROOT/lemonbar.log")" ; then + display_log "$TEST_ROOT/lemonbar.log" + fail "stderr" E "Unexpected script output" + fi + rm "$TEST_ROOT/lemonbar.log" + exit_suite --continue + done + exit_suite --continue +fi + +if ! powerline-lint \ + -p "$ROOT/powerline/config_files" \ + -p "$TEST_STATIC_ROOT/powerline" +then + fail "lint" F "Checking test config failed" +fi + +if test $FAILED -eq 0 ; then + rm -r "$TEST_ROOT" +fi + +exit_suite diff --git a/tests/test_bar/path/lemonbar b/tests/test_bar/path/lemonbar new file mode 100755 index 00000000..13c6030e --- /dev/null +++ b/tests/test_bar/path/lemonbar @@ -0,0 +1,11 @@ +#!/bin/sh + +RES_DIR="$TEST_ROOT/results" +mkdir -p "$RES_DIR" +RES_FILE="$RES_DIR/$$" +while test -e "$RES_FILE.log" ; do + RES_FILE="$RES_FILE.${RANDOM:-`date +%N | sed s/^0*//`}" +done + +echo $(basename $0) "$@" > "$RES_FILE.args" +cat > "$RES_FILE.log" diff --git a/tests/test_bar/path/xrandr b/tests/test_bar/path/xrandr new file mode 100755 index 00000000..d02e3005 --- /dev/null +++ b/tests/test_bar/path/xrandr @@ -0,0 +1,31 @@ +#!/bin/sh + +cat << EOF +Screen 0: minimum 8 x 8, current 1920 x 1200, maximum 16384 x 16384 +DVI-I-0 disconnected (normal left inverted right x axis y axis) +VGA-0 connected 1920x1200+1+0 (normal left inverted right x axis y axis) 520mm x 330mm + 1920x1200 59.95*+ + 1920x1080 60.00 + 1680x1050 59.95 + 1600x1200 60.00 + 1440x900 59.89 + 1280x1024 75.02 60.02 + 1280x800 59.81 + 1152x864 75.00 + 1024x768 75.03 60.00 + 800x600 75.00 60.32 + 640x480 75.00 59.94 +DVI-I-1 connected 1920x1200+0+0 (normal left inverted right x axis y axis) 520mm x 330mm + 1920x1200 59.95*+ + 1920x1080 60.00 + 1680x1050 59.95 + 1600x1200 60.00 + 1440x900 59.89 + 1280x1024 75.02 60.02 + 1280x800 59.81 + 1152x864 75.00 + 1024x768 75.03 60.00 + 800x600 75.00 60.32 + 640x480 75.00 59.94 +HDMI-0 disconnected (normal left inverted right x axis y axis) +EOF diff --git a/tests/test_bar/powerline/config.json b/tests/test_bar/powerline/config.json new file mode 100644 index 00000000..0b35d567 --- /dev/null +++ b/tests/test_bar/powerline/config.json @@ -0,0 +1,9 @@ +{ + "ext": { + "wm": { + "local_themes": { + "DVI-I-1": "dvi" + } + } + } +} diff --git a/tests/test_bar/powerline/themes/wm/default.json b/tests/test_bar/powerline/themes/wm/default.json new file mode 100644 index 00000000..d9eaf5f8 --- /dev/null +++ b/tests/test_bar/powerline/themes/wm/default.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "default-left" + } + ], + "right": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "default-right" + } + ] + } +} diff --git a/tests/test_bar/powerline/themes/wm/dvi.json b/tests/test_bar/powerline/themes/wm/dvi.json new file mode 100644 index 00000000..4cf3731b --- /dev/null +++ b/tests/test_bar/powerline/themes/wm/dvi.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "dvi-left" + } + ], + "right": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "dvi-right" + } + ] + } +} diff --git a/tests/test_configuration.py b/tests/test_configuration.py index c0708c3d..3cd7d387 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -825,28 +825,28 @@ class TestVim(TestCase): sys.path.pop(0) -class TestBar(TestRender): - def test_bar(self): +class TestLemonbar(TestRender): + def test_lemonbar(self): import powerline as powerline_module with swap_attributes(config, powerline_module): - with get_powerline_raw(config, powerline_module.Powerline, replace_gcp=True, ext='wm', renderer_module='bar') as powerline: + with get_powerline_raw(config, powerline_module.Powerline, replace_gcp=True, ext='wm', renderer_module='lemonbar') as powerline: self.assertRenderEqual( powerline, '%{l}%{F#ffc00000}%{B#ff008000}%{+u} A%{F-B--u}%{F#ff008000}%{B#ffc00000}>>%{F-B--u}%{F#ff008000}%{B#ffc00000}B%{F-B--u}%{F#ffc00000}>>%{F-B--u}%{r}%{F#ffc00000}<<%{F-B--u}%{F#ff804000}%{B#ffc00000}%{+u}C%{F-B--u}%{F#ff0000c0}%{B#ffc00000}<<%{F-B--u}%{F#ff008000}%{B#ff0000c0}D %{F-B--u}' ) @with_new_config - def test_bar_escape(self, config): + def test_lemonbar_escape(self, config): import powerline as powerline_module config['themes/wm/default']['segments']['left'] = ( highlighted_string('%{asd}', 'hl1'), highlighted_string('10% %', 'hl2'), ) with swap_attributes(config, powerline_module): - with get_powerline_raw(config, powerline_module.Powerline, replace_gcp=True, ext='wm', renderer_module='bar') as powerline: + with get_powerline_raw(config, powerline_module.Powerline, replace_gcp=True, ext='wm', renderer_module='lemonbar') as powerline: self.assertRenderEqual( powerline, - '%{l}%{F#ffc00000}%{B#ff008000}%{+u} %%{asd}%{F-B--u}%{F#ff008000}%{B#ffc00000}>>%{F-B--u}%{F#ff008000}%{B#ffc00000}10%% %%%{F-B--u}%{F#ffc00000}>>%{F-B--u}%{r}%{F#ffc00000}<<%{F-B--u}%{F#ff804000}%{B#ffc00000}%{+u}C%{F-B--u}%{F#ff0000c0}%{B#ffc00000}<<%{F-B--u}%{F#ff008000}%{B#ff0000c0}D %{F-B--u}' + '%{l}%{F#ffc00000}%{B#ff008000}%{+u} %%{}{asd}%{F-B--u}%{F#ff008000}%{B#ffc00000}>>%{F-B--u}%{F#ff008000}%{B#ffc00000}10%%{} %%{}%{F-B--u}%{F#ffc00000}>>%{F-B--u}%{r}%{F#ffc00000}<<%{F-B--u}%{F#ff804000}%{B#ffc00000}%{+u}C%{F-B--u}%{F#ff0000c0}%{B#ffc00000}<<%{F-B--u}%{F#ff008000}%{B#ff0000c0}D %{F-B--u}' ) diff --git a/tests/test_lib.py b/tests/test_lib.py index e6b28624..6c7aac25 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -17,6 +17,7 @@ from powerline.lib.vcs import guess, get_fallback_create_watcher from powerline.lib.threaded import ThreadedSegment, KwThreadedSegment from powerline.lib.monotonic import monotonic from powerline.lib.vcs.git import git_directory +from powerline.lib.shell import run_cmd import powerline.lib.unicode as plu @@ -48,6 +49,24 @@ def thread_number(): return len(threading.enumerate()) +class TestShell(TestCase): + def test_run_cmd(self): + pl = Pl() + self.assertEqual(run_cmd(pl, ['xxx_nonexistent_command_xxx']), None) + self.assertEqual(len(pl.exceptions), 1) + pl = Pl() + self.assertEqual(run_cmd(pl, ['echo', ' test ']), 'test') + self.assertFalse(pl) + self.assertEqual(run_cmd(pl, ['echo', ' test '], strip=True), 'test') + self.assertFalse(pl) + self.assertEqual(run_cmd(pl, ['echo', ' test '], strip=False), ' test \n') + self.assertFalse(pl) + self.assertEqual(run_cmd(pl, ['cat'], stdin='test'), 'test') + self.assertFalse(pl) + self.assertEqual(run_cmd(pl, ['sh', '-c', 'cat >&2'], stdin='test'), '') + self.assertFalse(pl) + + class TestThreaded(TestCase): def test_threaded_segment(self): log = [] diff --git a/tests/test_listers.py b/tests/test_listers.py new file mode 100644 index 00000000..3d3ed094 --- /dev/null +++ b/tests/test_listers.py @@ -0,0 +1,227 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import powerline.listers.i3wm as i3wm + +from tests.lib import Args, replace_attr, Pl +from tests import TestCase + + +class TestI3WM(TestCase): + @staticmethod + def get_workspaces(): + return iter([ + {'name': '1: w1', 'output': 'LVDS1', 'focused': False, 'urgent': False, 'visible': False}, + {'name': '2: w2', 'output': 'LVDS1', 'focused': False, 'urgent': False, 'visible': True}, + {'name': '3: w3', 'output': 'HDMI1', 'focused': False, 'urgent': True, 'visible': True}, + {'name': '4: w4', 'output': 'DVI01', 'focused': True, 'urgent': True, 'visible': True}, + ]) + + @staticmethod + def get_outputs(pl): + return iter([ + {'name': 'LVDS1'}, + {'name': 'HDMI1'}, + {'name': 'DVI01'}, + ]) + + def test_output_lister(self): + pl = Pl() + with replace_attr(i3wm, 'get_connected_xrandr_outputs', self.get_outputs): + self.assertEqual( + list(i3wm.output_lister(pl=pl, segment_info={'a': 1})), + [ + ({'a': 1, 'output': 'LVDS1'}, {'draw_inner_divider': None}), + ({'a': 1, 'output': 'HDMI1'}, {'draw_inner_divider': None}), + ({'a': 1, 'output': 'DVI01'}, {'draw_inner_divider': None}), + ] + ) + + def test_workspace_lister(self): + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Args(get_workspaces=self.get_workspaces)): + self.assertEqual( + list(i3wm.workspace_lister(pl=pl, segment_info={'a': 1})), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '1: w1', + 'focused': False, + 'urgent': False, + 'visible': False + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '2: w2', + 'focused': False, + 'urgent': False, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': { + 'name': '3: w3', + 'focused': False, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': { + 'name': '4: w4', + 'focused': True, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister(pl=pl, segment_info={'a': 1}, output='LVDS1')), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '1: w1', + 'focused': False, + 'urgent': False, + 'visible': False + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '2: w2', + 'focused': False, + 'urgent': False, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1, 'output': 'LVDS1'} + )), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '1: w1', + 'focused': False, + 'urgent': False, + 'visible': False + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '2: w2', + 'focused': False, + 'urgent': False, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1, 'output': 'LVDS1'}, + output=False + )), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '1: w1', + 'focused': False, + 'urgent': False, + 'visible': False + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': { + 'name': '2: w2', + 'focused': False, + 'urgent': False, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': { + 'name': '3: w3', + 'focused': False, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': { + 'name': '4: w4', + 'focused': True, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1}, + only_show=['focused', 'urgent'] + )), + [ + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': { + 'name': '3: w3', + 'focused': False, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': { + 'name': '4: w4', + 'focused': True, + 'urgent': True, + 'visible': True + } + }, {'draw_inner_divider': None}), + ] + ) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 00000000..6de4a389 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,467 @@ +# vim:fileencoding=utf-8:noet + +'''Tests for various logging features''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import re +import codecs +import os + +from io import StringIO +from shutil import rmtree + +from powerline import finish_common_config, create_logger + +from tests import TestCase +from tests.lib import replace_attr + + +TIMESTAMP_RE = r'\d{4}-\d\d-\d\d \d\d:\d\d:\d\d,\d{3}' + + +class TestRE(TestCase): + def assertMatches(self, text, regexp): + self.assertTrue( + re.match(regexp, text), + '{0!r} did not match {1!r}'.format(text, regexp), + ) + + +def close_handlers(logger): + for handler in logger.handlers: + handler.close() + + +class TestHandlers(TestRE): + def test_stderr_handler_is_default(self): + out = StringIO() + err = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config) + pl.error('Foo') + close_handlers(logger) + self.assertMatches(err.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(out.getvalue(), '') + + def test_stream_override(self): + out = StringIO() + err = StringIO() + stream = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertMatches(stream.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_explicit_none(self): + out = StringIO() + err = StringIO() + stream = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [None]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertMatches(stream.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_explicit_stream_handler(self): + out = StringIO() + err = StringIO() + stream = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [['logging.StreamHandler', [[]]]]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertEqual(stream.getvalue(), '') + self.assertMatches(err.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(out.getvalue(), '') + + def test_explicit_stream_handler_implicit_stream(self): + out = StringIO() + err = StringIO() + stream = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [['logging.StreamHandler', []]]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertMatches(stream.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_file_handler(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name = 'test_logging-test_file_handler' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': file_name}) + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_file_handler_create_dir(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name = 'test_logging-test_file_handler_create_dir/file' + + self.assertFalse(os.path.isdir(os.path.dirname(file_name))) + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': file_name}) + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertTrue(os.path.isdir(os.path.dirname(file_name))) + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + rmtree(os.path.dirname(file_name)) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_multiple_files(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name_1 = 'test_logging-test_multiple_files-1' + file_name_2 = file_name_1[:-1] + '2' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [file_name_1, file_name_2]}) + try: + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + for file_name in (file_name_1, file_name_2): + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name_1) + finally: + os.unlink(file_name_2) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_multiple_files_and_stream(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name_1 = 'test_logging-test_multiple_files_and_stream-1' + file_name_2 = file_name_1[:-1] + '2' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [file_name_1, file_name_2, None]}) + try: + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + for file_name in (file_name_1, file_name_2): + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name_1) + finally: + os.unlink(file_name_2) + self.assertMatches(stream.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_handler_args(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name = 'test_logging-test_handler_args' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['RotatingFileHandler', [[file_name]]] + ]}) + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_handler_args_kwargs(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name = 'test_logging-test_handler_args_kwargs' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['RotatingFileHandler', [[file_name], {'maxBytes': 1, 'backupCount': 1}]] + ]}) + try: + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + pl.error('Bar') + close_handlers(logger) + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$') + with codecs.open(file_name + '.1', encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name + '.1') + finally: + os.unlink(file_name) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_logger_level(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + stream2 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'WARNING'], + ['logging.StreamHandler', [[stream2]], 'ERROR'], + ]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertMatches(stream1.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:Foo\n' + + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$' + )) + self.assertMatches(stream2.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$') + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_logger_level_not_overriding_default(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'DEBUG'], + ]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.debug('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertMatches(stream1.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$') + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_top_log_level(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'DEBUG'], + ], 'log_level': 'DEBUG'}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.debug('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertMatches(stream1.getvalue(), ( + '^' + TIMESTAMP_RE + ':DEBUG:__unknown__:Foo\n' + + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$' + )) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_logger_format(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'WARNING', 'FOO'], + ]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertEqual(stream1.getvalue(), 'FOO\nFOO\n') + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_top_log_format(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + stream2 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'WARNING', 'FOO'], + ['logging.StreamHandler', [[stream2]], 'WARNING'], + ], 'log_format': 'BAR'}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertEqual(stream2.getvalue(), 'BAR\nBAR\n') + self.assertEqual(stream1.getvalue(), 'FOO\nFOO\n') + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + +class TestPowerlineLogger(TestRE): + def test_args_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('foo {0}', 'Test') + pl.warn('bar {0!r}', 'Test') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:foo Test\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:bar u?\'Test\'\n$' + )) + + def test_prefix_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.prefix = '1' + pl.warn('foo') + pl.prefix = '2' + pl.warn('bar') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:1:foo\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:2:bar\n$' + )) + + def test_kwargs_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('foo {arg}', arg='Test') + pl.warn('bar {arg!r}', arg='Test') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:foo Test\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:bar u?\'Test\'\n$' + )) + + def test_args_kwargs_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('foo {0!r} {arg}', 'Test0', arg='Test') + pl.warn('bar {0} {arg!r}', 'Test0', arg='Test') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:foo u?\'Test0\' Test\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:bar Test0 u?\'Test\'\n$' + )) + + def test_exception_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + try: + raise ValueError('foo') + except ValueError: + pl.exception('Message') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Message\n' + + 'Traceback \\(most recent call last\\):\n' + + '(?: File ".*?", line \\d+, in \\w+\n [^\n]*\n)+' + + 'ValueError: foo\n$' + )) + + def test_levels(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {'log_level': 'DEBUG'}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.debug('1') + pl.info('2') + pl.warn('3') + pl.error('4') + pl.critical('5') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':DEBUG:__unknown__:1\n' + + TIMESTAMP_RE + ':INFO:__unknown__:2\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:3\n' + + TIMESTAMP_RE + ':ERROR:__unknown__:4\n' + + TIMESTAMP_RE + ':CRITICAL:__unknown__:5\n$' + )) + + +old_cwd = None + + +def setUpModule(): + global old_cwd + global __file__ + old_cwd = os.getcwd() + __file__ = os.path.abspath(__file__) + os.chdir(os.path.dirname(__file__)) + + +def tearDownModule(): + global old_cwd + os.chdir(old_cwd) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_segments.py b/tests/test_segments.py index a1c70c98..0a7adcbf 100644 --- a/tests/test_segments.py +++ b/tests/test_segments.py @@ -811,6 +811,12 @@ class TestSys(TestCommon): {'contents': '4 ', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, {'contents': '2', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 75.0} ]) + self.assertEqual(self.module.system_load(pl=pl, short=True), [ + {'contents': '7.5', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + ]) + self.assertEqual(self.module.system_load(pl=pl, format='{avg:.0f}', threshold_good=0, threshold_bad=1, short=True), [ + {'contents': '8', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + ]) def test_cpu_load_percent(self): try: @@ -883,49 +889,85 @@ class TestWthr(TestCommon): class TestI3WM(TestCase): - def test_workspaces(self): - pl = Pl() - with replace_attr(i3wm, 'conn', Args(get_workspaces=lambda: iter([ + @staticmethod + def get_workspaces(): + return iter([ {'name': '1: w1', 'output': 'LVDS1', 'focused': False, 'urgent': False, 'visible': False}, {'name': '2: w2', 'output': 'LVDS1', 'focused': False, 'urgent': False, 'visible': True}, {'name': '3: w3', 'output': 'HDMI1', 'focused': False, 'urgent': True, 'visible': True}, {'name': '4: w4', 'output': 'DVI01', 'focused': True, 'urgent': True, 'visible': True}, - ]))): - self.assertEqual(i3wm.workspaces(pl=pl), [ + ]) + + def test_workspaces(self): + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Args(get_workspaces=self.get_workspaces)): + segment_info = {} + + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info), [ {'contents': '1: w1', 'highlight_groups': ['workspace']}, {'contents': '2: w2', 'highlight_groups': ['w_visible', 'workspace']}, {'contents': '3: w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, ]) - self.assertEqual(i3wm.workspaces(pl=pl, only_show=None), [ + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=None), [ {'contents': '1: w1', 'highlight_groups': ['workspace']}, {'contents': '2: w2', 'highlight_groups': ['w_visible', 'workspace']}, {'contents': '3: w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, ]) - self.assertEqual(i3wm.workspaces(pl=pl, only_show=['focused', 'urgent']), [ + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['focused', 'urgent']), [ {'contents': '3: w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, ]) - self.assertEqual(i3wm.workspaces(pl=pl, only_show=['visible']), [ + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible']), [ {'contents': '2: w2', 'highlight_groups': ['w_visible', 'workspace']}, {'contents': '3: w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, ]) - self.assertEqual(i3wm.workspaces(pl=pl, only_show=['visible'], strip=3), [ + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], strip=3), [ {'contents': 'w2', 'highlight_groups': ['w_visible', 'workspace']}, {'contents': 'w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, {'contents': 'w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, ]) - self.assertEqual(i3wm.workspaces(pl=pl, only_show=['focused', 'urgent'], output='DVI01'), [ + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['focused', 'urgent'], output='DVI01'), [ {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, ]) - self.assertEqual(i3wm.workspaces(pl=pl, only_show=['visible'], output='HDMI1'), [ + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], output='HDMI1'), [ {'contents': '3: w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, ]) - self.assertEqual(i3wm.workspaces(pl=pl, only_show=['visible'], strip=3, output='LVDS1'), [ + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], strip=3, output='LVDS1'), [ {'contents': 'w2', 'highlight_groups': ['w_visible', 'workspace']}, ]) + segment_info['output'] = 'LVDS1' + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], output='HDMI1'), [ + {'contents': '3: w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], strip=3), [ + {'contents': 'w2', 'highlight_groups': ['w_visible', 'workspace']}, + ]) + + def test_workspace(self): + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Args(get_workspaces=self.get_workspaces)): + segment_info = {} + + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='1: w1'), [ + {'contents': '1: w1', 'highlight_groups': ['workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='3: w3', strip=True), [ + {'contents': 'w3', 'highlight_groups': ['w_urgent', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='9: w9'), None) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info), [ + {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, + ]) + segment_info['workspace'] = next(self.get_workspaces()) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='4: w4'), [ + {'contents': '4: w4', 'highlight_groups': ['w_focused', 'w_urgent', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, strip=True), [ + {'contents': 'w1', 'highlight_groups': ['workspace']}, + ]) def test_mode(self): pl = Pl() @@ -934,6 +976,45 @@ class TestI3WM(TestCase): self.assertEqual(i3wm.mode(pl=pl, segment_info={'mode': 'default'}, names={'default': 'test'}), 'test') self.assertEqual(i3wm.mode(pl=pl, segment_info={'mode': 'test'}, names={'default': 'test', 'test': 't'}), 't') + def test_scratchpad(self): + class Conn(object): + def get_tree(self): + return self + + def descendents(self): + nodes_unfocused = [Args(focused = False)] + nodes_focused = [Args(focused = True)] + + workspace_scratch = lambda: Args(name='__i3_scratch') + workspace_noscratch = lambda: Args(name='2: www') + return [ + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_unfocused), + Args(scratchpad_state='changed', urgent=True, workspace=workspace_noscratch, nodes=nodes_focused), + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_unfocused), + Args(scratchpad_state=None, urgent=False, workspace=workspace_noscratch, nodes=nodes_unfocused), + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_focused), + Args(scratchpad_state=None, urgent=True, workspace=workspace_noscratch, nodes=nodes_unfocused), + ] + + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Conn()): + self.assertEqual(i3wm.scratchpad(pl=pl), [ + {'contents': 'O', 'highlight_groups': ['scratchpad']}, + {'contents': 'X', 'highlight_groups': ['scratchpad:urgent', 'scratchpad:focused', 'scratchpad:visible', 'scratchpad']}, + {'contents': 'O', 'highlight_groups': ['scratchpad']}, + {'contents': 'X', 'highlight_groups': ['scratchpad:visible', 'scratchpad']}, + {'contents': 'O', 'highlight_groups': ['scratchpad:focused', 'scratchpad']}, + {'contents': 'X', 'highlight_groups': ['scratchpad:urgent', 'scratchpad:visible', 'scratchpad']}, + ]) + self.assertEqual(i3wm.scratchpad(pl=pl, icons={'changed': '-', 'fresh': 'o'}), [ + {'contents': 'o', 'highlight_groups': ['scratchpad']}, + {'contents': '-', 'highlight_groups': ['scratchpad:urgent', 'scratchpad:focused', 'scratchpad:visible', 'scratchpad']}, + {'contents': 'o', 'highlight_groups': ['scratchpad']}, + {'contents': '-', 'highlight_groups': ['scratchpad:visible', 'scratchpad']}, + {'contents': 'o', 'highlight_groups': ['scratchpad:focused', 'scratchpad']}, + {'contents': '-', 'highlight_groups': ['scratchpad:urgent', 'scratchpad:visible', 'scratchpad']}, + ]) + class TestMail(TestCommon): module_name = 'mail' diff --git a/tests/test_tabline.vim b/tests/test_tabline.vim index ded985ba..a2e1374f 100755 --- a/tests/test_tabline.vim +++ b/tests/test_tabline.vim @@ -16,7 +16,7 @@ catch cquit endtry -if result isnot# '%1T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc  %2T2 ./def %#Pl_236_3158064_240_5789784_NONE# %3T%#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %T%#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Tabs ' +if result isnot# '%1T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc %#Pl_244_8421504_236_3158064_NONE# %2T%#Pl_247_10395294_236_3158064_NONE#2 ./def %#Pl_236_3158064_240_5789784_NONE# %3T%#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %T%#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Tabs ' call writefile(['Unexpected tabline', result], 'message.fail') cquit endif @@ -30,7 +30,7 @@ catch cquit endtry -if result isnot# '%T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc  2 ./def %#Pl_236_3158064_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Bufs ' +if result isnot# '%T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc %#Pl_244_8421504_236_3158064_NONE# %#Pl_247_10395294_236_3158064_NONE#2 ./def %#Pl_236_3158064_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Bufs ' call writefile(['Unexpected tabline (2)', result], 'message.fail') cquit endif @@ -42,7 +42,7 @@ catch call writefile(['Exception while evaluating &tabline (3)', v:exception], 'message.fail') endtry -if result isnot# '%T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc  2 ./def %#Pl_236_3158064_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Bufs ' +if result isnot# '%T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc %#Pl_244_8421504_236_3158064_NONE# %#Pl_247_10395294_236_3158064_NONE#2 ./def %#Pl_236_3158064_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Bufs ' call writefile(['Unexpected tabline (3)', result], 'message.fail') cquit endif diff --git a/tests/vim.py b/tests/vim.py index 142d6929..3f6882c8 100644 --- a/tests/vim.py +++ b/tests/vim.py @@ -265,6 +265,14 @@ def eval(expr): import os assert os.path.basename(current.buffer.name).startswith('NERD_tree_') return '/usr/include' + elif expr.startswith('getbufvar('): + import re + match = re.match(r'^getbufvar\((\d+), ["\'](.+)["\']\)$', expr) + if not match: + raise NotImplementedError(expr) + bufnr = int(match.group(1)) + varname = match.group(2) + return _emul_getbufvar(bufnr, varname) elif expr == 'tabpagenr()': return current.tabpage.number elif expr == 'tabpagenr("$")':