diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..94eab89c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# editorconfig ini file +# Check out http://editorconfig.org for a list of plugins for different +# IDEs/text editors that support this file. Vim plugin to support this: +# +# http://www.vim.org/scripts/script.php?script_id=3934 +# https://github.com/editorconfig/editorconfig-vim +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = tab +# Despite promise somewhere alignment is done only using tabs. Thus setting +# indent_size and tab_width is a requirement. +indent_size = 4 +tab_width = 4 +charset = utf-8 + +[*.rst] +indent_style = space diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..e3074859 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.rst whitespace=-blank-at-eol diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..dbae1ed8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +tags + +*.py[co] +__pycache__ + +*.egg +*.egg-info +dist +build + +message.fail + +client/powerline diff --git a/.local.vimrc b/.local.vimrc new file mode 100644 index 00000000..c8e1ef38 --- /dev/null +++ b/.local.vimrc @@ -0,0 +1,11 @@ +" Project vimrc file. To be sourced each time you open any file in this +" repository. You may use [vimscript #3393][1] [(homepage)][2] to do this +" automatically. +" +" [1]: http://www.vim.org/scripts/script.php?script_id=3393 +" [2]: https://github.com/thinca/vim-localrc +let g:syntastic_python_flake8_args = '--ignore=W191,E501,E128,W291,E126,E101' +let b:syntastic_checkers = ['flake8'] +unlet! g:python_space_error_highlight +let g:pymode_syntax_indent_errors = 0 +let g:pymode_syntax_space_errors = 0 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..4eccbf31 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.2" + - "3.3" + - "3.4" + - "pypy" +install: tests/install.sh +script: tests/test.sh + +# vim: et diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..2ae9facd --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,137 @@ +***************** +How to contribute +***************** + +So you want to contribute to the Powerline project? Awesome! This document +describes the guidelines you should follow when making contributions to the +project. + +**Please note that these guidelines aren't mandatory in any way, but your +pull request will be merged a lot faster if you follow them.** + +Getting started +=============== + +* Make sure you have a `GitHub account `_. +* Submit an `issue on GitHub + `_, assuming one does not + already exist. + + * Clearly describe the issue. + * If the issue is a bug: make sure you include steps to reproduce, and + include the earliest revision that you know has the issue. + +* Fork the repository on GitHub. + +Making changes +============== + +* Create a topic branch from where you want to base your work. + + * Powerline uses the `Git Flow + `_ branching + model. + * Most contributions should be based off the ``develop`` branch. + * Prefix your branch with ``feature/`` if you're working on a new feature. + * Include the issue number in your topic branch, e.g. + ``321-fix-some-error`` or ``feature/123-a-cool-feature``. + +* Make commits of logical units. +* Run your code through ``flake8`` and fix any programming style errors. Use + common sense regarding whitespace warnings, not all warnings need to be + fixed. +* Make sure your commit messages are in the `proper format + `_. + The summary must be no longer than 70 characters. Refer to any related + issues with e.g. ``Ref #123`` or ``Fixes #234`` at the bottom of the + commit message. Commit messages can use Markdown with the following + exceptions: + + * No HTML extensions. + * Only indented code blocks (no ``````` blocks). + * Long links should be moved to the bottom if they make the text wrap or + extend past 72 columns. + +* Make sure you have added the necessary tests for your changes. +* Run *all* the tests to assure nothing else was accidentally broken. + +Programming style +----------------- + +* The project uses *tabs for indentation* and *spaces for alignment*, this + is also included in a vim modeline on top of every script file. +* Run your code through ``flake8 --ignore=W191,E501,E128,W291,E126,E101`` to fix + any style errors. Use common sense regarding whitespace warnings, not all + ``flake8`` warnings need to be fixed. +* Trailing whitespace to indicate a continuing paragraph is OK in comments, + documentation and commit messages. +* It is allowed to have too long lines. It is advised though to avoid lines + wider then a hundred of characters. +* Imports have the following structure: + + 1. Shebang and modeline in a form + + .. code-block:: python + + #!/usr/bin/env python + # vim:fileencoding=utf-8:noet + + . Modeline is required, shebang is not. If shebang is present file must end + with + + .. code-block:: python + + if __name__ == '__main__': + # Actual script here + + 2. Module docstring. + 3. ``__future__`` import exactly in a form + + .. code-block:: python + + from __future__ import (unicode_literals, division, absolute_import, print_function) + + (powerline.shell is the only exception due to problems with argparse). It + is not separated by newline with shebang and modeline, but is with + docstring. + 4. Standard python library imports in a form ``import X``. + 5. Standard python library imports in a form ``from X import Y``. + 6. Third-party (non-python and non-powerline) library imports in a form + ``import X``. + 7. Third-party library imports in a form ``from X import Y``. + 8. Powerline non-test imports in a form ``from powerline.X import Y``. + 9. Powerline test imports in a form ``import tests.vim as vim_module``. + 10. Powerline test imports in a form ``from tests.X import Y``. + + Each entry is separated by newline from another entry. Any entry except for + the first and third ones is optional. Example with all entries: + + .. code-block:: python + + #!/usr/bin/env python + # vim:fileencoding=utf-8:noet + + '''Powerline super module''' + + import sys + + from argparse import ArgumentParser + + import psutil + + from colormath.color_diff import delta_e_cie2000 + + from powerline.lib.unicode import u + + import tests.vim as vim_module + + from tests import TestCase + +Submitting changes +================== + +* Push your changes to a topic branch in your fork of the repository. +* If necessary, use ``git rebase -i `` to squash or reword commits + before submitting a pull request. +* Submit a pull request to `Lokaltog's repository + `_. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ad3cf61a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Copyright 2013 Kim Silkebækken and other contributors +https://github.com/Lokaltog/powerline + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..1e03cb74 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include powerline *.json *.vim +recursive-include powerline/bindings *.* +recursive-exclude powerline/bindings *.pyc *.pyo +recursive-include client *.* diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..efc6d4a0 --- /dev/null +++ b/README.rst @@ -0,0 +1,95 @@ +Powerline +========= + +:Author: Kim Silkebækken (kim.silkebaekken+vim@gmail.com) +:Source: https://github.com/Lokaltog/powerline +:Version: beta + +**Powerline is a statusline plugin for vim, and provides statuslines and +prompts for several other applications, including zsh, bash, tmux, IPython, +Awesome and Qtile.** + +* `Support forum`_ (powerline-support@googlegroups.com) +* `Development discussion`_ (powerline-dev@googlegroups.com) + +.. image:: https://api.travis-ci.org/Lokaltog/powerline.png?branch=develop + :target: `travis-build-status`_ + :alt: Build status + +.. _travis-build-status: https://travis-ci.org/Lokaltog/powerline +.. _`Support forum`: https://groups.google.com/forum/#!forum/powerline-support +.. _`Development discussion`: https://groups.google.com/forum/#!forum/powerline-dev + +Features +-------- + +* **Extensible and feature rich, written in Python.** Powerline was + completely rewritten in Python to get rid of as much vimscript as + possible. This has allowed much better extensibility, leaner and better + config files, and a structured, object-oriented codebase with no mandatory + third-party dependencies other than a Python interpreter. +* **Stable and testable code base.** Using Python has allowed unit testing + of all the project code. The code is tested to work in Python 2.6+ and + Python 3. +* **Support for prompts and statuslines in many applications.** Originally + created exclusively for vim statuslines, the project has evolved to + provide statuslines in tmux and several WMs, and prompts for shells like + bash/zsh and other applications. It's simple to write renderers for any + other applications that Powerline doesn't yet support. +* **Configuration and colorschemes written in JSON.** JSON is + a standardized, simple and easy to use file format that allows for easy + user configuration across all of Powerline's supported applications. +* **Fast and lightweight, with daemon support for even better performance.** + Although the code base spans a couple of thousand lines of code with no + goal of "less than X lines of code", the main focus is on good performance + and as little code as possible while still providing a rich set of + features. The new daemon also ensures that only one Python instance is + launched for prompts and statuslines, which provides excellent + performance. + +*But I hate Python / I don't need shell prompts / this is just too much +hassle for me / what happened to the original vim-powerline project / …* + +You should check out some of the Powerline derivatives. The most lightweight +and feature-rich alternative is currently Bailey Ling's `vim-airline +`_ project. + +------ + +* Consult the `documentation + `_ for more information and + installation instructions. +* Check out `powerline-fonts `_ + for pre-patched versions of popular, open source coding fonts. + +Screenshots +----------- + +Vim statusline +^^^^^^^^^^^^^^ + +**Mode-dependent highlighting** + +* .. image:: https://raw.github.com/Lokaltog/powerline/develop/docs/source/_static/img/pl-mode-normal.png + :alt: Normal mode +* .. image:: https://raw.github.com/Lokaltog/powerline/develop/docs/source/_static/img/pl-mode-insert.png + :alt: Insert mode +* .. image:: https://raw.github.com/Lokaltog/powerline/develop/docs/source/_static/img/pl-mode-visual.png + :alt: Visual mode +* .. image:: https://raw.github.com/Lokaltog/powerline/develop/docs/source/_static/img/pl-mode-replace.png + :alt: Replace mode + +**Automatic truncation of segments in small windows** + +* .. image:: https://raw.github.com/Lokaltog/powerline/develop/docs/source/_static/img/pl-truncate1.png + :alt: Truncation illustration +* .. image:: https://raw.github.com/Lokaltog/powerline/develop/docs/source/_static/img/pl-truncate2.png + :alt: Truncation illustration +* .. image:: https://raw.github.com/Lokaltog/powerline/develop/docs/source/_static/img/pl-truncate3.png + :alt: Truncation illustration + +---- + +The font in the screenshots is `Pragmata Pro`_ by Fabrizio Schiavi. + +.. _`Pragmata Pro`: http://www.fsd.it/fonts/pragmatapro.htm diff --git a/client/powerline.c b/client/powerline.c new file mode 100644 index 00000000..f53e3457 --- /dev/null +++ b/client/powerline.c @@ -0,0 +1,143 @@ +/* vim:fileencoding=utf-8:noet + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HANDLE_ERROR(msg) \ + do { \ + perror(msg); \ + exit(EXIT_FAILURE); \ + } while (0) + +#define TEMP_FAILURE_RETRY(var, expression) \ + do { \ + ptrdiff_t __result; \ + do { \ + __result = (expression); \ + } while (__result == -1L && errno == EINTR); \ + var = __result; \ + } while (0) + +extern char **environ; + +void do_write(int sd, const char *raw, size_t len) { + size_t written = 0; + ptrdiff_t n = -1; + + while (written < len) { + TEMP_FAILURE_RETRY(n, write(sd, raw + written, len - written)); + if (n == -1) { + close(sd); + HANDLE_ERROR("write() failed"); + } + written += (size_t) n; + } +} + +#ifdef __APPLE__ +# define ADDRESS_TEMPLATE "/tmp/powerline-ipc-%d" +# define A +#else +# define ADDRESS_TEMPLATE "powerline-ipc-%d" +# define A +1 +#endif + +#define ADDRESS_SIZE sizeof(ADDRESS_TEMPLATE) + (sizeof(uid_t) * 4) +#define NUM_ARGS_SIZE (sizeof(int) * 2) +#define BUF_SIZE 4096 +#define NEW_ARGV_SIZE 200 + +int main(int argc, char *argv[]) { + int sd = -1; + int i; + ptrdiff_t read_size; + struct sockaddr_un server; + char address_buf[ADDRESS_SIZE]; + const char eof[2] = "\0\0"; + char num_args[NUM_ARGS_SIZE]; + char buf[BUF_SIZE]; + char *newargv[NEW_ARGV_SIZE]; + char *wd = NULL; + char **envp; + const char *address; + + if (argc < 2) { + printf("Must provide at least one argument.\n"); + return EXIT_FAILURE; + } + + if (argc > 3 && strcmp(argv[1], "--socket") == 0) { + address = argv[2]; + argv += 2; + argc -= 2; + } else { + snprintf(address_buf, ADDRESS_SIZE, ADDRESS_TEMPLATE, getuid()); + address = &(address_buf[0]); + } + + sd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sd == -1) + HANDLE_ERROR("socket() failed"); + + memset(&server, 0, sizeof(struct sockaddr_un)); + server.sun_family = AF_UNIX; + strncpy(server.sun_path A, address, strlen(address)); + + if (connect(sd, (struct sockaddr *) &server, (socklen_t) (sizeof(server.sun_family) + strlen(address) A)) < 0) { + close(sd); + /* We failed to connect to the daemon, execute powerline instead */ + argc = (argc < NEW_ARGV_SIZE - 1) ? argc : NEW_ARGV_SIZE - 1; + for (i = 1; i < argc; i++) + newargv[i] = argv[i]; + newargv[0] = "powerline-render"; + newargv[argc] = NULL; + execvp("powerline-render", newargv); + } + + snprintf(num_args, NUM_ARGS_SIZE, "%x", argc - 1); + do_write(sd, num_args, strlen(num_args)); + do_write(sd, eof, 1); + + for (i = 1; i < argc; i++) { + do_write(sd, argv[i], strlen(argv[i])); + do_write(sd, eof, 1); + } + + wd = getcwd(NULL, 0); + if (wd != NULL) { + do_write(sd, wd, strlen(wd)); + free(wd); + wd = NULL; + } + do_write(sd, eof, 1); + + for(envp=environ; *envp; envp++) { + do_write(sd, *envp, strlen(*envp)); + do_write(sd, eof, 1); + } + + do_write(sd, eof, 2); + + read_size = -1; + while (read_size != 0) { + TEMP_FAILURE_RETRY(read_size, read(sd, buf, BUF_SIZE)); + if (read_size == -1) { + close(sd); + HANDLE_ERROR("read() failed"); + } else if (read_size > 0) { + do_write(STDOUT_FILENO, buf, (size_t) read_size); + } + } + + close(sd); + + return 0; +} diff --git a/client/powerline.py b/client/powerline.py new file mode 100755 index 00000000..07a01cb6 --- /dev/null +++ b/client/powerline.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import socket +import errno +import os + +from locale import getpreferredencoding + +try: + from posix import environ +except ImportError: + from os import environ + + +if len(sys.argv) < 2: + print('Must provide at least one argument.', file=sys.stderr) + raise SystemExit(1) + +platform = sys.platform.lower() +use_filesystem = 'darwin' in platform +del platform + +if sys.argv[1] == '--socket': + address = sys.argv[2] + if not use_filesystem: + address = '\0' + address + del sys.argv[1:3] +else: + address = ('/tmp/powerline-ipc-%d' if use_filesystem else '\0powerline-ipc-%d') % os.getuid() + +sock = socket.socket(family=socket.AF_UNIX) + + +def eintr_retry_call(func, *args, **kwargs): + while True: + try: + return func(*args, **kwargs) + except EnvironmentError as e: + if getattr(e, 'errno', None) == errno.EINTR: + continue + raise + + +try: + eintr_retry_call(sock.connect, address) +except Exception: + # Run the powerline renderer + args = ['powerline-render'] + sys.argv[1:] + os.execvp('powerline-render', args) + +fenc = getpreferredencoding() or 'utf-8' + + +def tobytes(s): + if isinstance(s, bytes): + return s + else: + return s.encode(fenc) + + +args = [tobytes('%x' % (len(sys.argv) - 1))] +args.extend((tobytes(s) for s in sys.argv[1:])) + + +try: + cwd = os.getcwd() +except EnvironmentError: + pass +else: + if not isinstance(cwd, bytes): + cwd = cwd.encode(fenc) + args.append(cwd) + + +args.extend((tobytes(k) + b'=' + tobytes(v) for k, v in environ.items())) + +EOF = b'\0\0' + +for a in args: + eintr_retry_call(sock.sendall, a + b'\0') + +eintr_retry_call(sock.sendall, EOF) + +received = [] +while True: + r = sock.recv(4096) + if not r: + break + received.append(r) + +sock.close() + +if sys.version_info < (3,): + sys.stdout.write(b''.join(received)) +else: + sys.stdout.buffer.write(b''.join(received)) diff --git a/client/powerline.sh b/client/powerline.sh new file mode 100755 index 00000000..b112ec21 --- /dev/null +++ b/client/powerline.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +test "${OSTYPE#darwin}" = "${OSTYPE}" && darwin=n || darwin=y + +if test "$1" = "--socket" ; then + shift + ADDRESS="$1" + shift +else + ADDRESS="powerline-ipc-${UID:-`id -u`}" + test "$darwin" = y && ADDRESS="/tmp/$ADDRESS" +fi + +if test "$darwin" = y; then + ENV=genv +else + ENV=env + ADDRESS="abstract-client:$ADDRESS" +fi + +# Warning: env -0 does not work in busybox. Consider switching to parsing +# `set` output in this case +( + printf '%x\0' "$#" + for argv in "$@" ; do + printf '%s\0' "$argv" + done + printf '%s\0' "$PWD" + $ENV -0 +) | socat -lf/dev/null -t 10 - "$ADDRESS" + +if test $? -ne 0 ; then + powerline-render "$@" +fi diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..e35d8850 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +_build diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..3a9d3de8 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,36 @@ +# Makefile for Sphinx documentation +SPHINXOPTS = +SPHINXBUILD = sphinx-build2 +PAPER = +BUILDDIR = _build + +# Internal variables +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +GH_PAGES_SOURCES = source Makefile +GH_SOURCE_BRANCH = develop + +.PHONY: clean html gh-pages + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +gh-pages: + git checkout gh-pages + find .. -maxdepth 1 ! -name '.git*' -and ! -name 'docs' -exec rm -rf {} \; + git checkout ${GH_SOURCE_BRANCH} ${GH_PAGES_SOURCES} + git reset HEAD + make html + mv -fv _build/html/* .. + rm -rf _build ${GH_PAGES_SOURCES} + git add .. + git commit -m "Create gh-pages for '`git log develop -1 --pretty=oneline --abbrev-commit`'" + git push origin gh-pages + git checkout ${GH_SOURCE_BRANCH} diff --git a/docs/source/_static/img/icons/cross.png b/docs/source/_static/img/icons/cross.png new file mode 100644 index 00000000..33a38374 Binary files /dev/null and b/docs/source/_static/img/icons/cross.png differ diff --git a/docs/source/_static/img/icons/error.png b/docs/source/_static/img/icons/error.png new file mode 100644 index 00000000..dbfda229 Binary files /dev/null and b/docs/source/_static/img/icons/error.png differ diff --git a/docs/source/_static/img/icons/tick.png b/docs/source/_static/img/icons/tick.png new file mode 100644 index 00000000..c277e6b4 Binary files /dev/null and b/docs/source/_static/img/icons/tick.png differ diff --git a/docs/source/_static/img/pl-mode-insert.png b/docs/source/_static/img/pl-mode-insert.png new file mode 100644 index 00000000..9b09e185 Binary files /dev/null and b/docs/source/_static/img/pl-mode-insert.png differ diff --git a/docs/source/_static/img/pl-mode-normal.png b/docs/source/_static/img/pl-mode-normal.png new file mode 100644 index 00000000..29d37166 Binary files /dev/null and b/docs/source/_static/img/pl-mode-normal.png differ diff --git a/docs/source/_static/img/pl-mode-replace.png b/docs/source/_static/img/pl-mode-replace.png new file mode 100644 index 00000000..d7c89a45 Binary files /dev/null and b/docs/source/_static/img/pl-mode-replace.png differ diff --git a/docs/source/_static/img/pl-mode-visual.png b/docs/source/_static/img/pl-mode-visual.png new file mode 100644 index 00000000..d654763b Binary files /dev/null and b/docs/source/_static/img/pl-mode-visual.png differ diff --git a/docs/source/_static/img/pl-truncate1.png b/docs/source/_static/img/pl-truncate1.png new file mode 100644 index 00000000..c687502c Binary files /dev/null and b/docs/source/_static/img/pl-truncate1.png differ diff --git a/docs/source/_static/img/pl-truncate2.png b/docs/source/_static/img/pl-truncate2.png new file mode 100644 index 00000000..1630f1d1 Binary files /dev/null and b/docs/source/_static/img/pl-truncate2.png differ diff --git a/docs/source/_static/img/pl-truncate3.png b/docs/source/_static/img/pl-truncate3.png new file mode 100644 index 00000000..83e5b217 Binary files /dev/null and b/docs/source/_static/img/pl-truncate3.png differ diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..f451f462 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,31 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys + + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(os.getcwd())))) +sys.path.insert(0, os.path.abspath(os.getcwd())) + +extensions = ['powerline_autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] +source_suffix = '.rst' +master_doc = 'index' +project = u'Powerline' +version = 'beta' +release = 'beta' +exclude_patterns = ['_build'] +pygments_style = 'sphinx' +html_theme = 'default' +html_static_path = ['_static'] +html_show_copyright = False + +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +if not on_rtd: # only import and set the theme if we're building docs locally + try: + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + except ImportError: + pass diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst new file mode 100644 index 00000000..fa7c420d --- /dev/null +++ b/docs/source/configuration.rst @@ -0,0 +1,138 @@ +******************************* +Configuration and customization +******************************* + +.. note:: **You DO NOT have to fork the main GitHub repo to personalize your + Powerline configuration!** Please read through the :ref:`quick-guide` for + a quick introduction to user configuration. + +Powerline is configured with one main configuration file, and with separate +configuration files for themes and colorschemes. All configuration files are +written in JSON, with the exception of segment definitions, which are +written in Python. + +Powerline provides default configurations in the following locations: + +:ref:`Main configuration ` + :file:`powerline/config.json` +:ref:`Colorschemes ` + :file:`powerline/colorschemes/{name}.json`, + :file:`powerline/colorscheme/{extension}/__main__.json`, + :file:`powerline/colorschemes/{extension}/{name}.json` +:ref:`Themes ` + :file:`powerline/themes/{top_theme}.json`, + :file:`powerline/themes/{extension}/__main__.json`, + :file:`powerline/themes/{extension}/default.json` + +The default configuration files are stored in the main package. User +configuration files are stored in :file:`$XDG_CONFIG_HOME/powerline` for +Linux users, and in :file:`~/.config/powerline` for OS X users. This usually +corresponds to :file:`~/.config/powerline` on both platforms. + +If you need per-instance configuration please refer to :ref:`Local configuration +overrides `. + +.. note:: If you have multiple configuration files with the same name in + different directories then these files will be merged. Merging happens in + the following order: + + * :file:`{powerline_root}/powerline/config_files` is checked for + configuration first. Configuration from this source has least priority. + * :file:`$XDG_CONFIG_DIRS/powerline` directories are the next ones to check. + Checking happens in the reversed order: directories mentioned last are + checked before directories mentioned first. Each new found file is merged + with the result of previous merge. + * :file:`$XDG_CONFIG_HOME/powerline` directory is the last to check. + Configuration from there has top priority. + + When merging configuration only dictionaries are merged and they are merged + recursively: keys from next file overrule those from the previous unless + corresponding values are both dictionaries in which case these dictionaries + are merged and key is assigned the result of the merge. + +.. note:: Some configuration files (i.e. themes and colorschemes) have two level + of merging: first happens merging described above, second theme- or + colorscheme-specific merging happens. + +.. _quick-guide: + +Quick setup guide +================= + +This guide will help you with the initial configuration of Powerline. + +Start by copying the entire set of default configuration files to the +corresponding path in your user config directory: + +.. code-block:: sh + + mkdir ~/.config/powerline + cp -R /path/to/powerline/config_files/* ~/.config/powerline + +Each extension (vim, tmux, etc.) has its own theme, and they are located in +:file:`{config directory}/themes/{extension}/default.json`. + +If you want to move, remove or customize any of the provided segments, you +can do that by updating the segment dictionary in the theme you want to +customize. A segment dictionary looks like this: + +.. code-block:: javascript + + { + "name": "segment_name" + ... + } + +You can move the segment dictionaries around to change the segment +positions, or remove the entire dictionary to remove the segment from the +prompt or statusline. + +.. note:: It's essential that the contents of all your configuration files + is valid JSON! It's strongly recommended that you run your configuration + files through ``jsonlint`` after changing them. + +Some segments need a user configuration to work properly. Here's a couple of +segments that you may want to customize right away: + +**E-mail alert segment** + You have to set your username and password (and possibly server/port) + for the e-mail alert segment. If you're using GMail it's recommended + that you `generate an application-specific password + `_ for this purpose. + + Open a theme file, scroll down to the ``email_imap_alert`` segment and + set your ``username`` and ``password``. The server defaults to GMail's + IMAP server, but you can set the server/port by adding a ``server`` and + a ``port`` argument. +**Weather segment** + The weather segment will try to find your location using a GeoIP lookup, + so unless you're on a VPN you probably won't have to change the location + query. + + If you want to change the location query or the temperature unit you'll + have to update the segment arguments. Open a theme file, scroll down to + the weather segment and update it to include unit/location query + arguments: + + .. code-block:: javascript + + { + "name": "weather", + "priority": 50, + "args": { + "unit": "F", + "location_query": "oslo, norway" + } + }, + +References +========== + +.. toctree:: + :glob: + + configuration/reference + configuration/segments + configuration/listers + configuration/selectors + configuration/local diff --git a/docs/source/configuration/listers.rst b/docs/source/configuration/listers.rst new file mode 100644 index 00000000..dc544b91 --- /dev/null +++ b/docs/source/configuration/listers.rst @@ -0,0 +1,25 @@ +.. _config-listers: + +**************** +Lister reference +**************** + +Listers are special segment collections which allow to show some list of +segments for each entity in the list of entities (multiply their segments list +by a list of entities). E.g. ``powerline.listers.vim.tablister`` presented with +``powerline.segments.vim.tabnr`` and ``….file_name`` as segments will emit +segments with buffer names and tabpage numbers for each tabpage shown by vim. + +Listers appear in configuration as irregular segments having ``segment_list`` as +their type and ``segments`` key with a list of segments (a bit more details in +:ref:`Themes section of configuration reference `). + +More information in :ref:`Writing listers ` section. + +Currently only Vim listers are available. + +Vim listers +----------- + +.. automodule:: powerline.listers.vim + :members: diff --git a/docs/source/configuration/local.rst b/docs/source/configuration/local.rst new file mode 100644 index 00000000..385bdfcf --- /dev/null +++ b/docs/source/configuration/local.rst @@ -0,0 +1,147 @@ +.. _local-configuration-overrides: + +***************************** +Local configuration overrides +***************************** + +Depending on the application used it is possible to override configuration. Here +is the list: + +Vim overrides +============= + +Vim configuration can be overridden using the following options: + +``g:powerline_config_overrides`` + Dictionary, recursively merged with contents of + :file:`powerline/config.json`. + +``g:powerline_theme_overrides__{theme_name}`` + Dictionary, recursively merged with contents of + :file:`powerline/themes/vim/{theme_name}.json`. Note that this way you can’t + redefine some value (e.g. segment) in list, only the whole list itself: only + dictionaries are merged recursively. + +``g:powerline_config_paths`` + Paths list (each path must be expanded, ``~`` shortcut is not supported). + Points to the list of directories which will be searched for configuration. + When this option is present, none of the other locations are searched. + +``g:powerline_no_python_error`` + If this variable is set to a true value it will prevent Powerline from reporting + an error when loaded in a copy of vim without the necessary Python support. + +Powerline script overrides +========================== + +Powerline script has a number of options controlling powerline behavior. Here +``VALUE`` always means “some JSON object”. + +``-c KEY.NESTED_KEY=VALUE`` or ``--config=KEY.NESTED_KEY=VALUE`` + Overrides options from :file:`powerline/config.json`. + ``KEY.KEY2.KEY3=VALUE`` is a shortcut for ``KEY={"KEY2": {"KEY3": VALUE}}``. + Multiple options (i.e. ``-c K1=V1 -c K2=V2``) are allowed, result (in the + example: ``{"K1": V1, "K2": V2}``) is recursively merged with the contents + of the file. + + If ``VALUE`` is omitted then corresponding key will be removed from the + configuration (if it was present). + +``-t THEME_NAME.KEY.NESTED_KEY=VALUE`` or ``--theme_option=THEME_NAME.KEY.NESTED_KEY=VALUE`` + Overrides options from :file:`powerline/themes/{ext}/{THEME_NAME}.json`. + ``KEY.NESTED_KEY=VALUE`` is processed like described above, ``{ext}`` is the + first argument to powerline script. May be passed multiple times. + + If ``VALUE`` is omitted then corresponding key will be removed from the + configuration (if it was present). + +``-p PATH`` or ``--config_path=PATH`` + Sets directory where configuration should be read from. If present, no + default locations are searched for configuration. No expansions are + performed by powerline script itself, but ``-p ~/.powerline`` will likely be + expanded by the shell to something like ``-p /home/user/.powerline``. + +Zsh/zpython overrides +===================== + +Here overrides are controlled by similarly to the powerline script, but values +are taken from zsh variables. + +``POWERLINE_CONFIG`` + Overrides options from :file:`powerline/config.json`. Should be a zsh + associative array with keys equal to ``KEY.NESTED_KEY`` and values being + JSON strings. Pair ``KEY.KEY1 VALUE`` is equivalent to ``{"KEY": {"KEY1": + VALUE}}``. All pairs are then recursively merged into one dictionary and + this dictionary is recursively merged with the contents of the file. + +``POWERLINE_THEME_CONFIG`` + Overrides options from :file:`powerline/themes/shell/*.json`. Should be + a zsh associative array with keys equal to ``THEME_NAME.KEY.NESTED_KEY`` and + values being JSON strings. Is processed like the above ``POWERLINE_CONFIG``, + but only subdictionaries for ``THEME_NAME`` key are merged with theme + configuration when theme with given name is requested. + +``POWERLINE_CONFIG_PATHS`` + Sets directories where configuration should be read from. If present, no + default locations are searched for configuration. No expansions are + performed by powerline script itself, but zsh usually performs them on its + own if you set variable without quotes: ``POWERLINE_CONFIG_PATHS=( ~/example + )``. You should use array parameter or the usual colon-separated + ``POWERLINE_CONFIG_PATHS=$HOME/path1:$HOME/path2``. + +Ipython overrides +================= + +Ipython overrides depend on ipython version. Before ipython-0.11 you should pass +additional keyword arguments to setup() function. After ipython-0.11 you should +use ``c.Powerline.KEY``. Supported ``KEY`` strings or keyword argument names: + +``config_overrides`` + Overrides options from :file:`powerline/config.json`. Should be a dictionary + that will be recursively merged with the contents of the file. + +``theme_overrides`` + Overrides options from :file:`powerline/themes/ipython/*.json`. Should be + a dictionary where keys are theme names and values are dictionaries which + will be recursively merged with the contents of the given theme. + +``paths`` + Sets directories where configuration should be read from. If present, no + default locations are searched for configuration. No expansions are + performed thus you cannot use paths starting with ``~/``. + +Prompt command +============== + +In addition to the above configuration options you can use +``$POWERLINE_COMMAND`` environment variable to tell shell or tmux to use +specific powerline implementation and ``$POWERLINE_CONFIG`` to tell zsh or tmux +where ``powerline-config`` script is located. This is mostly useful for putting +powerline into different directory. + +.. note:: + + ``$POWERLINE_COMMAND`` appears in shell scripts without quotes thus you can + specify additional parameters in bash. In tmux it is passed to ``eval`` and + depends on the shell used. POSIX-compatible shells, zsh, bash and fish will + split this variable in this case. + +If you want to disable prompt in shell, but still have tmux support or if you +want to disable tmux support you can use variables +``$POWERLINE_NO_{SHELL}_PROMPT``/``$POWERLINE_NO_SHELL_PROMPT`` and +``$POWERLINE_NO_{SHELL}_TMUX_SUPPORT``/``$POWERLINE_NO_SHELL_TMUX_SUPPORT`` +(substitute ``{SHELL}`` with the name of the shell (all-caps) you want to +disable support for (e.g. ``BASH``) or use all-inclusive ``SHELL`` that will +disable support for all shells). These variables have no effect after +configuration script was sourced (in fish case: after ``powerline-setup`` +function was run). To disable specific feature support set one of these +variables to some non-empty value. + +If you do not want to disable prompt in shell, but yet do not want to launch +python twice to get :ref:`above ` lines you do not use in +tcsh you should set ``$POWERLINE_NO_TCSH_ABOVE`` or +``$POWERLINE_NO_SHELL_ABOVE`` variable. + +If you do not want to see additional space which is added to the right prompt in +fish in order to support multiline prompt you should set +``$POWERLINE_NO_FISH_ABOVE`` or ``$POWERLINE_NO_SHELL_ABOVE`` variables. diff --git a/docs/source/configuration/reference.rst b/docs/source/configuration/reference.rst new file mode 100644 index 00000000..76931690 --- /dev/null +++ b/docs/source/configuration/reference.rst @@ -0,0 +1,537 @@ +*********************** +Configuration reference +*********************** + +.. _config-main: + +Main configuration +================== + +:Location: :file:`powerline/config.json` + +The main configuration file defines some common options that applies to all +extensions, as well as some extension-specific options like themes and +colorschemes. + +Common configuration +-------------------- + +Common configuration is a subdictionary that is a value of ``common`` key in +:file:`powerline/config.json` file. + +.. _config-common-term_truecolor: + +``term_truecolor`` + Defines whether to output cterm indices (8-bit) or RGB colors (24-bit) + to the terminal emulator. See the :ref:`term-feature-support-matrix` for + information on whether your terminal emulator supports 24-bit colors. + +.. _config-common-ambiwidth: + +``ambiwidth`` + Tells powerline what to do with characters with East Asian Width Class + Ambigious (such as Euro, Registered Sign, Copyright Sign, Greek + letters, Cyrillic letters). Valid values: any positive integer; it is + suggested that you only set it to 1 (default) or 2. + +.. _config-common-watcher: + +``watcher`` + Select filesystem watcher. Variants are + + ======= =================================== + Variant Description + ======= =================================== + auto Selects most performant watcher. + inotify Select inotify watcher. Linux only. + stat Select stat-based polling watcher. + uv Select libuv-based watcher. + ======= =================================== + + Default is ``auto``. + +.. _config-common-additional_escapes: + +``additional_escapes`` + Valid for shell extensions, makes sense only if :ref:`term_truecolor + ` is enabled. Is to be set from command-line + (unless you are sure you always need it). Controls additional escaping that + is needed for tmux/screen to work with terminal true color escape codes: + normally tmux/screen prevent terminal emulator from receiving these control + codes thus rendering powerline prompt colorless. Valid values: ``"tmux"``, + ``"screen"``, ``null`` (default). + +.. _config-common-paths: + +``paths`` + Defines additional paths which will be searched for modules when using + :ref:`function segment option ` or :ref:`Vim + local_themes option `. Paths defined here have + priority when searching for modules. + +.. _config-common-log: + +``log_file`` + Defines path which will hold powerline logs. If not present, logging will be + done to stderr. + +``log_level`` + String, determines logging level. Defaults to ``WARNING``. + +``log_format`` + String, determines format of the log messages. Defaults to + ``'%(asctime)s:%(level)s:%(message)s'``. + +``interval`` + Number, determines time (in seconds) between checks for changed + configuration. Checks are done in a seprate thread. Use ``null`` to check + for configuration changes on ``.render()`` call in main thread. + Defaults to ``None``. + +``reload_config`` + Boolean, determines whether configuration should be reloaded at all. + Defaults to ``True``. + +.. _config-common-default_top_theme: + +``default_top_theme`` + String, determines which top-level theme will be used as the default. + Defaults to ``powerline`` in unicode locales and ``ascii`` in non-unicode + locales. See `Themes`_ section for more details. + +Extension-specific configuration +-------------------------------- + +Common configuration is a subdictionary that is a value of ``ext`` key in +:file:`powerline/config.json` file. + +``colorscheme`` + Defines the colorscheme used for this extension. + +.. _config-ext-theme: + +``theme`` + Defines the theme used for this extension. + +.. _config-ext-top_theme: + +``top_theme`` + Defines the top-level theme used for this extension. See `Themes`_ section + for more details. + +.. _config-ext-local_themes: + +``local_themes`` + Defines themes used when certain conditions are met, e.g. for + buffer-specific statuslines in vim. Value depends on extension used. For vim + it is a dictionary ``{matcher_name : theme_name}``, where ``matcher_name`` + is either ``matcher_module.module_attribute`` or ``module_attribute`` + (``matcher_module`` defaults to ``powerline.matchers.vim``) and + ``module_attribute`` should point to a function that returns boolean value + indicating that current buffer has (not) matched conditions. There is an + exception for ``matcher_name`` though: if it is ``__tabline__`` no functions + are loaded. This special theme is used for ``tabline`` Vim option. + + For shell and ipython it is a simple ``{prompt_type : theme_name}``, where + ``prompt_type`` is a string with no special meaning (specifically it does + not refer to any Python function). Shell has ``continuation``, and + ``select`` prompts with rather self-explanatory names, IPython has ``in2``, + ``out`` and ``rewrite`` prompts (refer to IPython documentation for more + details) while ``in`` prompt is the default. + +``components`` + Determines which extension components should be enabled. This key is highly + extension-specific, here is the table of extensions and corresponding + components: + + +---------+----------+-----------------------------------------------------+ + |Extension|Component |Description | + +---------+----------+-----------------------------------------------------+ + |vim |statusline|Makes Vim use powerline statusline. | + | +----------+-----------------------------------------------------+ + | |tabline |Makes Vim use powerline tabline. | + +---------+----------+-----------------------------------------------------+ + |shell |prompt |Makes shell display powerline prompt. | + | +----------+-----------------------------------------------------+ + | |tmux |Makes shell report its current working directory | + | | |and screen width to tmux for tmux powerline | + | | |bindings. | + | | |  | + +---------+----------+-----------------------------------------------------+ + + All components are enabled by default. + +.. _config-colors: + +Color definitions +================= + +:Location: :file:`powerline/colors.json` + +.. _config-colors-colors: + +``colors`` + Color definitions, consisting of a dict where the key is the name of the + color, and the value is one of the following: + + * A cterm color index. + * A list with a cterm color index and a hex color string (e.g. ``[123, + "aabbcc"]``). This is useful for colorschemes that use colors that + aren't available in color terminals. + +``gradients`` + Gradient definitions, consisting of a dict where the key is the name of the + gradient, and the value is a list containing one or two items, second item + is optional: + + * A list of cterm color indicies. + * A list of hex color strings. + + It is expected that you define gradients from least alert color to most + alert or use non-alert colors. + +.. _config-colorschemes: + +Colorschemes +============ + +:Location: :file:`powerline/colorschemes/{name}.json`, + :file:`powerline/colorschemes/__main__.json`, + :file:`powerline/colorschemes/{extension}/{name}.json` + +Colorscheme files are processed in order given: definitions from each next file +override those from each previous file. It is required that either +:file:`powerline/colorschemes/{name}.json`, or +:file:`powerline/colorschemes/{extension}/{name}.json` exists. + +``name`` + Name of the colorscheme. + +.. _config-colorschemes-groups: + +``groups`` + Segment highlighting groups, consisting of a dict where the key is the + name of the highlighting group (usually the function name for function + segments), and the value is either + + #) a dict that defines the foreground color, background color and + attributes: + + ``fg`` + Foreground color. Must be defined in :ref:`colors + `. + + ``bg`` + Background color. Must be defined in :ref:`colors + `. + + ``attr`` + List of attributes. Valid values are one or more of ``bold``, + ``italic`` and ``underline``. Note that some attributes may be + unavailable in some applications or terminal emulators. If you do not + need any attributes leave this empty. + + #) a string (an alias): a name of existing group. This group’s definition + will be used when this color is requested. + +``mode_translations`` + Mode-specific highlighting for extensions that support it (e.g. the vim + extension). It's an easy way of changing a color in a specific mode. + Consists of a dict where the key is the mode and the value is a dict + with the following options: + + ``colors`` + A dict where the key is the color to be translated in this mode, and + the value is the new color. Both the key and the value must be defined + in :ref:`colors `. + + ``groups`` + Segment highlighting groups for this mode. Same syntax as the main + :ref:`groups ` option. + +.. _config-themes: + +Themes +====== + +:Location: :file:`powerline/themes/{top_theme}.json`, + :file:`powerline/themes/{extension}/__main__.json`, + :file:`powerline/themes/{extension}/{name}.json` + +Theme files are processed in order given: definitions from each next file +override those from each previous file. It is required that file +:file:`powerline/themes/{extension}/{name}.json` exists. + +`{top_theme}` component of the file name is obtained either from :ref:`top_theme +extension-specific key ` or from :ref:`default_top_theme +common configuration key `. Powerline ships +with the following top themes: + +.. _config-top_themes-list: + +========================== ==================================================== +Theme Description +========================== ==================================================== +powerline Default powerline theme with fancy powerline symbols +unicode Theme without any symbols from private use area +unicode_terminus Theme containing only symbols from terminus PCF font +unicode_terminus_condensed Like above, but occupies as less space as possible +ascii Theme without any unicode characters at all +========================== ==================================================== + +``name`` + Name of the theme. + +.. _config-themes-default_module: + +``default_module`` + Python module where segments will be looked by default. Defaults to + ``powerline.segments.{ext}``. + +``spaces`` + Defines number of spaces just before the divider (on the right side) or just + after it (on the left side). These spaces will not be added if divider is + not drawn. + +``use_non_breaking_spaces`` + Determines whether non-breaking spaces should be used in place of the + regular ones. This option is needed because regular spaces are not displayed + properly when using powerline with some font configuration. Defaults to + ``True``. + + .. note:: + Unlike all other options this one is only checked once at startup using + whatever theme is :ref:`the default `. If this option + is set in the local themes it will be ignored. This option may also be + ignored in some bindings. + + +``dividers`` + Defines the dividers used in all Powerline extensions. This option + should usually only be changed if you don't have a patched font, or if + you use a font patched with the legacy font patcher. + + The ``hard`` dividers are used to divide segments with different + background colors, while the ``soft`` dividers are used to divide + segments with the same background color. + +.. _config-themes-cursor_space: + +``cursor_space`` + Space reserved for user input in shell bindings. It is measured in per + cents. + +``cursor_columns`` + Space reserved for user input in shell bindings. Unlike :ref:`cursor_space + ` it is measured in absolute amout of columns. + +.. _config-themes-segment_data: + +``segment_data`` + A dict where keys are segment names or strings ``{module}.{function}``. Used + to specify default values for various keys: + :ref:`after `, + :ref:`before `, + :ref:`contents ` (only for string segments + if :ref:`name ` is defined), + :ref:`display `. + + Key :ref:`args ` (only for function and + segments_list segments) is handled specially: unlike other values it is + merged with all other values, except that a single ``{module}.{function}`` + key if found prevents merging all ``{function}`` values. + + When using :ref:`local themes ` values of these + keys are first searched in the segment description, then in ``segment_data`` + key of a local theme, then in ``segment_data`` key of a :ref:`default theme + `. For the :ref:`default theme ` itself + step 2 is obviously avoided. + + .. note:: Top-level themes are out of equation here: they are merged + before the above merging process happens. + +.. _config-themes-segments: + +``segments`` + A dict with a ``left`` and a ``right`` lists, consisting of segment + dictionaries. Shell themes may also contain ``above`` list of dictionaries. + Each item in ``above`` list may have ``left`` and ``right`` keys like this + dictionary, but no ``above`` key. + + .. _config-themes-above: + + ``above`` list is used for multiline shell configurations. + + ``left`` and ``right`` lists are used for segments that should be put on the + left or right side in the output. Actual mechanizm of putting segments on + the left or the right depends on used renderer, but most renderers require + one to specify segment with :ref:`width ` ``auto`` + on either side to make generated line fill all of the available width. + + Each segment dictionary has the following options: + + .. _config-themes-seg-type: + + ``type`` + The segment type. Can be one of ``function`` (default), ``string`` or + ``segments_list``: + + ``function`` + The segment contents is the return value of the function defined in + the :ref:`function option `. + + List of function segments is available in :ref:`Segment reference + ` section. + + ``string`` + A static string segment where the contents is defined in the + :ref:`contents option `, and the + highlighting group is defined in the :ref:`highlight_group + option `. + + ``segments_list`` + Sub-list of segments. This list only allows :ref:`function + `, :ref:`segments + ` and :ref:`args + ` options. + + List of lister segments is available in :ref:`Lister reference + ` section. + + .. _config-themes-seg-name: + + ``name`` + Segment name. If present allows referring to this segment in + :ref:`segment_data ` dictionary by this + name. If not ``string`` segments may not be referred there at all and + ``function`` and ``segments_list`` segments may be referred there using + either ``{module}.{function_name}`` or ``{function_name}``, whichever + will be found first. Function name is taken from :ref:`function key + `. + + .. note:: + If present prevents ``function`` key from acting as a segment name. + + .. _config-themes-seg-function: + + ``function`` + Function used to get segment contents, in format ``{module}.{function}`` + or ``{function}``. If ``{module}`` is omitted :ref:`default_module + option ` is used. + + .. _config-themes-seg-highlight_group: + + ``highlight_group`` + Highlighting group for this segment. Consists of a prioritized list of + highlighting groups, where the first highlighting group that is + available in the colorscheme is used. + + Ignored for segments that have ``function`` type. + + .. _config-themes-seg-before: + + ``before`` + A string which will be prepended to the segment contents. + + .. _config-themes-seg-after: + + ``after`` + A string which will be appended to the segment contents. + + .. _config-themes-seg-contents: + + ``contents`` + Segment contents, only required for ``string`` segments. + + .. _config-themes-seg-args: + + ``args`` + A dict of arguments to be passed to a ``function`` segment. + + .. _config-themes-seg-align: + + ``align`` + Aligns the segments contents to the left (``l``), center (``c``) or + right (``r``). Has no sense if ``width`` key was not specified or if + segment provides its own function for ``auto`` ``width`` handling and + does not care about this option. + + .. _config-themes-seg-width: + + ``width`` + Enforces a specific width for this segment. + + This segment will work as a spacer if the width is set to ``auto``. + Several spacers may be used, and the space will be distributed + equally among all the spacer segments. Spacers may have contents, + either returned by a function or a static string, and the contents + can be aligned with the ``align`` property. + + .. _config-themes-seg-priority: + + ``priority`` + Optional segment priority. Segments with priority ``None`` (the default + priority, represented by ``null`` in json) will always be included, + regardless of the width of the prompt/statusline. + + If the priority is any number, the segment may be removed if the + prompt/statusline width is too small for all the segments to be + rendered. A lower number means that the segment has a higher priority. + + Segments are removed according to their priority, with low priority + segments being removed first. + + .. _config-themes-seg-draw_divider: + + ``draw_hard_divider``, ``draw_soft_divider`` + Whether to draw a divider between this and the adjacent segment. The + adjacent segment is to the *right* for segments on the *left* side, and + vice versa. Hard dividers are used between segments with different + background colors, soft ones are used between segments with same + background. Both options default to ``True``. + + .. _config-themes-seg-draw_inner_divider: + + ``draw_inner_divider`` + Determines whether inner soft dividers are to be drawn for function + segments. Only applicable for functions returning multiple segments. + Defaults to ``False``. + + .. _config-themes-seg-exclude_modes: + + ``exclude_modes``, ``include_modes`` + A list of modes where this segment will be excluded: the segment is not + included or is included in all modes, *except* for the modes in one of + these lists respectively. If ``exclude_modes`` is not present then it + acts like an empty list (segment is not excluded from any modes). + Without ``include_modes`` it acts like a list with all possible modes + (segment is included in all modes). When there are both + ``exclude_modes`` overrides ``include_modes``. + + .. _config-themes-seg-exclude_function: + + ``exclude_function``, ``include_function`` + Function name in a form ``{name}`` or ``{module}.{name}`` (in the first + form ``{module}`` defaults to ``powerline.selectors.{ext}``). Determines + under which condition specific segment will be included or excluded. By + default segment is always included and never excluded. + ``exclude_function`` overrides ``include_function``. + + .. note:: + Options :ref:`exclude_/include_modes + ` complement + ``exclude_/include_functions``: segment will be included if it is + included by either ``include_mode`` or ``include_function`` and will + be excluded if it is excluded by either ``exclude_mode`` or + ``exclude_function``. + + .. _config-themes-seg-display: + + ``display`` + Boolean. If false disables displaying of the segment. + Defaults to ``True``. + + .. _config-themes-seg-segments: + + ``segments`` + A list of subsegments. diff --git a/docs/source/configuration/segments.rst b/docs/source/configuration/segments.rst new file mode 100644 index 00000000..7b6edaaf --- /dev/null +++ b/docs/source/configuration/segments.rst @@ -0,0 +1,28 @@ +.. _config-segments: + +***************** +Segment reference +***************** + +Segments +======== + +Segments are written in Python, and the default segments provided with +Powerline are located in :file:`powerline/segments/{extension}.py`. +User-defined segments can be defined in any module in ``sys.path`` or +:ref:`paths common configuration option `, import is +always absolute. + +Segments are regular Python functions, and they may accept arguments. All +arguments should have a default value which will be used for themes that +don't provide an ``args`` dict. + +More information is available in :ref:`Writing segments ` section. + +Available segments +================== + +.. toctree:: + :glob: + + segments/* diff --git a/docs/source/configuration/segments/common.rst b/docs/source/configuration/segments/common.rst new file mode 100644 index 00000000..49dc2519 --- /dev/null +++ b/docs/source/configuration/segments/common.rst @@ -0,0 +1,6 @@ +*************** +Common segments +*************** + +.. automodule:: powerline.segments.common + :members: diff --git a/docs/source/configuration/segments/shell.rst b/docs/source/configuration/segments/shell.rst new file mode 100644 index 00000000..fb3c8045 --- /dev/null +++ b/docs/source/configuration/segments/shell.rst @@ -0,0 +1,6 @@ +************** +Shell segments +************** + +.. automodule:: powerline.segments.shell + :members: diff --git a/docs/source/configuration/segments/tmux.rst b/docs/source/configuration/segments/tmux.rst new file mode 100644 index 00000000..1a4a78fc --- /dev/null +++ b/docs/source/configuration/segments/tmux.rst @@ -0,0 +1,6 @@ +************* +Tmux segments +************* + +.. automodule:: powerline.segments.tmux + :members: diff --git a/docs/source/configuration/segments/vim.rst b/docs/source/configuration/segments/vim.rst new file mode 100644 index 00000000..ace646c4 --- /dev/null +++ b/docs/source/configuration/segments/vim.rst @@ -0,0 +1,34 @@ +************ +Vim segments +************ + +.. automodule:: powerline.segments.vim + :members: + + +Plugin-specific segments +======================== + +Syntastic segments +------------------ + +.. automodule:: powerline.segments.vim.plugin.syntastic + :members: + +Ctrl-P segments +--------------- + +.. automodule:: powerline.segments.vim.plugin.ctrlp + :members: + +Tagbar segments +--------------- + +.. automodule:: powerline.segments.vim.plugin.tagbar + :members: + +NERDTree segments +----------------- + +.. automodule:: powerline.segments.vim.plugin.nerdtree + :members: diff --git a/docs/source/configuration/selectors.rst b/docs/source/configuration/selectors.rst new file mode 100644 index 00000000..f9367b27 --- /dev/null +++ b/docs/source/configuration/selectors.rst @@ -0,0 +1,17 @@ +.. _config-selectors: + +****************** +Selector functions +****************** + +Selector functions are functions that return ``True`` or ``False`` depending on +application state. They are used for :ref:`exclude_function and include_function +segment options `. + +Available selectors +=================== + +.. toctree:: + :glob: + + selectors/* diff --git a/docs/source/configuration/selectors/vim.rst b/docs/source/configuration/selectors/vim.rst new file mode 100644 index 00000000..5097320e --- /dev/null +++ b/docs/source/configuration/selectors/vim.rst @@ -0,0 +1,6 @@ +************* +Vim selectors +************* + +.. automodule:: powerline.selectors.vim + :members: diff --git a/docs/source/develop.rst b/docs/source/develop.rst new file mode 100644 index 00000000..bf454988 --- /dev/null +++ b/docs/source/develop.rst @@ -0,0 +1,13 @@ +*************** +Developer guide +*************** + +.. toctree:: + :maxdepth: 2 + :glob: + + develop/segments + develop/listers + develop/local-themes + develop/extensions + develop/tips-and-tricks diff --git a/docs/source/develop/extensions.rst b/docs/source/develop/extensions.rst new file mode 100644 index 00000000..9f4437d6 --- /dev/null +++ b/docs/source/develop/extensions.rst @@ -0,0 +1,47 @@ +******************************** +Creating new powerline extension +******************************** + +Powerline extension is a code that tells powerline how to highlight and display +segments in some set of applications. Specifically this means + +#. Creating a :py:class:`powerline.Powerline` subclass that knows how to obtain + :ref:`local configuration overrides `. It also + knows how to load local themes, but not when to apply them. + + Instance of this class is the only instance that interacts directly with + bindings code, so it has a proxy :py:meth:`powerline.Powerline.render` and + :py:meth:`powerline.Powerline.shutdown` methods and other methods which may + be useful for bindings. + + This subclass must be placed directly in :file:`powerline` directory (e.g. in + :file:`powerline/vim.py`) and named like ``VimPowerline`` (version of the + file name without directory and extension and first capital letter + + ``Powerline``). There is no technical reason for naming classes like this. +#. Creating a :py:class:`powerline.renderer.Renderer` subclass that knows how to + highlight a segment or reset highlighting to the default value (only makes + sense in prompts). It is also responsible for selecting local themes and + computing text width. + + This subclass must be placed directly in :file:`powerline/renderers` + directory (if you are creating powerline extension for a set of applications + use :file:`powerline/renderers/{ext}/*.py`) and named like ``ExtRenderer`` or + ``AppPromptRenderer``. For technical reasons the class itself must be + referenced in ``renderer`` module attribute thus allowing only one renderer + per one module. +#. Creating an extension bindings. These are to be placed in + :file:`powerline/bindings/{ext}` and may contain virtually anything which may + be required for powerline to work inside given applications, assuming it does + not fit in other places. + +Powerline class +=============== + +.. autoclass:: powerline.Powerline + :members: + +Renderer class +============== + +.. autoclass:: powerline.renderer.Renderer + :members: diff --git a/docs/source/develop/listers.rst b/docs/source/develop/listers.rst new file mode 100644 index 00000000..37917fe2 --- /dev/null +++ b/docs/source/develop/listers.rst @@ -0,0 +1,49 @@ +.. _dev-listers: + +*************** +Writing listers +*************** + +Listers allow you to show some segments multiple times: once per each entity +(buffer, tabpage, etc) lister knows. They are functions which receive the +following arguments: + +``pl`` + A :py:class:`powerline.PowerlineLogger` class instance. It must be used for + logging. + +``segment_info`` + Base segment info dictionary. Lister function or class must have + ``powerline_requires_segment_info`` to receive this argument. + + .. warning:: + Listers are close to useless if they do not have access to this + argument. + + Refer to :ref:`segment_info detailed description ` for + further details. + +``draw_inner_divider`` + If False (default) soft dividers between segments in the listed group will + not be drawn regardless of actual segment settings. If True they will be + drawn, again regardless of actual segment settings. Set it to ``None`` in + order to respect segment settings. + +And also any other argument(s) specified by user in :ref:`args key +` (no additional arguments by default). + +Listers must return a sequence of pairs. First item in the pair must contain +a ``segment_info`` dictionary specific to one of the listed entities. + +Second item must contain another dictionary: it will be used to modify the +resulting segment. In addition to :ref:`usual keys that describe segment +` the following keys may be present (it is advised that +*only* the following keys will be used): + +``priority_multiplier`` + Value (usually a ``float``) used to multiply segment priority. It is useful + for finer-grained controlling which segments disappear first: e.g. when + listing tab pages make first disappear directory names of the tabpages which + are most far away from current tabpage, then (when all directory names + disappeared) buffer names. Check out existing listers implementation in + :file:`powerline/listers/vim.py`. diff --git a/docs/source/develop/local-themes.rst b/docs/source/develop/local-themes.rst new file mode 100644 index 00000000..e1ca6483 --- /dev/null +++ b/docs/source/develop/local-themes.rst @@ -0,0 +1,59 @@ +************ +Local themes +************ + +From the user point of view local themes are the regular themes with a specific +scope where they are applied (i.e. specific vim window or specific kind of +prompt). Used themes are defined in :ref:`local_themes key +`. + +Vim local themes +================ + +Vim is the only available extension that has a wide variaty of options for local +themes. It is the only extension where local theme key refers to a function as +described in :ref:`local_themes value documentation `. + +This function always takes a single value named ``matcher_info`` which is the +same dictionary as :ref:`segment_info dictionary `. Unlike +segments it takes this single argument as a *positional* argument, not as +a keyword one. + +Matcher function should return a boolean value: ``True`` if theme applies for +the given ``matcher_info`` dictionary or ``False`` if it is not. When one of the +matcher functions returns ``True`` powerline takes the corresponding theme at +uses it for the given window. Matchers are not tested in any particular order. + +In addition to :ref:`local_themes configuration key ` +developer of some plugin which wishes to support powerline without including his +code in powerline tree may use +:py:meth:`powerline.vim.VimPowerline.add_local_theme` method. It accepts two +arguments: matcher name (same as in :ref:`local_themes +`) and dictionary with theme. This dictionary is merged +with :ref:`top theme ` and +:file:`powerline/themes/vim/__main__.json`. Note that if user already specified +your matcher in his configuration file ``KeyError`` is raised. + +Other local themes +================== + +Except for Vim only IPython and shells have local themes. Unlike Vim these +themes are names with no special meaning (they do not refer to or cause loading +of any Python functions): + ++---------+------------+-------------------------------------------------------+ +|Extension|Theme name |Description | ++---------+------------+-------------------------------------------------------+ +|Shell |continuation|Shown for unfinished command (unclosed quote, | +| | |unfinished cycle). | +| +------------+-------------------------------------------------------+ +| |select |Shown for ``select`` command available in some shells. | ++---------+------------+-------------------------------------------------------+ +|IPython |in2 |Continuation prompt: shown for unfinished (multiline) | +| | |expression, unfinished class or function definition. | +| +------------+-------------------------------------------------------+ +| |out |Displayed before the result. | +| +------------+-------------------------------------------------------+ +| |rewrite |Displayed before the actually executed code when | +| | |``autorewrite`` IPython feature is enabled.  | ++---------+------------+-------------------------------------------------------+ diff --git a/docs/source/develop/segments.rst b/docs/source/develop/segments.rst new file mode 100644 index 00000000..6454caa8 --- /dev/null +++ b/docs/source/develop/segments.rst @@ -0,0 +1,451 @@ +.. _dev-segments: + +**************** +Writing segments +**************** + +Each powerline segment is a callable object. It is supposed to be either +a Python function or :py:class:`powerline.segments.Segment` class. As a callable +object it should receive the following arguments: + +.. note:: All received arguments are keyword arguments. + +``pl`` + A :py:class:`powerline.PowerlineLogger` instance. It must be used every time + you need to log something. + +``segment_info`` + A dictionary. It is only received if callable has + ``powerline_requires_segment_info`` attribute. + + Refer to :ref:`segment_info detailed description ` for + further details. + +``create_watcher`` + Function that will create filesystem watcher once called. Which watcher will + be created exactly is controlled by :ref:`watcher configuration option + `. + +And also any other argument(s) specified by user in :ref:`args key +` (no additional arguments by default). + +Object representing segment may have the following attributes used by +powerline: + +``powerline_requires_segment_info`` + This attribute controls whether segment will receive ``segment_info`` + argument: if it is present argument will be received. + +``powerline_requires_filesystem_watcher`` + This attribute controls whether segment will receive ``create_watcher`` + argument: if it is present argument will be received. + +``powerline_segment_datas`` + This attribute must be a dictionary containing ``top_theme: segment_data`` + mapping where ``top_theme`` is any theme name (it is expected that all of + the names from :ref:`top-level themes list ` are + present) and ``segment_data`` is a dictionary like the one that is contained + inside :ref:`segment_data dictionary in configuration + `. This attribute should be used to specify + default theme-specific values for *third-party* segments: powerline + theme-specific values go directly to :ref:`top-level themes + `. + +.. _dev-segments-startup: + +``startup`` + This attribute must be a callable which accepts the following keyword + arguments: + + * ``pl``: :py:class:`powerline.PowerlineLogger` instance which is to be used + for logging. + * ``shutdown_event``: :py:class:`Event` object which will be set when + powerline will be shut down. + * Any arguments found in user configuration for the given segment (i.e. + :ref:`args key `). + + This function is called at powerline startup when using long-running + processes (e.g. powerline in vim, in zsh with libzpython, in ipython or in + powerline daemon) and not called when ``powerline-render`` executable is + used (more specific: when :py:class:`powerline.Powerline` constructor + received true ``run_once`` argument). + +.. _dev-segments-shutdown: + +``shutdown`` + This attribute must be a callable that accepts no arguments and shuts down + threads and frees any other resources allocated in ``startup`` method of the + segment in question. + + This function is not called when ``startup`` method is not called. + +.. _dev-segments-expand: + +``expand`` + This attribute must be a callable that accepts the following keyword + arguments: + + * ``pl``: :py:class:`powerline.PowerlineLogger` instance which is to be used + for logging. + * ``amount``: integer number representing amount of display cells result + must occupy. + + .. warning:: + “Amount of display cells” is *not* number of Unicode codepoints, string + length, or byte count. It is suggested that your function should look + something like ``return (' ' * amount) + segment['contents']`` where + ``' '`` may be replaced with anything that is known to occupy exactly + one display cell. + * ``segment``: :ref:`segment dictionary `. + * Any arguments found in user configuration for the given segment (i.e. + :ref:`args key `). + + It must return new value of :ref:`contents ` key. + +.. _dev-segments-truncate: + +``truncate`` + Like :ref:`expand function `, but for truncating + segments. Here ``amount`` means the number of display cells which must be + freed. + + This function is called for all segments before powerline starts purging + them to free space. + +This callable object should may return either a string (``unicode`` in Python2 +or ``str`` in Python3, *not* ``str`` in Python2 or ``bytes`` in Python3) object +or a list of dictionaries. String object is a short form of the following return +value: + +.. code-block:: python + + [{ + 'contents': original_return, + 'highlight_group': [segment_name], + }] + +.. _dev-segments-return: + +Returned list is a list of segments treated independently, except for +:ref:`draw_inner_divider key `. + +All keys in segments returned by the function override those obtained from +:ref:`configuration ` and have the same meaning. + +Detailed description of used dictionary keys: + +``contents`` + Text displayed by segment. Should be a ``unicode`` (Python2) or ``str`` + (Python3) instance. + +.. _dev-segments-draw_inner_divider: + +``draw_hard_divider``, ``draw_soft_divider``, ``draw_inner_divider`` + Determines whether given divider should be drawn. All have the same meaning + as :ref:`the similar keys in configuration ` + (:ref:`draw_inner_divider `). + +.. _dev-segments-highlight_group: + +``highlight_group`` + Determines segment highlighting. Refer to :ref:`themes documentation + ` for more details. + + Defaults to the name of the segment. + + .. note:: + If you want to include your segment in powerline you must specify all + highlighting groups used in the segment documentation in the form:: + + Highlight groups used: ``g1``[ or ``g2``]*[, ``g3`` (gradient)[ or ``g4``]*]*. + + I.e. use:: + + Highlight groups used: ``foo_gradient`` (gradient) or ``foo``, ``bar``. + + to specify that your segment uses *either* ``foo_gradient`` group or + ``foo`` group *and* ``bar`` group meaning that ``powerline-lint`` will + check that at least one of the first two groups is defined (and if + ``foo_gradient`` is defined it must use at least one gradient color) and + third group is defined as well. + + You must specify all groups on one line. + +``divider_highlight_group`` + Determines segment divider highlight group. Only applicable for soft + dividers: colors for hard dividers are determined by colors of adjacent + segments. + + .. note:: + If you want to include your segment in powerline you must specify used + groups in the segment documentation in the form:: + + Divider highlight group used: ``group``. + + This text must not wrap and you are supposed to end all divider + highlight group names with ``:divider``: e.g. ``cwd:divider``. + +``gradient_level`` + First and the only key that may not be specified in user configuration. It + determines which color should be used for this segment when one of the + highlighting groups specified by :ref:`highlight_group + ` was defined to use the color gradient. + + This key may have any value from 0 to 100 inclusive, value is supposed to be + an ``int`` or ``float`` instance. + + No error occurs if segment has this key, but no used highlight groups use + gradient color. + +``_*`` + Keys starting with underscore are reserved for powerline and must not be + returned. + +``__*`` + Keys starting with two underscores are reserved for the segment functions, + specifically for :ref:`expand function `. + +.. _dev-segments-segment: + +Segment dictionary +================== + +Segment dictionary contains the following keys: + +* All keys returned by segment function (if it was used). + +* All of the following keys: + + ``name`` + Segment name: value of the :ref:`name key ` or + function name (last component of the :ref:`function key + `). May be ``None``. + + ``type`` + :ref:`Segment type `. Always represents actual type + and is never ``None``. + + ``highlight_group``, ``divider_highlight_group`` + Used highlight groups. May be ``None``. + + ``highlight_group_prefix`` + If this key is present then given prefix will be prepended to each highlight + group (both regular and divider) used by this segment in a form + ``{prefix}:{group}`` (note the colon). This key is mostly useful for + :ref:`segment listers `. + + .. _dev-segments-seg-around: + + ``before``, ``after`` + Value of :ref:`before ` or :ref:`after + ` configuration options. May be ``None`` as well as + an empty string. + + ``contents_func`` + Function used to get segment contents. May be ``None``. + + .. _dev-segments-seg-contents: + + ``contents`` + Actual segment contents, excluding dividers and :ref:`before/after + `. May be ``None``. + + ``priority`` + :ref:`Segment priority `. May be ``None`` for no + priority (such segments are always shown). + + ``draw_soft_divider``, ``draw_hard_divider``, ``draw_inner_divider`` + :ref:`Divider control flags `. + + ``side`` + Segment side: ``right`` or ``left``. + + ``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`` + or ``False`` to indicate whether particular segment should be processed. + + This key is constructed based on :ref:`exclude_/include_modes keys + ` and :ref:`exclude_/include_function keys + `. + + ``width``, ``align`` + :ref:`Width and align options `. May be ``None``. + + ``expand``, ``truncate`` + Partially applied :ref:`expand ` or :ref:`truncate + ` function. Accepts ``pl``, ``amount`` and + ``segment`` positional parameters, keyword parameters from :ref:`args + ` key were applied. + + ``startup`` + Partially applied :ref:`startup function `. Accepts + ``pl`` and ``shutdown_event`` positional parameters, keyword parameters from + :ref:`args ` key were applied. + + ``shutdown`` + :ref:`Shutdown function `. Accepts no argument. + +Segments layout +=============== + +Powerline segments are all located in one of the ``powerline.segments`` +submodules. For extension-specific segments ``powerline.segments.{ext}`` module +should be used (e.g. ``powerline.segments.shell``), for extension-agnostic there +is ``powerline.segments.common``. + +Plugin-specific segments (currently only those that are specific to vim plugins) +should live in ``powerline.segments.{ext}.plugin.{plugin_name}``: e.g. +``powerline.segments.vim.plugin.gundo``. + +.. _dev-segments-info: + +Segment information used in various extensions +============================================== + +Each ``segment_info`` value should be a dictionary with at least the following +keys: + +``environ`` + Current environment, may be an alias to ``os.environ``. Is guaranteed to + have ``__getitem__`` and ``get`` methods and nothing more. + + .. warning:: + You must not ever use ``os.environ``. If your segment is run in daemon + you will get daemon’s environment which is not correct. If your segment + is run in Vim or in zsh with libzpython you will get Vim or zsh + environment at python startup. + +``getcwd`` + Function that returns current working directory being called with no + arguments. You must not use ``os.getcwd`` for the same reasons you must not + use ``os.environ``, except that current working directory is valid in Vim + and zsh (but not in daemon). + +``home`` + Current home directory. May be false. + +.. _dev-segment_info-vim: + +Vim +--- + +Vim ``segment_info`` argument is a dictionary with the following keys: + +``window`` + ``vim.Window`` object. You may obtain one using ``vim.current.window`` or + ``vim.windows[number - 1]``. May be a false object, in which case you should + not use any of this objects’ properties. + +``winnr`` + Window number. Same as ``segment_info['window'].number`` *assuming* Vim is + new enough for ``vim.Window`` object to have ``number`` attribute. + +``window_id`` + Internal powerline window id, unique for each newly created window. You + should assume that this ID is hashable and supports equality comparison, but + you must not use any other assumptions about it. Currently uses integer + numbers incremented each time window is created. + +``buffer`` + ``vim.Buffer`` object. You may obtain one using ``vim.current.buffer``, + ``segment_info['window'].buffer`` or ``vim.buffers[some_number]``. Note that + in the latter case depending on vim version ``some_number`` may be ``bufnr`` + or the internal Vim buffer index which is *not* buffer number. For this + reason to get ``vim.Buffer`` object other then stored in ``segment_info`` + dictionary you must iterate over ``vim.buffers`` and check their ``number`` + attributes. + +``bufnr`` + Buffer number. + +``tabpage`` + ``vim.Tabpage`` object. You may obtain one using ``vim.current.tabpage`` or + ``vim.tabpages[number - 1]``. May be a false object, in which case you + should not use any of this objects’ properties. + +``tabnr`` + Tabpage number. + +``mode`` + Current mode. + +.. note:: + Your segment generally should not assume that it is run for the current + window, current buffer or current tabpage. “Current window” and “current + buffer” restrictions may be ignored if you use ``window_cached`` decorator, + “current tabpage” restriction may be safely ignored if you do not plan to + ever see your segment in the tabline. + +.. warning:: + Powerline is being tested with vim-7.2 and will be tested with it until + travis changes used vim version. This means that you may not use most of the + functionality like ``vim.Window.number``, ``vim.*.vars``, ``vim.*.options`` + or even ``dir(vim object)`` if you want your segment to be included in + powerline. + +Shell +----- + +``args`` + Parsed shell arguments: a ``argparse.Namespace`` object. Check out + ``powerline-render --help`` for the list of all available arguments. + Currently it is expected to contain at least the following attributes: + + ``last_exit_code`` + Exit code returned by last shell command. + + ``last_pipe_status`` + List of exit codes returned by last programs in the pipe or some false + object. Only available in ``zsh``. + + ``jobnum`` + Number of background jobs. + + ``renderer_arg`` + Dictionary containing some keys that are additional arguments used by + shell bindings. *You must not use this attribute directly*: all + arguments from this dictionary are merged with ``segment_info`` + dictionary. Known to have at least the following keys: + + ``client_id`` + Identifier unique to one shell instance. Is used to record instance + state by powerline daemon. + + It is not guaranteed that existing client ID will not be retaken + when old shell with this ID quit: usually process PID is used as + a client ID. + + It is also not guaranteed that client ID will be process PID, number + or something else at all. It is guaranteed though that client ID + will be some hashable object which supports equality comparison. + + ``local_theme`` + Local theme that will be used by shell. One should not rely on the + existence of this key. + + Other keys, if any, are specific to segments. + +Ipython +------- + +``ipython`` + Some object which has ``prompt_count`` attribute. Currently it is guaranteed + to have only this attribute. + + Attribute ``prompt_count`` contains the so-called “history count” + (equivalent to ``\N`` in ``in_template``). + +Segment class +============= + +.. autoclass:: powerline.segments.Segment + :members: + +PowerlineLogger class +===================== + +.. autoclass:: powerline.PowerlineLogger + :members: + :undoc-members: diff --git a/docs/source/develop/tips-and-tricks.rst b/docs/source/develop/tips-and-tricks.rst new file mode 100644 index 00000000..c850659c --- /dev/null +++ b/docs/source/develop/tips-and-tricks.rst @@ -0,0 +1,21 @@ +**************************************** +Tips and tricks for powerline developers +**************************************** + +Profiling powerline in Vim +========================== + +Given that current directory is the root of the powerline repository the +following command may be used: + +.. code-block:: sh + + vim --cmd 'let g:powerline_pyeval="powerline#debug#profile_pyeval"' \ + --cmd 'set rtp=powerline/bindings/vim' \ + -c 'runtime! plugin/powerline.vim' \ + {other arguments if needed} + +After some time run ``:WriteProfiling {filename}`` Vim command. Currently this +only works with recent Vim and python-2*. It should be easy to modify +:file:`powerline/bindings/vim/autoload/powerline/debug.vim` to suit other +needs. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..e97a17ad --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,23 @@ +********* +Powerline +********* + +.. toctree:: + :maxdepth: 3 + :glob: + + overview + installation + usage + configuration + develop + troubleshooting + tips-and-tricks + license-and-credits + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 00000000..9fa1b87f --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,107 @@ +************ +Installation +************ + +Generic requirements +==================== + +* Python 2.6 or later, 3.2 or later, PyPy 2.0 or later. It is the only + non-optional requirement. +* C compiler. Required to build powerline client on linux. If it is not present + then powerline will fall back to shell script or python client. +* ``socat`` program. Required for shell variant of client which runs a bit + faster than python version of the client, but still slower than C version. +* ``psutil`` python package. Required for some segments like cpu_percent. Some + segments have linux-only fallbacks for ``psutil`` functionality. +* ``mercurial`` python package (note: *not* standalone executable). Required to + work with mercurial repositories. +* ``pygit2`` python package or ``git`` executable. Required to work with ``git`` + repositories. +* ``bzr`` python package (note: *not* standalone executable). Required to work + 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. + +.. note:: + Until mercurial and bazaar support Python-3 or PyPy powerline will not + support repository information when running in these interpreters. + +Pip installation +================ + +This project is currently unavailable from PyPI due to a naming conflict with an +unrelated project, thus you will have to use the following command to install +powerline with ``pip``: + +.. code-block:: sh + + pip install --user git+git://github.com/Lokaltog/powerline + +. You may also choose to clone powerline repository somewhere and use + +.. code-block:: sh + + pip install -e --user {path_to_powerline} + +, but note that in this case ``pip`` will not install ``powerline`` executable +and you will have to do something like + +.. code-block:: sh + + ln -s {path_to_powerline}/scripts/powerline ~/.local/bin + +(:file:`~/.local/bin` should be replaced with some path present in ``$PATH``). + +.. note:: + If your ISP blocks git protocol for some reason github also provides ``ssh`` + (``git+ssh://git@github.com/Lokaltog/powerline``) and ``https`` + (``git+https://github.com/Lokaltog/powerline``) protocols. ``git`` protocol + should be the fastest, but least secure one though. + +To install release version uploaded to PyPI use + +.. code-block:: sh + + pip install powerline-status + +Fonts installation +================== + +Powerline uses several special glyphs to get the arrow effect and some custom +symbols for developers. This requires that you either have a symbol font or +a patched font on your system. Your terminal emulator must also support either +patched fonts or fontconfig for Powerline to work properly. + +You can also enable :ref:`24-bit color support ` +if your terminal emulator supports it (see :ref:`the terminal emulator support +matrix `). + +There are basically two ways to get powerline glyphs displayed: use +:file:`PowerlineSymbols.otf` font as a fallback for one of the existing fonts or +install a patched font. + +.. _installation-patched-fonts: + +Patched fonts +------------- + +This method is the fallback method and works for every terminal, with the +exception of :ref:`rxvt-unicode `. + +Download the font of your choice from `powerline-fonts`_. If you can't find +your preferred font in the `powerline-fonts`_ repo, you'll have to patch your +own font instead. + +.. _powerline-fonts: https://github.com/Lokaltog/powerline-fonts + +After downloading this font refer to platform-specific instructions. + +Installation on various platforms +================================= + +.. toctree:: + + Linux + OS X diff --git a/docs/source/installation/linux.rst b/docs/source/installation/linux.rst new file mode 100644 index 00000000..5f8cd6d7 --- /dev/null +++ b/docs/source/installation/linux.rst @@ -0,0 +1,95 @@ +********************* +Installation on Linux +********************* + +The following distribution-specific packages are officially supported, and they +provide an easy way of installing and upgrading Powerline. The packages will +automatically do most of the configuration for you. + +* `Arch Linux (AUR), Python 2 version `_ +* `Arch Linux (AUR), Python 3 version `_ +* Gentoo Live ebuild in `raiagent `_ overlay + +If you're running a distribution without an official package you'll have to +follow the installation guide below: + +1. Install Python 3.2+ or Python 2.6+ with ``pip``. This step is + distribution-specific, so no commands provided. +2. Install Powerline using the following command:: + + pip install --user git+git://github.com/Lokaltog/powerline + +.. note:: You need to use the GitHub URI when installing Powerline! This + project is currently unavailable on the PyPI due to a naming conflict + with an unrelated project. + +.. note:: If you are powerline developer you should be aware that ``pip install + --editable`` does not currently fully work. If you + install powerline this way you will be missing ``powerline`` executable and + need to symlink it. It will be located in ``scripts/powerline``. + +Fonts installation +================== + +Fontconfig +---------- + +This method only works on Linux. It's the recommended method if your +terminal emulator supports it as you don't have to patch any fonts, and it +generally works well with any coding font. + +#. Download the latest version of the symbol font and fontconfig file:: + + wget https://github.com/Lokaltog/powerline/raw/develop/font/PowerlineSymbols.otf + wget https://github.com/Lokaltog/powerline/raw/develop/font/10-powerline-symbols.conf + +#. Move the symbol font to a valid X font path. Valid font paths can be + listed with ``xset q``:: + + mv PowerlineSymbols.otf ~/.fonts/ + +#. Update font cache for the path you moved the font to (you may need to be + root to update the cache for system-wide paths):: + + fc-cache -vf ~/.fonts/ + +#. Install the fontconfig file. For newer versions of fontconfig the config + path is ``~/.config/fontconfig/conf.d/``, for older versions it's + ``~/.fonts.conf.d/``:: + + mv 10-powerline-symbols.conf ~/.config/fontconfig/conf.d/ + +If you can't see the custom symbols, please close all instances of your +terminal emulator. You may need to restart X for the changes to take +effect. + +If you *still* can't see the custom symbols, double-check that you have +installed the font to a valid X font path, and that you have installed the +fontconfig file to a valid fontconfig path. Alternatively try to install +a :ref:`patched font `. + +Patched font installation +------------------------- + +After downloading font you should do the following: + +#. Move the patched font to a valid X font path. Valid font paths can be + listed with ``xset q``:: + + mv 'MyFont for Powerline.otf' ~/.fonts/ + +#. Update font cache for the path you moved the font to (you may need to be + root to update the cache for system-wide paths):: + + fc-cache -vf ~/.fonts/ + +After installing the patched font you need to update Gvim or your terminal +emulator to use the patched font. The correct font usually ends with *for +Powerline*. + +If you can't see the custom symbols, please close all instances of your +terminal emulator. You may need to restart X for the changes to take +effect. + +If you *still* can't see the custom symbols, double-check that you have +installed the font to a valid X font path. diff --git a/docs/source/installation/osx.rst b/docs/source/installation/osx.rst new file mode 100644 index 00000000..fc2f6f8f --- /dev/null +++ b/docs/source/installation/osx.rst @@ -0,0 +1,60 @@ +******************** +Installation on OS X +******************** + +Python package +============== + +1. Install a proper Python version (see `issue #39 + `_ for a discussion + regarding the required Python version on OS X):: + + sudo port select python python27-apple + + . You may use homebrew for this:: + + brew install python + + . + + .. note:: + In case you want or have to use ``powerline.sh`` socat-based client you + should also install GNU env named ``genv``. This may be achieved by + running ``brew install coreutils``. + +2. Install Powerline using the following command:: + + pip install --user git+git://github.com/Lokaltog/powerline + + .. warning:: + When using ``brew install`` to install Python one must not supply + ``--user`` flag to ``pip``. + + .. note:: + You need to use the GitHub URI when installing Powerline! This project is + currently unavailable on the PyPI due to a naming conflict with an + unrelated project. + + .. note:: + If you are powerline developer you should be aware that ``pip install + --editable`` does not currently fully work. If you install powerline this + way you will be missing ``powerline`` executable and need to symlink it. It + will be located in ``scripts/powerline``. + +Vim installation +================ + +Any terminal vim version with Python 3.2+ or Python 2.6+ support should work, +but if you're using MacVim you need to install it using the following command:: + + brew install macvim --env-std --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. + +After installing the patched font you need to update MacVim or your terminal +emulator to use the patched font. The correct font usually ends with *for +Powerline*. diff --git a/docs/source/license-and-credits.rst b/docs/source/license-and-credits.rst new file mode 100644 index 00000000..bdd7ede8 --- /dev/null +++ b/docs/source/license-and-credits.rst @@ -0,0 +1,23 @@ +******************* +License and credits +******************* + +Powerline is licensed under the `MIT license +`_. + +Authors +------- + +* `Kim Silkebækken `_ +* `Nikolay Pavlov `_ +* `Kovid Goyal `_ + +Contributors +------------ + +* `List of contributors + `_ +* The glyphs in the font patcher are created by Fabrizio Schiavi, creator of + the excellent coding font `Pragmata Pro`_. + +.. _`Pragmata Pro`: http://www.fsd.it/fonts/pragmatapro.htm diff --git a/docs/source/overview.rst b/docs/source/overview.rst new file mode 100644 index 00000000..b599b349 --- /dev/null +++ b/docs/source/overview.rst @@ -0,0 +1,67 @@ +******** +Overview +******** + +**Powerline is a statusline plugin for vim, and provides statuslines and +prompts for several other applications, including zsh, bash, tmux, IPython, +Awesome and Qtile.** + +Features +-------- + +* **Extensible and feature rich, written in Python.** Powerline was + completely rewritten in Python to get rid of as much vimscript as + possible. This has allowed much better extensibility, leaner and better + config files, and a structured, object-oriented codebase with no mandatory + third-party dependencies other than a Python interpreter. +* **Stable and testable code base.** Using Python has allowed unit testing + of all the project code. The code is tested to work in Python 2.6+ and + Python 3. +* **Support for prompts and statuslines in many applications.** Originally + created exclusively for vim statuslines, the project has evolved to + provide statuslines in tmux and several WMs, and prompts for shells like + bash/zsh and other applications. It's simple to write renderers for any + other applications that Powerline doesn't yet support. +* **Configuration and colorschemes written in JSON.** JSON is + a standardized, simple and easy to use file format that allows for easy + user configuration across all of Powerline's supported applications. +* **Fast and lightweight, with daemon support for even better performance.** + Although the code base spans a couple of thousand lines of code with no + goal of "less than X lines of code", the main focus is on good performance + and as little code as possible while still providing a rich set of + features. The new daemon also ensures that only one Python instance is + launched for prompts and statuslines, which provides excellent + performance. + +*But I hate Python / I don't need shell prompts / this is just too much +hassle for me / what happened to the original vim-powerline project / …* + +You should check out some of the Powerline derivatives. The most lightweight +and feature-rich alternative is currently Bailey Ling's `vim-airline +`_ project. + +Screenshots +----------- + +Vim statusline +^^^^^^^^^^^^^^ + +**Mode-dependent highlighting** + +* .. image:: _static/img/pl-mode-normal.png + :alt: Normal mode +* .. image:: _static/img/pl-mode-insert.png + :alt: Insert mode +* .. image:: _static/img/pl-mode-visual.png + :alt: Visual mode +* .. image:: _static/img/pl-mode-replace.png + :alt: Replace mode + +**Automatic truncation of segments in small windows** + +* .. image:: _static/img/pl-truncate1.png + :alt: Truncation illustration +* .. image:: _static/img/pl-truncate2.png + :alt: Truncation illustration +* .. image:: _static/img/pl-truncate3.png + :alt: Truncation illustration diff --git a/docs/source/powerline_autodoc.py b/docs/source/powerline_autodoc.py new file mode 100644 index 00000000..971717df --- /dev/null +++ b/docs/source/powerline_autodoc.py @@ -0,0 +1,34 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from inspect import formatargspec + +from sphinx.ext import autodoc + +from powerline.lint.inspect import getconfigargspec +from powerline.segments import Segment +from powerline.lib.unicode import unicode + + +def formatvalue(val): + if type(val) is str: + return '="' + unicode(val, 'utf-8').replace('"', '\\"').replace('\\', '\\\\') + '"' + else: + return '=' + repr(val) + + +class ThreadedDocumenter(autodoc.FunctionDocumenter): + '''Specialized documenter subclass for ThreadedSegment subclasses.''' + @classmethod + def can_document_member(cls, member, membername, isattr, parent): + return (isinstance(member, Segment) or + super(ThreadedDocumenter, cls).can_document_member(member, membername, isattr, parent)) + + def format_args(self): + argspec = getconfigargspec(self.object) + return formatargspec(*argspec, formatvalue=formatvalue).replace('\\', '\\\\') + + +def setup(app): + autodoc.setup(app) + app.add_autodocumenter(ThreadedDocumenter) diff --git a/docs/source/tips-and-tricks.rst b/docs/source/tips-and-tricks.rst new file mode 100644 index 00000000..4be6c764 --- /dev/null +++ b/docs/source/tips-and-tricks.rst @@ -0,0 +1,94 @@ +*************** +Tips and tricks +*************** + +Vim +=== + +Useful settings +--------------- + +You may find the following vim settings useful when using the Powerline +statusline: + +.. code-block:: vim + + set laststatus=2 " Always display the statusline in all windows + set showtabline=2 " Always display the tabline, even if there is only one tab + set noshowmode " Hide the default mode text (e.g. -- INSERT -- below the statusline) + +.. _tips-and-tricks-urxvt: + +Rxvt-unicode +============ + +Terminus font and urxvt +----------------------- + +The Terminus fonts does not have the powerline glyphs and unless someone submits +a patch to the font author, it is unlikely to happen. However, Andre Klärner +came up with this work around: In your ``~/.Xdefault`` file add the following:: + + urxvt*font: xft:Terminus:pixelsize=12,xft:Inconsolata\ for\ Powerline:pixelsize=12 + +This will allow urxvt to fallback onto the Inconsolata fonts in case it does not +find the right glyphs within the terminus font. + +Source Code Pro font and urxvt +------------------------------ + +Much like the terminus font that was mentioned above, a similar fix can be +applied to the Source Code Pro fonts. + +In the ``~/.Xdefaults`` add the following:: + + URxvt*font: xft:Source\ Code\ Pro\ Medium:pixelsize=13:antialias=true:hinting=true,xft:Source\ Code\ Pro\ Medium:pixelsize=13:antialias=true:hinting=true + +I noticed that Source Code Pro has the glyphs there already, but the pixel size +of the fonts play a role in whether or not the > or the < separators showing up +or not. Using font size 12, glyphs on the right hand side of the powerline are +present, but the ones on the left don't. Pixel size 14, brings the reverse +problem. Font size 13 seems to work just fine. + +Reloading powerline after update +================================ + +Once you have updated powerline you generally have the following options: + +#. Restart the application you are using it in. This is the safest one. Will not + work if the application uses ``powerline-daemon``. +#. For shell and tmux bindings (except for zsh with libzpython): do not do + anything if you do not use ``powerline-daemon``, run ``powerline-daemon + --replace`` if you do. +#. Use powerline reloading feature. + + .. warning:: + This feature is an unsafe one. It is not guaranteed to work always, it may + render your Python constantly error out in place of displaying powerline + and sometimes may render your application useless, forcing you to + restart. + + *Do not report any bugs occurred when using this feature unless you know + both what caused it and how this can be fixed.* + + * When using zsh with libzpython use + + .. code-block:: bash + + powerline-reload + + .. note:: This shell function is only defined when using libzpython. + + * When using IPython use + + :: + + %powerline reload + + * When using Vim use + + .. code-block:: Vim + + py powerline.reload() + " or (depending on Python version you are using) + py3 powerline.reload() diff --git a/docs/source/troubleshooting.rst b/docs/source/troubleshooting.rst new file mode 100644 index 00000000..120d7107 --- /dev/null +++ b/docs/source/troubleshooting.rst @@ -0,0 +1,244 @@ +*************** +Troubleshooting +*************** + +System-specific issues +====================== + +.. toctree:: + + Linux + OS X + +Common issues +============= + +I'm using tmux and Powerline looks like crap, what's wrong? +----------------------------------------------------------- + +* You need to tell tmux that it has 256-color capabilities. Add this to your + :file:`.tmux.conf` to solve this issue:: + + set -g default-terminal "screen-256color" +* If you're using iTerm2, make sure that you have enabled the setting + :guilabel:`Set locale variables automatically` in :menuselection:`Profiles --> + Terminal --> Environment`. +* Make sure tmux knows that terminal it is running in support 256 colors. You + may tell it tmux by using ``-2`` option when launching it. + +I’m using tmux/screen and Powerline is colorless +------------------------------------------------ + +* If the above advices do not help, then you need to disable + :ref:`term_truecolor `. +* Alternative: set :ref:`additional_escapes ` + to ``"tmux"`` or ``"screen"``. Note that it is known to work perfectly in + screen, but in tmux it may produce ugly spaces. + + +After an update something stopped working +----------------------------------------- + +Assuming powerline was working before update and stopped only after there are +two possible explanations: + +* You have more then one powerline installation (e.g. ``pip`` and ``Vundle`` + installations) and you have updated only one. +* Update brought some bug to powerline. + +In the second case you, of course, should report the bug to `powerline bug +tracker `_. In the first you should make +sure you either have only one powerline installation or you update all of them +simultaneously (beware that in the second case you are not supported). To +diagnose this problem you may do the following: + +#) If this problem is observed within the shell make sure that + + .. code-block:: sh + + python -c 'import powerline; print (powerline.__file__)' + + which should report something like + :file:`/usr/lib64/python2.7/site-packages/powerline/__init__.pyc` (if + powerline is installed system-wide) or + :file:`/home/USER/.../powerline/__init__.pyc` (if powerline was cloned + somewhere, e.g. in :file:`/home/USER/.vim/bundle/powerline`) reports the same + location you use to source in your shell configuration: in first case it + should be some location in :file:`/usr` (e.g. + :file:`/usr/share/zsh/site-contrib/powerline.zsh`), in the second it should + be something like + :file:`/home/USER/.../powerline/bindings/zsh/powerline.zsh`. If this is true + it may be a powerline bug, but if locations do not match you should not + report the bug until you observe it on configuration where locations do + match. +#) If this problem is observed specifically within bash make sure that you clean + ``$POWERLINE_COMMAND`` and ``$PROMPT_COMMAND`` environment variables on + startup or, at least, that it was cleaned after update. While different + ``$POWERLINE_COMMAND`` variable should not cause any troubles most of time + (and when it will cause troubles are rather trivial) spoiled + ``$PROMPT_COMMAND`` may lead to strange error messages or absense of exit + code reporting. + + These are the sources which may keep outdated environment variables: + + * Any command launched from any application inherits its environment unless + callee explicitly requests to use specific environment. So if you did + ``exec bash`` after update it is rather unlikely to fix the problem. + * More interesting: `tmux` is a client-server application, it keeps one + server instance per one user. You probably already knew that, but there is + an interesting consequence: once `tmux` server was started it inherits its + environment from the callee and keeps it *forever* (i.e. until server is + killed). This environment is then inherited by applications you start with + ``tmux new-session``. Easiest solution is to kill tmux with ``tmux + kill-server``, but you may also use ``tmux set-environment -u`` to unset + offending variables. + * Also check `When using z powerline shows wrong number of jobs`_: though + this problem should not be seen after update only, it contains another + example of ``$PROMPT_COMMAND`` spoiling results. + +#) If this problem is observed within the vim instance you should check out the + output of the following Ex mode commands + + .. code-block:: vim + + python import powerline as pl ; print (pl.__file__) + python3 import powerline as pl ; print (pl.__file__) + + One (but not both) of them will most likely error out, this is OK. The same + rules apply as in the 1), but in place of sourcing you should seek for the + place where you modify `runtimepath` vim option. If you install powerline + using `VAM `_ then no + explicit modifications of runtimpath were performed in your vimrc + (runtimepath is modified by VAM in this case), but powerline will be placed + in :file:`{plugin_root_dir}/powerline` where `{plugin_root_dir}` is stored in + VAM settings dictionary: do `echo g:vim_addon_manager.plugin_root_dir`. + +There is a hint if you want to place powerline repository somewhere, but still +make powerline package importable anywhere: use + + .. code-block:: sh + + pip install --user --editable path/to/powerline + +Shell issues +============ + +I am suffering bad lags before displaying shell prompt +------------------------------------------------------ + +To get rid of these lags there currently are two options: + +* Run ``powerline-daemon``. Powerline does not automatically start it for you. +* Compile and install ``libzpython`` module that lives in + https://bitbucket.org/ZyX_I/zpython. This variant is zsh-specific. + +Prompt is spoiled after completing files in ksh +----------------------------------------------- + +This is exactly why powerline has official mksh support, but not official ksh +support. If you know the solution feel free to share it in `powerline bug +tracker`_. + +When using z powerline shows wrong number of jobs +------------------------------------------------- + +This happens because `z `_ is launching some jobs in +the background from ``$POWERLINE_COMMAND`` and these jobs fail to finish before +powerline prompt is run. + +Solution to this problem is simple: be sure that :file:`z.sh` is sourced +strictly after :file:`powerline/bindings/bash/powerline.sh`. This way background +jobs are spawned by `z `_ after powerline has done +its job. + +When using shell I do not see powerline fancy characters +-------------------------------------------------------- + +If your locale encoding is not unicode (any encoding that starts with “utf” or +“ucs” will work, case is ignored) powerline falls back to ascii-only theme. You +should set up your system to use unicode locale or forget about powerline fancy +characters. + +Vim issues +========== + +My vim statusline has strange characters like ``^B`` in it! +----------------------------------------------------------- + +* Please add ``set encoding=utf-8`` to your :file:`vimrc`. + +My vim statusline has a lot of ``^`` or underline characters in it! +------------------------------------------------------------------- + +* You need to configure the ``fillchars`` setting to disable statusline + fillchars (see ``:h fillchars`` for details). Add this to your + :file:`vimrc` to solve this issue: + + .. code-block:: vim + + set fillchars+=stl:\ ,stlnc:\ + +My vim statusline is hidden/only appears in split windows! +---------------------------------------------------------- + +* Make sure that you have ``set laststatus=2`` in your :file:`vimrc`. + +My vim statusline is not displayed completely and has too much spaces +--------------------------------------------------------------------- + +* Be sure you have ``ambiwidth`` option set to ``single``. +* Alternative: set :ref:`ambiwidth ` to 2, remove fancy + dividers (they suck when ``ambiwidth`` is set to double). + +Powerline loses color after editing vimrc +----------------------------------------- + +If your vimrc has something like + +.. code-block:: vim + + autocmd! BufWritePost vimrc :source ~/.vimrc + +to automatically source vimrc after saving it you must then add ``nested`` after +pattern (``vimrc`` in this case): + +.. code-block:: vim + + autocmd! BufWritePost vimrc nested :source ~/.vimrc + +. Alternatively move ``:colorscheme`` command out of the vimrc to the file which +will not be automatically resourced. Observed problem is that when you use +``:colorscheme`` command existing highlighting groups are usually cleared, +including those defined by powerline. To workaround this issue powerline hooks +``Colorscheme`` event, but when you source vimrc with ``BufWritePost`` event, +but without ``nested`` this event is not launched. See also `autocmd-nested +`_ +Vim documentation. + +Powerline loses color after saving any file +------------------------------------------- + +It may be one of the incarnations of the above issue: specifically minibufexpl +is known to trigger it. If you are using minibufexplorer you should set + +.. code-block:: vim + + let g:miniBufExplForceSyntaxEnable = 1 + +variable so that this issue is not triggered. Complete explanation: + +#. When MBE autocommand is executed it launches ``:syntax enable`` Vim command… +#. … which makes Vim source :file:`syntax/syntax.vim` file … +#. … which in turn sources :file:`syntax/synload.vim` … +#. … which executes ``:colorscheme`` command. Normally this command triggers + ``Colorscheme`` event, but in the first point minibufexplorer did set up + autocommands that miss ``nested`` attribute meaning that no events will be + triggered when processing MBE events. + +.. note:: + This setting was introduced in version 6.3.1 of `minibufexpl + `_ and removed in + version 6.5.0 of its successor `minibufexplorer + `_. It is highly + advised to use the latter because `minibufexpl`_ was last updated late in + 2004. diff --git a/docs/source/troubleshooting/linux.rst b/docs/source/troubleshooting/linux.rst new file mode 100644 index 00000000..ed069d94 --- /dev/null +++ b/docs/source/troubleshooting/linux.rst @@ -0,0 +1,21 @@ +************************ +Troubleshooting on Linux +************************ + +I can't see any fancy symbols, what's wrong? +-------------------------------------------- + +* Make sure that you've configured gvim or your terminal emulator to use + a patched 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. +* Make sure that vim is compiled with the ``--with-features=big`` flag. +* If you're using rxvt-unicode, make sure that it's compiled with the + ``--enable-unicode3`` flag. + +The fancy symbols look a bit blurry or "off"! +--------------------------------------------- + +* Make sure that you have patched all variants of your font (i.e. both the + regular and the bold font files). diff --git a/docs/source/troubleshooting/osx.rst b/docs/source/troubleshooting/osx.rst new file mode 100644 index 00000000..653a06be --- /dev/null +++ b/docs/source/troubleshooting/osx.rst @@ -0,0 +1,61 @@ +*********************** +Troubleshooting on OS X +*********************** + +I can't see any fancy symbols, what's wrong? +-------------------------------------------- + +* If you're using iTerm2, please update to `this revision + `_ + or newer. +* 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. + +The colors look weird in the default OS X Terminal app! +------------------------------------------------------- + +* The arrows may have the wrong colors if you have changed the "minimum + contrast" slider in the color tab of your OS X settings. +* The default OS X Terminal app is known to have some issues with the + Powerline colors. Please use another terminal emulator. iTerm2 should work + fine. + +The colors look weird in iTerm2! +-------------------------------- + +* The arrows may have the wrong colors if you have changed the "minimum + contrast" slider in the color tab of your OS X settings. +* Please disable background transparency to resolve this issue. + +Statusline is getting wrapped to the next line in iTerm2 +-------------------------------------------------------- + +* Turn off “Treat ambigious-width characters as double width” in `Preferences + --> Text`. +* Alternative: remove fancy dividers (they suck in this case), set + :ref:`ambiwidth ` to 2. + +I receive a ``NameError`` when trying to use Powerline with MacVim! +------------------------------------------------------------------- + +* Please install MacVim using this command:: + + brew install macvim --env-std --override-system-vim + + Then install Powerline locally with ``pip install --user``, or by + running these commands in the ``powerline`` directory:: + + ./setup.py build + ./setup.py install --user + +I receive an ``ImportError`` when trying to use Powerline on OS X! +------------------------------------------------------------------ + +* This is caused by an invalid ``sys.path`` when using system vim and system + Python. Please try to select another Python distribution:: + + sudo port select python python27-apple + +* See `issue #39 `_ for + a discussion and other possible solutions for this issue. diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 00000000..c5d829fb --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,74 @@ +***** +Usage +***** + +Application-specific requirements +--------------------------------- + +Vim plugin requirements +^^^^^^^^^^^^^^^^^^^^^^^ + +The vim plugin requires a vim version with Python support compiled in. You +can check if your vim supports Python by running ``vim --version | grep ++python``. + +If your vim version doesn't have support for Python, you'll have to compile +it with the ``--enable-pythoninterp`` flag (``--enable-python3interp`` if +you want Python 3 support instead). Note that this also requires the related +Python headers to be installed on your system. Please consult your +distribution's documentation for details on how to compile and install +packages. + +Vim version 7.4 or newer is recommended for performance reasons, but Powerline +is known to work on vim-7.0.112 (some segments may not work though as it was not +actually tested). + +.. _usage-terminal-emulators: + +Terminal emulator requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Powerline uses several special glyphs to get the arrow effect and some +custom symbols for developers. This requires that you either have a symbol +font or a patched font on your system. Your terminal emulator must also +support either patched fonts or fontconfig for Powerline to work properly. + +You can also enable :ref:`24-bit color support ` +if your terminal emulator supports it. + +.. table:: Application/terminal emulator feature support matrix + :name: term-feature-support-matrix + + ===================== ======= ===================== ===================== ===================== + Name OS Patched font support Fontconfig support 24-bit color support + ===================== ======= ===================== ===================== ===================== + Gvim Linux |i_yes| |i_no| |i_yes| + iTerm2 OS X |i_yes| |i_no| |i_no| + Konsole Linux |i_yes| |i_yes| |i_yes| + lxterminal Linux |i_yes| |i_yes| |i_no| + MacVim OS X |i_yes| |i_no| |i_yes| + rxvt-unicode Linux |i_partial| [#]_ |i_no| |i_no| + st Linux |i_yes| |i_yes| |i_yes| [#]_ + Terminal.app OS X |i_yes| |i_no| |i_no| + libvte-based [#]_ Linux |i_yes| |i_yes| |i_yes| [#]_ + xterm Linux |i_yes| |i_no| |i_partial| [#]_ + ===================== ======= ===================== ===================== ===================== + +.. |i_yes| image:: _static/img/icons/tick.png +.. |i_no| image:: _static/img/icons/cross.png +.. |i_partial| image:: _static/img/icons/error.png + +.. [#] Must be compiled with ``--enable-unicode3`` for the patched font to work. +.. [#] Since version 0.5. +.. [#] Including XFCE terminal and GNOME terminal. +.. [#] Since version 0.36. +.. [#] Uses nearest color from 8-bit palette. + +Plugins +------- + +.. toctree:: + + usage/shell-prompts + usage/wm-widgets + usage/other diff --git a/docs/source/usage/other.rst b/docs/source/usage/other.rst new file mode 100644 index 00000000..06b39472 --- /dev/null +++ b/docs/source/usage/other.rst @@ -0,0 +1,121 @@ +************* +Other plugins +************* + +.. _vim-vimrc: + +Vim statusline +============== + +If installed using pip just add + +.. code-block:: vim + + python from powerline.vim import setup as powerline_setup + python powerline_setup() + python del powerline_setup + +(replace ``python`` with ``python3`` if appropriate) to your :file:`vimrc`. + +If you just cloned the repository add the following line to your :file:`vimrc`, +where ``{repository_root}`` is the absolute path to your Powerline installation +directory: + +.. code-block:: vim + + set rtp+={repository_root}/powerline/bindings/vim + +If you're using pathogen and don't want Powerline functionality in any other +applications, simply add Powerline as a bundle and point the path above to the +Powerline bundle directory, e.g. +``~/.vim/bundle/powerline/powerline/bindings/vim``. + +With Vundle you may instead use + +.. code-block:: vim + + Bundle 'Lokaltog/powerline', {'rtp': 'powerline/bindings/vim/'} + +(replace ``Bundle`` with ``NeoBundle`` for NeoBundle). + +For vim-addon-manager it is even easier since you don’t need to write this big +path or install anything by hand: ``powerline`` is installed and run just like +any other plugin using + +.. code-block:: vim + + call vam#ActivateAddons(['powerline']) + +.. warning:: + *Never* install powerline with pathogen/VAM/Vundle/NeoBundle *and* with pip. + If you want powerline functionality in vim and other applications use + system-wide installation if your system has powerline package, pip-only or + ``pip install --editable`` kind of installation performed on the repository + installed by Vim plugin manager. + + If you have installed powerline with pip and with some of Vim package + managers do never report any errors to powerline bug tracker, especially + errors occurring after updates. + +.. note:: + If you use supplied :file:`powerline.vim` file to load powerline there are + additional configuration variables available: ``g:powerline_pycmd`` and + ``g:powerline_pyeval``. First sets command used to load powerline: expected + values are ``"py"`` and ``"py3"``. Second sets function used in statusline, + expected values are ``"pyeval"`` and ``"py3eval"``. + + If ``g:powerline_pycmd`` is set to the one of the expected values then + ``g:powerline_pyeval`` will be set accordingly. If it is set to some other + value then you must also set ``g:powerline_pyeval``. Powerline will not + check that Vim is compiled with Python support if you set + ``g:powerline_pycmd`` to an unexpected value. + + These values are to be used to specify the only Python that is to be loaded + if you have both versions: Vim may disable loading one python version if + other was already loaded. They should also be used if you have two python + versions able to load simultaneously, but with powerline installed only for + python-3 version. + +Tmux statusline +=============== + +Add the following lines to your :file:`.tmux.conf`, where ``{repository_root}`` +is the absolute path to your Powerline installation directory:: + + source "{repository_root}/powerline/bindings/tmux/powerline.conf" + +.. note:: + The availability of the ``powerline-config`` command is required for + powerline support. You may specify location of this script via + ``$POWERLINE_CONFIG_COMMAND`` environment variable. + +.. note:: + It is advised that you run ``powerline-daemon`` before adding the above line + to tmux.conf. To do so add:: + + run-shell "powerline-daemon -q" + + to :file:`.tmux.conf`. + +IPython prompt +============== + +For IPython<0.11 add the following lines to your +:file:`.ipython/ipy_user_conf.py`:: + + # top + from powerline.bindings.ipython.pre_0_11 import setup as powerline_setup + + # main() function (assuming you launched ipython without configuration to + # create skeleton ipy_user_conf.py file): + powerline_setup() + +For IPython>=0.11 add the following line to your :file:`ipython_config.py` +file in the profile you are using:: + + c.InteractiveShellApp.extensions = [ + 'powerline.bindings.ipython.post_0_11' + ] + +IPython=0.11* is not supported and does not work. IPython<0.10 was not +tested (not installable by pip). diff --git a/docs/source/usage/shell-prompts.rst b/docs/source/usage/shell-prompts.rst new file mode 100644 index 00000000..75fe7a3e --- /dev/null +++ b/docs/source/usage/shell-prompts.rst @@ -0,0 +1,114 @@ +************* +Shell prompts +************* + +.. note:: + Powerline daemon is not run automatically by any of my bindings. It is + advised that you add + + .. code-block:: bash + + powerline-daemon -q + + before any other powerline-related code in your shell configuration file. + +Bash prompt +=========== + +Add the following line to your :file:`bashrc`, where ``{repository_root}`` is +the absolute path to your Powerline installation directory: + +.. code-block:: bash + + . {repository_root}/powerline/bindings/bash/powerline.sh + +.. note:: + Since without powerline daemon bash bindings are very slow PS2 + (continuation) and PS3 (select) prompts are not set up. Thus it is advised + to use + + .. code-block:: bash + + powerline-daemon -q + POWERLINE_BASH_CONTINUATION=1 + POWERLINE_BASH_SELECT=1 + . {repository_root}/powerline/bindings/bash/powerline.sh + + in your bash configuration file. Without ``POWERLINE_BASH_*`` variables PS2 + and PS3 prompts are computed exactly once at bash startup. + +.. warning:: + At maximum bash continuation PS2 and select PS3 prompts are computed each + time main PS1 prompt is computed. Do not expect it to work properly if you + e.g. put current time there. + + At minimum they are computed once on startup. + +Zsh prompt +========== + +Add the following line to your :file:`zshrc`, where ``{repository_root}`` is the +absolute path to your Powerline installation directory: + +.. code-block:: bash + + . {repository_root}/powerline/bindings/zsh/powerline.zsh + +Fish prompt +=========== + +Add the following line to your :file:`config.fish`, where ``{repository_root}`` +is the absolute path to your Powerline installation directory: + +.. code-block:: bash + + set fish_function_path $fish_function_path "{repository_root}/powerline/bindings/fish" + powerline-setup + +.. _tmux-statusline: + +Busybox (ash), mksh and dash prompt +===================================== + +After launching busybox run the following command: + +.. code-block:: bash + + . {repository_root}/powerline/bindings/shell/powerline.sh + +Mksh users may put this line into ``~/.mkshrc`` file. Dash users may use the +following in ``~/.profile``: + +.. code-block:: bash + + if test "x$0" != "x${0#dash}" ; then + export ENV={repository_root}/powerline/bindings/shell/powerline.sh + fi + +.. note:: + Dash users that already have ``$ENV`` defined should either put the ``. + …/shell/powerline.sh`` line in the ``$ENV`` file or create a new file which + will source (using ``.`` command) both former ``$ENV`` file and + :file:`powerline.sh` files and set ``$ENV`` to the path of this new file. + +.. warning:: + Mksh users have to set ``$POWERLINE_SHELL_CONTINUATION`` and + ``$POWERLINE_SHELL_SELECT`` to 1 to get PS2 and PS3 (continuation and + select) prompts support respectively: as command substitution is not + performed in these shells for these prompts they are updated once each time + PS1 prompt is displayed which may be slow. + + It is also known that while PS2 and PS3 update is triggered at PS1 update it + is *actually performed* only *next* time PS1 is displayed which means that + PS2 and PS3 prompts will be outdated and may be incorrect for this reason. + + Without these variables PS2 and PS3 prompts will be set once at startup. + This only touches mksh users: busybox and dash both have no such problem. + +.. warning:: + Job count is using some weird hack that uses signals and temporary files for + interprocess communication. It may be wrong sometimes. Not the case in mksh. + +.. warning:: + Busybox has two shells: ``ash`` and ``hush``. Second is known to segfault in + busybox 1.22.1 when using :file:`powerline.sh` script. diff --git a/docs/source/usage/wm-widgets.rst b/docs/source/usage/wm-widgets.rst new file mode 100644 index 00000000..5383a2fd --- /dev/null +++ b/docs/source/usage/wm-widgets.rst @@ -0,0 +1,64 @@ +********************** +Window manager widgets +********************** + +Awesome widget +============== + +.. note:: Powerline currently only supports awesome 3.5. + +.. note:: The Powerline widget will spawn a shell script that runs in the + background and updates the statusline with ``awesome-client``. + +Add the following to your :file:`rc.lua`, where ``{repository_root}`` is the +absolute path to your Powerline installation directory: + +.. code-block:: lua + + package.path = package.path .. ';{repository_root}/powerline/bindings/awesome/?.lua' + require('powerline') + +Then add the ``powerline_widget`` to your ``wibox``: + +.. code-block:: lua + + right_layout:add(powerline_widget) + +Qtile widget +============ + +Add the following to your :file:`~/.config/qtile/config.py`: + +.. code-block:: python + + from powerline.bindings.qtile.widget import Powerline + + screens = [ + Screen( + top=bar.Bar([ + # ... + Powerline(timeout=2), + # ... + ], + ), + ), + ] + +I3 bar +====== + +.. note:: Until the patch is done in i3, you will need a custom ``i3bar`` build + called ``i3bgbar``. The source is available `here + `_. + +Add the following to your :file:`~/.i3/config`:: + + bar { + i3bar_command i3bgbar + + status_command python /path/to/powerline/bindings/i3/powerline-i3.py + font pango:PowerlineFont 12 + } + +where ``i3bgbar`` may be replaced with the path to the custom i3bar binary and +``PowerlineFont`` is any system font with powerline support. diff --git a/font/10-powerline-symbols.conf b/font/10-powerline-symbols.conf new file mode 100644 index 00000000..7e34a12a --- /dev/null +++ b/font/10-powerline-symbols.conf @@ -0,0 +1,105 @@ + + + + + + monospace + PowerlineSymbols + + + Droid Sans Mono + PowerlineSymbols + + + Droid Sans Mono Slashed + PowerlineSymbols + + + Droid Sans Mono Dotted + PowerlineSymbols + + + DejaVu Sans Mono + PowerlineSymbols + + + DejaVu Sans Mono + PowerlineSymbols + + + Envy Code R + PowerlineSymbols + + + Inconsolata + PowerlineSymbols + + + Lucida Console + PowerlineSymbols + + + Monaco + PowerlineSymbols + + + Pragmata + PowerlineSymbols + + + PragmataPro + PowerlineSymbols + + + Menlo + PowerlineSymbols + + + Source Code Pro + PowerlineSymbols + + + Consolas + PowerlineSymbols + + + Anonymous pro + PowerlineSymbols + + + Bitstream Vera Sans Mono + PowerlineSymbols + + + Liberation Mono + PowerlineSymbols + + + Ubuntu Mono + PowerlineSymbols + + + Meslo LG L + PowerlineSymbols + + + Meslo LG L DZ + PowerlineSymbols + + + Meslo LG M + PowerlineSymbols + + + Meslo LG M DZ + PowerlineSymbols + + + Meslo LG S + PowerlineSymbols + + + Meslo LG S DZ + PowerlineSymbols + + diff --git a/font/PowerlineSymbols.otf b/font/PowerlineSymbols.otf new file mode 100644 index 00000000..b1582afb Binary files /dev/null and b/font/PowerlineSymbols.otf differ diff --git a/powerline/__init__.py b/powerline/__init__.py new file mode 100644 index 00000000..1d01d5d5 --- /dev/null +++ b/powerline/__init__.py @@ -0,0 +1,864 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import logging + +from locale import getpreferredencoding +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.config import DEFAULT_SYSTEM_CONFIG_DIR +from powerline.lib import mergedicts + + +def _config_loader_condition(path): + if path and os.path.isfile(path): + return path + return None + + +def _find_config_files(search_paths, config_file, config_loader=None, loader_callback=None): + config_file += '.json' + found = False + for path in search_paths: + config_file_path = os.path.join(path, config_file) + if os.path.isfile(config_file_path): + yield config_file_path + found = True + elif config_loader: + config_loader.register_missing(_config_loader_condition, loader_callback, config_file_path) + if not found: + raise IOError('Config file not found in search paths ({0}): {1}'.format( + ', '.join(search_paths), + config_file + )) + + +class PowerlineLogger(object): + '''Proxy class for logging.Logger instance + + It emits messages in format ``{ext}:{prefix}:{message}`` where + + ``{ext}`` + is a used powerline extension (e.g. “vim”, “shell”, “ipython”). + ``{prefix}`` + is a local prefix, usually a segment name. + ``{message}`` + is the original message passed to one of the logging methods. + + Each of the methods (``critical``, ``exception``, ``info``, ``error``, + ``warn``, ``debug``) expects to receive message in an ``str.format`` format, + not in printf-like format. + + Log is saved to the location :ref:`specified by user `. + ''' + + def __init__(self, use_daemon_threads, logger, ext): + self.logger = logger + self.ext = ext + self.use_daemon_threads = use_daemon_threads + self.prefix = '' + self.last_msgs = {} + + def _log(self, attr, msg, *args, **kwargs): + prefix = kwargs.get('prefix') or self.prefix + prefix = self.ext + ((':' + prefix) if prefix else '') + msg = safe_unicode(msg) + if args or kwargs: + args = [safe_unicode(s) if isinstance(s, bytes) else s for s in args] + kwargs = dict(( + (k, safe_unicode(v) if isinstance(v, bytes) else v) + for k, v in kwargs.items() + )) + msg = msg.format(*args, **kwargs) + msg = prefix + ':' + msg + key = attr + ':' + prefix + if msg != self.last_msgs.get(key): + getattr(self.logger, attr)(msg) + self.last_msgs[key] = msg + + def critical(self, msg, *args, **kwargs): + self._log('critical', msg, *args, **kwargs) + + def exception(self, msg, *args, **kwargs): + self._log('exception', msg, *args, **kwargs) + + def info(self, msg, *args, **kwargs): + self._log('info', msg, *args, **kwargs) + + def error(self, msg, *args, **kwargs): + self._log('error', msg, *args, **kwargs) + + def warn(self, msg, *args, **kwargs): + self._log('warning', msg, *args, **kwargs) + + def debug(self, msg, *args, **kwargs): + self._log('debug', msg, *args, **kwargs) + + +_fallback_logger = None + + +def get_fallback_logger(stream=None): + global _fallback_logger + if _fallback_logger: + return _fallback_logger + + log_format = '%(asctime)s:%(levelname)s:%(message)s' + formatter = logging.Formatter(log_format) + + level = logging.WARNING + handler = logging.StreamHandler(stream) + handler.setLevel(level) + handler.setFormatter(formatter) + + logger = logging.getLogger('powerline') + logger.setLevel(level) + logger.addHandler(handler) + _fallback_logger = PowerlineLogger(None, logger, '_fallback_') + return _fallback_logger + + +def _generate_change_callback(lock, key, dictionary): + def on_file_change(path): + with lock: + dictionary[key] = True + return on_file_change + + +def get_config_paths(): + '''Get configuration paths from environment variables. + + Uses $XDG_CONFIG_HOME and $XDG_CONFIG_DIRS according to the XDG specification. + + :return: list of paths + ''' + config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')) + config_path = os.path.join(config_home, 'powerline') + config_paths = [config_path] + config_dirs = os.environ.get('XDG_CONFIG_DIRS', DEFAULT_SYSTEM_CONFIG_DIR) + if config_dirs is not None: + config_paths[:0] = reversed([os.path.join(d, 'powerline') for d in config_dirs.split(':')]) + plugin_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'config_files') + config_paths.insert(0, plugin_path) + return config_paths + + +def generate_config_finder(get_config_paths=get_config_paths): + '''Generate find_config_files function + + This function will find .json file given its path. + + :param function get_config_paths: + Function that being called with no arguments will return a list of paths + that should be searched for configuration files. + + :return: + Function that being given configuration file name will return full path + to it or raise IOError if it failed to find the file. + ''' + config_paths = get_config_paths() + return lambda *args: _find_config_files(config_paths, *args) + + +def load_config(cfg_path, find_config_files, config_loader, loader_callback=None): + '''Load configuration file and setup watches + + Watches are only set up if loader_callback is not None. + + :param str cfg_path: + Path for configuration file that should be loaded. + :param function find_config_files: + Function that finds configuration file. Check out the description of + the return value of ``generate_config_finder`` function. + :param ConfigLoader config_loader: + Configuration file loader class instance. + :param function loader_callback: + Function that will be called by config_loader when change to + configuration file is detected. + + :return: Configuration file contents. + ''' + found_files = find_config_files(cfg_path, config_loader, loader_callback) + ret = None + for path in found_files: + if loader_callback: + config_loader.register(loader_callback, path) + if ret is None: + ret = config_loader.load(path) + else: + mergedicts(ret, config_loader.load(path)) + return ret + + +def _get_log_handler(common_config, stream=None): + '''Get log handler. + + :param dict common_config: + Configuration dictionary used to create handler. + + :return: logging.Handler subclass. + ''' + 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) + + +def create_logger(common_config, stream=None): + '''Create logger according to provided configuration + ''' + log_format = common_config['log_format'] + formatter = logging.Formatter(log_format) + + 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 + + +def finish_common_config(encoding, common_config): + '''Add default values to common config and expand ~ in paths + + :param dict common_config: + Common configuration, as it was just loaded. + + :return: + Copy of common configuration with all configuration keys and expanded + paths. + ''' + encoding = encoding.lower() + if encoding.startswith('utf') or encoding.startswith('ucs'): + default_top_theme = 'powerline' + else: + default_top_theme = 'ascii' + + common_config = common_config.copy() + common_config.setdefault('default_top_theme', default_top_theme) + common_config.setdefault('paths', []) + common_config.setdefault('watcher', 'auto') + common_config.setdefault('log_level', 'WARNING') + common_config.setdefault('log_format', '%(asctime)s:%(levelname)s:%(message)s') + common_config.setdefault('term_truecolor', False) + common_config.setdefault('ambiwidth', 1) + 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['paths'] = [ + os.path.expanduser(path) for path in common_config['paths'] + ] + + return common_config + + +if sys.version_info < (3,): + # `raise exception[0], None, exception[1]` is a SyntaxError in python-3* + # Not using ('''…''') because this syntax does not work in python-2.6 + exec(( + 'def reraise(exception):\n' + ' if type(exception) is tuple:\n' + ' raise exception[0], None, exception[1]\n' + ' else:\n' + ' raise exception\n' + )) +else: + def reraise(exception): + if type(exception) is tuple: + raise exception[0].with_traceback(exception[1]) + else: + raise exception + + +def gen_module_attr_getter(pl, import_paths, imported_modules): + def get_module_attr(module, attr, prefix='powerline'): + '''Import module and get its attribute. + + Replaces ``from {module} import {attr}``. + + :param str module: + Module name, will be passed as first argument to ``__import__``. + :param str attr: + Module attribute, will be passed to ``__import__`` as the only value + in ``fromlist`` tuple. + + :return: + Attribute value or ``None``. Note: there is no way to distinguish + between successfull import of attribute equal to ``None`` and + unsuccessfull import. + ''' + oldpath = sys.path + sys.path = import_paths + sys.path + module = str(module) + attr = str(attr) + try: + imported_modules.add(module) + return getattr(__import__(module, fromlist=(attr,)), attr) + except Exception as e: + pl.exception('Failed to import attr {0} from module {1}: {2}', attr, module, str(e), prefix=prefix) + return None + finally: + sys.path = oldpath + + return get_module_attr + + +class Powerline(object): + '''Main powerline class, entrance point for all powerline uses. Sets + powerline up and loads the configuration. + + :param str ext: + extension used. Determines where configuration files will + searched and what renderer module will be used. Affected: used ``ext`` + dictionary from :file:`powerline/config.json`, location of themes and + colorschemes, render module (``powerline.renders.{ext}``). + :param str renderer_module: + Overrides renderer module (defaults to ``ext``). Should be the name of + the package imported like this: ``powerline.renderers.{render_module}``. + If this parameter contains a dot ``powerline.renderers.`` is not + prepended. There is also a special case for renderers defined in + toplevel modules: ``foo.`` (note: dot at the end) tries to get renderer + from module ``foo`` (because ``foo`` (without dot) tries to get renderer + from module ``powerline.renderers.foo``). When ``.foo`` (with leading + dot) variant is used ``renderer_module`` will be + ``powerline.renderers.{ext}{renderer_module}``. + :param bool run_once: + Determines whether :py:meth:`render` method will be run only once + during python session. + :param Logger logger: + If present no new logger will be created and the provided logger will be + used. + :param bool use_daemon_threads: + When creating threads make them daemon ones. + :param Event shutdown_event: + Use this Event as shutdown_event instead of creating new event. + :param ConfigLoader config_loader: + Instance of the class that manages (re)loading of the configuration. + ''' + + def __init__(self, *args, **kwargs): + self.init_args = (args, kwargs) + self.init(*args, **kwargs) + + def init(self, + ext, + renderer_module=None, + run_once=False, + logger=None, + use_daemon_threads=True, + shutdown_event=None, + config_loader=None): + '''Do actual initialization. + + __init__ function only stores the arguments and runs this function. This + function exists for powerline to be able to reload itself: it is easier + to make ``__init__`` store arguments and call overriddable ``init`` than + tell developers that each time they override Powerline.__init__ in + subclasses they must store actual arguments. + ''' + self.ext = ext + self.run_once = run_once + self.logger = logger + self.use_daemon_threads = use_daemon_threads + + if not renderer_module: + self.renderer_module = 'powerline.renderers.' + ext + elif '.' not in renderer_module: + self.renderer_module = 'powerline.renderers.' + renderer_module + elif renderer_module.startswith('.'): + self.renderer_module = 'powerline.renderers.' + ext + renderer_module + elif renderer_module.endswith('.'): + self.renderer_module = renderer_module[:-1] + else: + self.renderer_module = renderer_module + + self.find_config_files = generate_config_finder(self.get_config_paths) + + self.cr_kwargs_lock = Lock() + self.cr_kwargs = {} + self.cr_callbacks = {} + for key in ('main', 'colors', 'colorscheme', 'theme'): + self.cr_kwargs['load_' + key] = True + self.cr_callbacks[key] = _generate_change_callback( + self.cr_kwargs_lock, + 'load_' + key, + self.cr_kwargs + ) + + self.shutdown_event = shutdown_event or Event() + self.config_loader = config_loader or ConfigLoader(shutdown_event=self.shutdown_event, run_once=run_once) + self.run_loader_update = False + + self.renderer_options = {} + + self.prev_common_config = None + self.prev_ext_config = None + self.pl = None + self.setup_args = () + self.setup_kwargs = {} + self.imported_modules = set() + + get_encoding = staticmethod(getpreferredencoding) + '''Get encoding used by the current application + + Usually returns encoding of the current locale. + ''' + + 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 + successfully initialized. If any of the below parameters except + ``load_main`` is True renderer object will be recreated. + + :param bool load_main: + Determines whether main configuration file (:file:`config.json`) + should be loaded. If appropriate configuration changes implies + ``load_colorscheme`` and ``load_theme`` and recreation of renderer + object. Won’t trigger recreation if only unrelated configuration + changed. + :param bool load_colors: + Determines whether colors configuration from :file:`colors.json` + should be (re)loaded. + :param bool load_colorscheme: + Determines whether colorscheme configuration should be (re)loaded. + :param bool load_theme: + Determines whether theme configuration should be reloaded. + ''' + common_config_differs = False + ext_config_differs = False + if load_main: + self._purge_configs('main') + config = self.load_main_config() + self.common_config = finish_common_config(self.get_encoding(), config['common']) + if self.common_config != self.prev_common_config: + common_config_differs = True + + load_theme = (load_theme + or not self.prev_common_config + or self.prev_common_config['default_top_theme'] != self.common_config['default_top_theme']) + + self.prev_common_config = self.common_config + + self.import_paths = self.common_config['paths'] + + if not self.logger: + self.logger = create_logger(self.common_config, self.default_log_stream) + + if not self.pl: + self.pl = PowerlineLogger(self.use_daemon_threads, self.logger, self.ext) + 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'], + ambiwidth=self.common_config['ambiwidth'], + tmux_escape=self.common_config['additional_escapes'] == 'tmux', + screen_escape=self.common_config['additional_escapes'] == 'screen', + theme_kwargs={ + 'ext': self.ext, + 'common_config': self.common_config, + 'run_once': self.run_once, + 'shutdown_event': self.shutdown_event, + 'get_module_attr': self.get_module_attr, + }, + )) + + if not self.run_once and self.common_config['reload_config']: + interval = self.common_config['interval'] + self.config_loader.set_interval(interval) + self.run_loader_update = (interval is None) + if interval is not None and not self.config_loader.is_alive(): + self.config_loader.start() + + self.ext_config = config['ext'][self.ext] + + top_theme = ( + self.ext_config.get('top_theme') + or self.common_config['default_top_theme'] + ) + self.theme_levels = ( + os.path.join('themes', top_theme), + os.path.join('themes', self.ext, '__main__'), + ) + self.renderer_options['theme_kwargs']['top_theme'] = top_theme + + if self.ext_config != self.prev_ext_config: + ext_config_differs = True + if ( + not self.prev_ext_config + or self.ext_config.get('components') != self.prev_ext_config.get('components') + ): + self.setup_components(self.ext_config.get('components')) + if ( + not self.prev_ext_config + or self.ext_config.get('local_themes') != self.prev_ext_config.get('local_themes') + ): + self.renderer_options['local_themes'] = self.get_local_themes(self.ext_config.get('local_themes')) + load_colorscheme = ( + load_colorscheme + or not self.prev_ext_config + or self.prev_ext_config['colorscheme'] != self.ext_config['colorscheme'] + ) + load_theme = ( + load_theme + or not self.prev_ext_config + or self.prev_ext_config['theme'] != self.ext_config['theme'] + ) + self.prev_ext_config = self.ext_config + + create_renderer = load_colors or load_colorscheme or load_theme or common_config_differs or ext_config_differs + + if load_colors: + self._purge_configs('colors') + self.colors_config = self.load_colors_config() + + if load_colorscheme or load_colors: + self._purge_configs('colorscheme') + if load_colorscheme: + self.colorscheme_config = self.load_colorscheme_config(self.ext_config['colorscheme']) + self.renderer_options['theme_kwargs']['colorscheme'] = ( + Colorscheme(self.colorscheme_config, self.colors_config)) + + if load_theme: + self._purge_configs('theme') + self.renderer_options['theme_config'] = self.load_theme_config(self.ext_config.get('theme', 'default')) + + if create_renderer: + Renderer = self.get_module_attr(self.renderer_module, 'renderer') + if not Renderer: + if hasattr(self, 'renderer'): + return + else: + raise ImportError('Failed to obtain renderer') + + # Renderer updates configuration file via segments’ .startup thus it + # should be locked to prevent state when configuration was updated, + # but .render still uses old renderer. + try: + renderer = Renderer(**self.renderer_options) + except Exception as e: + self.exception('Failed to construct renderer object: {0}', str(e)) + if not hasattr(self, 'renderer'): + raise + else: + self.renderer = renderer + + default_log_stream = sys.stdout + '''Default stream for default log handler + + Usually it is ``sys.stderr``, but there is sometimes a reason to prefer + ``sys.stdout`` or a custom file-like object. It is not supposed to be used + to write to some file. + ''' + + def setup_components(self, components): + '''Run component-specific setup + + :param set components: + Set of the enabled componets or None. + + Should be overridden by subclasses. + ''' + pass + + @staticmethod + def get_config_paths(): + '''Get configuration paths. + + Should be overridden in subclasses in order to provide a way to override + used paths. + + :return: list of paths + ''' + return get_config_paths() + + def load_config(self, cfg_path, cfg_type): + '''Load configuration and setup watches + + :param str cfg_path: + Path to the configuration file without any powerline configuration + directory or ``.json`` suffix. + :param str cfg_type: + Configuration type. May be one of ``main`` (for ``config.json`` + file), ``colors``, ``colorscheme``, ``theme``. + + :return: dictionary with loaded configuration. + ''' + return load_config( + cfg_path, + self.find_config_files, + self.config_loader, + self.cr_callbacks[cfg_type] + ) + + def _purge_configs(self, cfg_type): + function = self.cr_callbacks[cfg_type] + self.config_loader.unregister_functions(set((function,))) + self.config_loader.unregister_missing(set(((self.find_config_files, function),))) + + def load_main_config(self): + '''Get top-level configuration. + + :return: dictionary with :ref:`top-level configuration `. + ''' + return self.load_config('config', 'main') + + def _load_hierarhical_config(self, cfg_type, levels, ignore_levels): + '''Load and merge multiple configuration files + + :param str cfg_type: + Type of the loaded configuration files (e.g. ``colorscheme``, + ``theme``). + :param list levels: + Configuration names resembling levels in hierarchy, sorted by + priority. Configuration file names with higher priority should go + last. + :param set ignore_levels: + If only files listed in this variable are present then configuration + file is considered not loaded: at least one file on the level not + listed in this variable must be present. + ''' + config = {} + loaded = 0 + exceptions = [] + for i, cfg_path in enumerate(levels): + try: + lvl_config = self.load_config(cfg_path, cfg_type) + except IOError as e: + if sys.version_info < (3,): + tb = sys.exc_info()[2] + exceptions.append((e, tb)) + else: + exceptions.append(e) + else: + if i not in ignore_levels: + loaded += 1 + mergedicts(config, lvl_config) + if not loaded: + for exception in exceptions: + if type(exception) is tuple: + e = exception[0] + else: + e = exception + self.exception('Failed to load %s: {0}' % cfg_type, e, exception=exception) + raise e + return config + + def load_colorscheme_config(self, name): + '''Get colorscheme. + + :param str name: + Name of the colorscheme to load. + + :return: dictionary with :ref:`colorscheme configuration `. + ''' + levels = ( + os.path.join('colorschemes', name), + os.path.join('colorschemes', self.ext, '__main__'), + os.path.join('colorschemes', self.ext, name), + ) + return self._load_hierarhical_config('colorscheme', levels, (1,)) + + def load_theme_config(self, name): + '''Get theme configuration. + + :param str name: + Name of the theme to load. + + :return: dictionary with :ref:`theme configuration ` + ''' + levels = self.theme_levels + ( + os.path.join('themes', self.ext, name), + ) + return self._load_hierarhical_config('theme', levels, (0, 1,)) + + def load_colors_config(self): + '''Get colorscheme. + + :return: dictionary with :ref:`colors configuration `. + ''' + return self.load_config('colors', 'colors') + + @staticmethod + def get_local_themes(local_themes): + '''Get local themes. No-op here, to be overridden in subclasses if + required. + + :param dict local_themes: + Usually accepts ``{matcher_name : theme_name}``. May also receive + None in case there is no local_themes configuration. + + :return: + anything accepted by ``self.renderer.get_theme`` and processable by + ``self.renderer.add_local_theme``. Renderer module is determined by + ``__init__`` arguments, refer to its documentation. + ''' + return None + + def update_renderer(self): + '''Updates/creates a renderer if needed.''' + if self.run_loader_update: + self.config_loader.update() + cr_kwargs = None + with self.cr_kwargs_lock: + if self.cr_kwargs: + cr_kwargs = self.cr_kwargs.copy() + if cr_kwargs: + try: + self.create_renderer(**cr_kwargs) + except Exception as e: + self.exception('Failed to create renderer: {0}', str(e)) + if hasattr(self, 'renderer'): + with self.cr_kwargs_lock: + self.cr_kwargs.clear() + else: + raise + else: + with self.cr_kwargs_lock: + self.cr_kwargs.clear() + + def render(self, *args, **kwargs): + '''Update/create renderer if needed and pass all arguments further to + ``self.renderer.render()``. + ''' + try: + self.update_renderer() + return self.renderer.render(*args, **kwargs) + except Exception as e: + try: + self.exception('Failed to render: {0}', str(e)) + except Exception as e: + # Updates e variable to new value, masking previous one. + # Normally it is the same exception (due to raise in case pl is + # unset), but it may also show error in logger. Note that latter + # is not logged by logger for obvious reasons, thus this also + # prevents us from seeing logger traceback. + pass + return FailedUnicode(safe_unicode(e)) + + def render_above_lines(self, *args, **kwargs): + '''Like .render(), but for ``self.renderer.render_above_lines()`` + ''' + try: + self.update_renderer() + for line in self.renderer.render_above_lines(*args, **kwargs): + yield line + except Exception as e: + try: + self.exception('Failed to render: {0}', str(e)) + except Exception as e: + # Updates e variable to new value, masking previous one. + # Normally it is the same exception (due to raise in case pl is + # unset), but it may also show error in logger. Note that latter + # is not logged by logger for obvious reasons, thus this also + # prevents us from seeing logger traceback. + pass + yield FailedUnicode(safe_unicode(e)) + + def setup(self, *args, **kwargs): + '''Setup the environment to use powerline. + + Must not be overridden by subclasses. This one only saves setup + arguments for :py:meth:`reload` method and calls :py:meth:`do_setup`. + ''' + self.shutdown_event.clear() + self.setup_args = args + self.setup_kwargs.update(kwargs) + self.do_setup(*args, **kwargs) + + @staticmethod + def do_setup(): + '''Function that does initialization + + Should be overridden by subclasses. May accept any number of regular or + keyword arguments. + ''' + pass + + def reload(self): + '''Reload powerline after update. + + Should handle most (but not all) powerline updates. + + Purges out all powerline modules and modules imported by powerline for + segment and matcher functions. Requires defining ``setup`` function that + updates reference to main powerline object. + + .. warning:: + Not guaranteed to work properly, use it at your own risk. It + may break your python code. + ''' + import sys + modules = self.imported_modules | set((module for module in sys.modules if module.startswith('powerline'))) + modules_holder = [] + for module in modules: + try: + # Needs to hold module to prevent garbage collecting until they + # are all reloaded. + modules_holder.append(sys.modules.pop(module)) + except KeyError: + pass + PowerlineClass = getattr(__import__(self.__module__, fromlist=(self.__class__.__name__,)), self.__class__.__name__) + self.shutdown(set_event=True) + init_args, init_kwargs = self.init_args + powerline = PowerlineClass(*init_args, **init_kwargs) + powerline.setup(*self.setup_args, **self.setup_kwargs) + + def shutdown(self, set_event=True): + '''Shut down all background threads. + + :param bool set_event: + Set ``shutdown_event`` and call ``renderer.shutdown`` which should + shut down all threads. Set it to False unless you are exiting an + application. + + If set to False this does nothing more then resolving reference + cycle ``powerline → config_loader → bound methods → powerline`` by + unsubscribing from config_loader events. + ''' + if set_event: + self.shutdown_event.set() + try: + self.renderer.shutdown() + except AttributeError: + pass + functions = tuple(self.cr_callbacks.values()) + self.config_loader.unregister_functions(set(functions)) + self.config_loader.unregister_missing(set(((self.find_config_files, function) for function in functions))) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.shutdown() + + def exception(self, msg, *args, **kwargs): + if 'prefix' not in kwargs: + kwargs['prefix'] = 'powerline' + exception = kwargs.pop('exception', None) + pl = getattr(self, 'pl', None) or get_fallback_logger(self.default_log_stream) + if exception: + try: + reraise(exception) + except Exception: + return pl.exception(msg, *args, **kwargs) + return pl.exception(msg, *args, **kwargs) diff --git a/powerline/bindings/__init__.py b/powerline/bindings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline/bindings/awesome/powerline-awesome.py b/powerline/bindings/awesome/powerline-awesome.py new file mode 100755 index 00000000..11d27f57 --- /dev/null +++ b/powerline/bindings/awesome/powerline-awesome.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from time import sleep +from subprocess import Popen, PIPE + +from powerline import Powerline +from powerline.lib.monotonic import monotonic + +powerline = Powerline('wm', renderer_module='pango_markup') +powerline.update_renderer() + +try: + interval = float(sys.argv[1]) +except IndexError: + interval = 2 + + +def read_to_log(pl, client): + for line in client.stdout: + if line: + pl.info(line, prefix='awesome-client') + for line in client.stderr: + if line: + pl.error(line, prefix='awesome-client') + if client.wait(): + pl.error('Client exited with {0}', client.returncode, prefix='awesome') + + +while True: + start_time = monotonic() + s = powerline.render(side='right') + request = "powerline_widget:set_markup('" + s.replace('\\', '\\\\').replace("'", "\\'") + "')\n" + client = Popen(['awesome-client'], shell=False, stdout=PIPE, stderr=PIPE, stdin=PIPE) + client.stdin.write(request.encode('utf-8')) + client.stdin.close() + read_to_log(powerline.pl, client) + sleep(max(interval - (monotonic() - start_time), 0.1)) diff --git a/powerline/bindings/awesome/powerline.lua b/powerline/bindings/awesome/powerline.lua new file mode 100644 index 00000000..659fade4 --- /dev/null +++ b/powerline/bindings/awesome/powerline.lua @@ -0,0 +1,11 @@ +local wibox = require('wibox') +local awful = require('awful') + +powerline_widget = wibox.widget.textbox() +powerline_widget:set_align('right') + +function powerline(mode, widget) end + +bindings_path = string.gsub(debug.getinfo(1).source:match('@(.*)$'), '/[^/]+$', '') +powerline_cmd = bindings_path .. '/powerline-awesome.py' +awful.util.spawn_with_shell('ps -C powerline-awesome.py || ' .. powerline_cmd) diff --git a/powerline/bindings/bash/powerline.sh b/powerline/bindings/bash/powerline.sh new file mode 100644 index 00000000..751e65f8 --- /dev/null +++ b/powerline/bindings/bash/powerline.sh @@ -0,0 +1,99 @@ +_powerline_columns_fallback() { + if which stty &>/dev/null ; then + local cols="$(stty size 2>/dev/null)" + if ! test -z "$cols" ; then + echo "${cols#* }" + return 0 + fi + fi + echo 0 + return 0 +} + +_powerline_tmux_setenv() { + TMUX="$_POWERLINE_TMUX" tmux setenv -g TMUX_"$1"_`tmux display -p "#D" | tr -d %` "$2" + TMUX="$_POWERLINE_TMUX" tmux refresh -S +} + +_powerline_tmux_set_pwd() { + if test "x$_POWERLINE_SAVED_PWD" != "x$PWD" ; then + _POWERLINE_SAVED_PWD="$PWD" + _powerline_tmux_setenv PWD "$PWD" + fi +} + +_powerline_tmux_set_columns() { + _powerline_tmux_setenv COLUMNS "${COLUMNS:-`_powerline_columns_fallback`}" +} + +_powerline_init_tmux_support() { + if test -n "$TMUX" && tmux refresh -S &>/dev/null ; then + # TMUX variable may be unset to create new tmux session inside this one + _POWERLINE_TMUX="$TMUX" + + trap "_powerline_tmux_set_columns" WINCH + _powerline_tmux_set_columns + + test "x$PROMPT_COMMAND" != "x${PROMPT_COMMAND/_powerline_tmux_set_pwd}" || + PROMPT_COMMAND="${PROMPT_COMMAND}"$'\n_powerline_tmux_set_pwd' + fi +} + +_powerline_local_prompt() { + # Arguments: side, renderer_module arg, last_exit_code, jobnum, local theme + $POWERLINE_COMMAND shell $1 \ + $2 \ + --last_exit_code=$3 \ + --jobnum=$4 \ + --renderer_arg="client_id=$$" \ + --renderer_arg="local_theme=$5" +} + +_powerline_prompt() { + # Arguments: side, last_exit_code, jobnum + $POWERLINE_COMMAND shell $1 \ + --width="${COLUMNS:-$(_powerline_columns_fallback)}" \ + -r.bash \ + --last_exit_code=$2 \ + --jobnum=$3 \ + --renderer_arg="client_id=$$" +} + +_powerline_set_prompt() { + local last_exit_code=$? + local jobnum="$(jobs -p|wc -l)" + PS1="$(_powerline_prompt aboveleft $last_exit_code $jobnum)" + if test -n "$POWERLINE_SHELL_CONTINUATION$POWERLINE_BASH_CONTINUATION" ; then + PS2="$(_powerline_local_prompt left -r.bash $last_exit_code $jobnum continuation)" + fi + if test -n "$POWERLINE_SHELL_SELECT$POWERLINE_BASH_SELECT" ; then + PS3="$(_powerline_local_prompt left '' $last_exit_code $jobnum select)" + fi + return $last_exit_code +} + +_powerline_setup_prompt() { + VIRTUAL_ENV_DISABLE_PROMPT=1 + if test -z "${POWERLINE_COMMAND}" ; then + POWERLINE_COMMAND="$("$POWERLINE_CONFIG" shell command)" + fi + test "x$PROMPT_COMMAND" != "x${PROMPT_COMMAND%_powerline_set_prompt*}" || + PROMPT_COMMAND=$'_powerline_set_prompt\n'"${PROMPT_COMMAND}" + PS2="$(_powerline_local_prompt left -r.bash 0 0 continuation)" + PS3="$(_powerline_local_prompt left '' 0 0 select)" +} + +if test -z "${POWERLINE_CONFIG}" ; then + if which powerline-config >/dev/null ; then + POWERLINE_CONFIG=powerline-config + else + POWERLINE_CONFIG="$(dirname "$BASH_SOURCE")/../../../scripts/powerline-config" + fi +fi + +if "${POWERLINE_CONFIG}" shell --shell=bash uses prompt ; then + _powerline_setup_prompt +fi +if "${POWERLINE_CONFIG}" shell --shell=bash uses tmux ; then + _powerline_init_tmux_support +fi diff --git a/powerline/bindings/config.py b/powerline/bindings/config.py new file mode 100644 index 00000000..71afca84 --- /dev/null +++ b/powerline/bindings/config.py @@ -0,0 +1,149 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re +import sys + +from locale import getpreferredencoding + +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.lib.shell import which +from powerline.bindings.tmux import TmuxVersionInfo, run_tmux_command, get_tmux_version + + +CONFIG_FILE_NAME = re.compile(r'powerline_tmux_(?P\d+)\.(?P\d+)(?P[a-z]+)?(?:_(?Pplus|minus))?\.conf') +CONFIG_MATCHERS = { + None: (lambda a, b: a.major == b.major and a.minor == b.minor), + 'plus': (lambda a, b: a[:2] <= b[:2]), + 'minus': (lambda a, b: a[:2] >= b[:2]), +} +CONFIG_PRIORITY = { + None: 3, + 'plus': 2, + 'minus': 1, +} + + +def list_all_tmux_configs(): + '''List all version-specific tmux configuration files''' + directory = TMUX_CONFIG_DIRECTORY + for root, dirs, files in os.walk(directory): + dirs[:] = () + for fname in files: + match = CONFIG_FILE_NAME.match(fname) + if match: + assert match.group('suffix') is None + yield ( + os.path.join(root, fname), + CONFIG_MATCHERS[match.group('mod')], + CONFIG_PRIORITY[match.group('mod')], + TmuxVersionInfo( + int(match.group('major')), + int(match.group('minor')), + match.group('suffix'), + ), + ) + + +def get_tmux_configs(version): + '''Get tmux configuration suffix given parsed tmux version + + :param TmuxVersionInfo version: Parsed tmux version. + ''' + for fname, matcher, priority, file_version in list_all_tmux_configs(): + if matcher(file_version, version): + yield (fname, priority + file_version.minor * 10 + file_version.major * 10000) + + +def source_tmux_files(pl, args): + '''Source relevant version-specific tmux configuration files + + Files are sourced in the following order: + * First relevant files with older versions are sourced. + * If files for same versions are to be sourced then first _minus files are + sourced, then _plus files and then files without _minus or _plus suffixes. + ''' + version = get_tmux_version(pl) + for fname, priority in sorted(get_tmux_configs(version), key=(lambda v: v[1])): + run_tmux_command('source', fname) + if not os.environ.get('POWERLINE_COMMAND'): + cmd = deduce_command() + if cmd: + run_tmux_command('set-environment', '-g', 'POWERLINE_COMMAND', deduce_command()) + run_tmux_command('refresh-client') + + +def get_main_config(args): + find_config_files = generate_config_finder() + config_loader = ConfigLoader(run_once=True) + return load_config('config', find_config_files, config_loader) + + +def create_powerline_logger(args): + config = get_main_config(args) + common_config = finish_common_config(getpreferredencoding(), config['common']) + logger = create_logger(common_config) + return PowerlineLogger(use_daemon_threads=True, logger=logger, ext='config') + + +def check_command(cmd): + if which(cmd): + return cmd + + +def deduce_command(): + '''Deduce which command to use for ``powerline`` + + Candidates: + + * ``powerline``. Present only when installed system-wide. + * ``{powerline_root}/scripts/powerline``. Present after ``pip install -e`` + was run and C client was compiled (in this case ``pip`` does not install + binary file). + * ``{powerline_root}/client/powerline.sh``. Useful when ``sh``, ``sed`` and + ``socat`` are present, but ``pip`` or ``setup.py`` was not run. + * ``{powerline_root}/client/powerline.py``. Like above, but when one of + ``sh``, ``sed`` and ``socat`` was not present. + * ``powerline-render``. Should not really ever be used. + * ``{powerline_root}/scripts/powerline-render``. Same. + ''' + return ( + None + or check_command('powerline') + or check_command(os.path.join(POWERLINE_ROOT, 'scripts', 'powerline')) + or ((which('sh') and which('sed') and which('socat')) + and check_command(os.path.join(POWERLINE_ROOT, 'client', 'powerline.sh'))) + or check_command(os.path.join(POWERLINE_ROOT, 'client', 'powerline.py')) + or check_command('powerline-render') + or check_command(os.path.join(POWERLINE_ROOT, 'scripts', 'powerline-render')) + ) + + +def shell_command(pl, args): + cmd = deduce_command() + if cmd: + print(cmd) + else: + sys.exit(1) + + +def uses(pl, args): + component = args.component + if not component: + raise ValueError('Must specify component') + shell = args.shell + template = 'POWERLINE_NO_{shell}_{component}' + for sh in (shell, 'shell') if shell else ('shell'): + varname = template.format(shell=sh.upper(), component=component.upper()) + if os.environ.get(varname): + print ('HERE') + sys.exit(1) + config = get_main_config(args) + if component in config.get('ext', {}).get('shell', {}).get('components', ('tmux', 'prompt')): + sys.exit(0) + else: + print ('THERE') + sys.exit(1) diff --git a/powerline/bindings/fish/powerline-setup.fish b/powerline/bindings/fish/powerline-setup.fish new file mode 100644 index 00000000..fad70f7b --- /dev/null +++ b/powerline/bindings/fish/powerline-setup.fish @@ -0,0 +1,103 @@ +function powerline-setup + function _powerline_columns_fallback + if which stty >/dev/null + if stty size >/dev/null + stty size | cut -d' ' -f2 + return 0 + end + end + echo 0 + return 0 + end + + function _powerline_columns + # Hack: `test "" -eq 0` is true, as well as `test 0 -eq 0` + # Note: at fish startup `$COLUMNS` is equal to zero, meaning that it may + # not be used. + if test "$COLUMNS" -eq 0 + _powerline_columns_fallback + else + echo "$COLUMNS" + end + end + + if test -z "$POWERLINE_CONFIG" + if which powerline-config >/dev/null + set -g POWERLINE_CONFIG powerline-config + else + set -g POWERLINE_CONFIG (dirname (status -f))/../../../scripts/powerline-config + end + end + + if eval $POWERLINE_CONFIG shell --shell=fish uses prompt + if test -z "$POWERLINE_COMMAND" + set -g POWERLINE_COMMAND (eval $POWERLINE_CONFIG shell command) + end + function --on-variable fish_bind_mode _powerline_bind_mode + set -g -x _POWERLINE_MODE $fish_bind_mode + end + function --on-variable fish_key_bindings _powerline_set_default_mode + if test x$fish_key_bindings != xfish_vi_key_bindings + set -g -x _POWERLINE_DEFAULT_MODE default + else + set -g -e _POWERLINE_DEFAULT_MODE + end + end + function --on-variable POWERLINE_COMMAND _powerline_update + set -l addargs "--last_exit_code=\$status" + set -l addargs "$addargs --last_pipe_status=\$status" + set -l addargs "$addargs --jobnum=(jobs -p | wc -l)" + # One random value has an 1/32767 = 0.0031% probability of having + # the same value in two shells + set -l addargs "$addargs --renderer_arg=client_id="(random) + set -l addargs "$addargs --width=\$_POWERLINE_COLUMNS" + set -l promptside + set -l rpromptpast + set -l columnsexpr + if test -z "$POWERLINE_NO_FISH_ABOVE$POWERLINE_NO_SHELL_ABOVE" + set promptside aboveleft + set rpromptpast 'echo -n " "' + set columnsexpr '(math (_powerline_columns) - 1)' + else + set promptside left + set rpromptpast + set columnsexpr '(_powerline_columns)' + end + eval " + function fish_prompt + $POWERLINE_COMMAND shell $promptside $addargs + end + function fish_right_prompt + $POWERLINE_COMMAND shell right $addargs + $rpromptpast + end + function --on-signal WINCH _powerline_set_columns + set -g _POWERLINE_COLUMNS $columnsexpr + end + " + _powerline_set_columns + end + _powerline_bind_mode + _powerline_set_default_mode + _powerline_update + end + if eval $POWERLINE_CONFIG shell --shell=fish uses tmux + if test -n "$TMUX" + if tmux refresh -S ^/dev/null + function _powerline_tmux_setenv + tmux setenv -g TMUX_$argv[1]_(tmux display -p "#D" | tr -d "%") "$argv[2]" + tmux refresh -S + end + function --on-variable PWD _powerline_tmux_set_pwd + _powerline_tmux_setenv PWD "$PWD" + end + function --on-signal WINCH _powerline_tmux_set_columns + _powerline_tmux_setenv COLUMNS (_powerline_columns) + end + _powerline_tmux_set_columns + _powerline_tmux_set_pwd + end + end + end +end +# vim: ft=fish diff --git a/powerline/bindings/i3/powerline-i3.py b/powerline/bindings/i3/powerline-i3.py new file mode 100755 index 00000000..c5e01c8a --- /dev/null +++ b/powerline/bindings/i3/powerline-i3.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import time + +from threading import Lock + +import i3 + +from powerline import Powerline +from powerline.lib.monotonic import monotonic + + +if __name__ == '__main__': + name = 'wm' + if len(sys.argv) > 1: + name = sys.argv[1] + + powerline = Powerline(name, renderer_module='i3bar') + powerline.update_renderer() + + interval = 0.5 + + print ('{"version": 1, "custom_workspace": true}') + print ('[') + print ('\t[[],[]]') + + lock = Lock() + + def render(event=None, data=None, sub=None): + global lock + with lock: + s = '[\n' + powerline.render(side='right')[:-2] + '\n]\n' + s += ',[\n' + powerline.render(side='left')[:-2] + '\n]' + print (',[\n' + s + '\n]') + sys.stdout.flush() + + sub = i3.Subscription(render, 'workspace') + + while True: + start_time = monotonic() + render() + time.sleep(max(interval - (monotonic() - start_time), 0.1)) diff --git a/powerline/bindings/ipython/__init__.py b/powerline/bindings/ipython/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline/bindings/ipython/post_0_11.py b/powerline/bindings/ipython/post_0_11.py new file mode 100644 index 00000000..bf2358ac --- /dev/null +++ b/powerline/bindings/ipython/post_0_11.py @@ -0,0 +1,106 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from weakref import ref + +from IPython.core.prompts import PromptManager +from IPython.core.magic import Magics, magics_class, line_magic + +from powerline.ipython import IPythonPowerline, RewriteResult + + +@magics_class +class PowerlineMagics(Magics): + def __init__(self, ip, powerline): + super(PowerlineMagics, self).__init__(ip) + self._powerline = powerline + + @line_magic + def powerline(self, line): + if line == 'reload': + self._powerline.reload() + else: + raise ValueError('Expected `reload`, but got {0}'.format(line)) + + +class IPythonInfo(object): + def __init__(self, shell): + self._shell = shell + + @property + def prompt_count(self): + return self._shell.execution_count + + +class PowerlinePromptManager(PromptManager): + def __init__(self, powerline, shell): + self.powerline = powerline + self.powerline_segment_info = IPythonInfo(shell) + self.shell = shell + + def render(self, name, color=True, *args, **kwargs): + res = self.powerline.render( + is_prompt=name.startswith('in'), + side='left', + output_width=True, + output_raw=not color, + matcher_info=name, + segment_info=self.powerline_segment_info, + ) + self.txtwidth = res[-1] + self.width = res[-1] + ret = res[0] if color else res[1] + if name == 'rewrite': + return RewriteResult(ret) + else: + return ret + + +class ShutdownHook(object): + powerline = lambda: None + + def __call__(self): + from IPython.core.hooks import TryNext + powerline = self.powerline() + if powerline is not None: + powerline.shutdown() + raise TryNext() + + +class ConfigurableIPythonPowerline(IPythonPowerline): + def init(self, ip): + config = ip.config.Powerline + self.config_overrides = config.get('config_overrides') + self.theme_overrides = config.get('theme_overrides', {}) + self.paths = config.get('paths') + super(ConfigurableIPythonPowerline, self).init() + + def do_setup(self, ip, shutdown_hook): + prompt_manager = PowerlinePromptManager( + powerline=self, + shell=ip.prompt_manager.shell, + ) + magics = PowerlineMagics(ip, self) + shutdown_hook.powerline = ref(self) + + ip.prompt_manager = prompt_manager + ip.register_magics(magics) + + +old_prompt_manager = None + + +def load_ipython_extension(ip): + global old_prompt_manager + old_prompt_manager = ip.prompt_manager + + powerline = ConfigurableIPythonPowerline(ip) + shutdown_hook = ShutdownHook() + + powerline.setup(ip, shutdown_hook) + + ip.hooks.shutdown_hook.add(shutdown_hook) + + +def unload_ipython_extension(ip): + ip.prompt_manager = old_prompt_manager diff --git a/powerline/bindings/ipython/pre_0_11.py b/powerline/bindings/ipython/pre_0_11.py new file mode 100644 index 00000000..6e5e356d --- /dev/null +++ b/powerline/bindings/ipython/pre_0_11.py @@ -0,0 +1,146 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re + +from weakref import ref + +from IPython.Prompts import BasePrompt +from IPython.ipapi import get as get_ipython +from IPython.ipapi import TryNext + +from powerline.ipython import IPythonPowerline, RewriteResult +from powerline.lib.unicode import string + + +class IPythonInfo(object): + def __init__(self, cache): + self._cache = cache + + @property + def prompt_count(self): + return self._cache.prompt_count + + +class PowerlinePrompt(BasePrompt): + def __init__(self, powerline, powerline_last_in, old_prompt): + self.powerline = powerline + self.powerline_last_in = powerline_last_in + self.powerline_segment_info = IPythonInfo(old_prompt.cache) + self.cache = old_prompt.cache + if hasattr(old_prompt, 'sep'): + self.sep = old_prompt.sep + self.pad_left = False + + def __str__(self): + self.set_p_str() + return string(self.p_str) + + def set_p_str(self): + self.p_str, self.p_str_nocolor, self.powerline_prompt_width = ( + self.powerline.render( + is_prompt=self.powerline_is_prompt, + side='left', + output_raw=True, + output_width=True, + segment_info=self.powerline_segment_info, + matcher_info=self.powerline_prompt_type, + ) + ) + + @staticmethod + def set_colors(): + pass + + +class PowerlinePrompt1(PowerlinePrompt): + powerline_prompt_type = 'in' + powerline_is_prompt = True + rspace = re.compile(r'(\s*)$') + + def __str__(self): + self.cache.prompt_count += 1 + self.set_p_str() + self.cache.last_prompt = self.p_str_nocolor.split('\n')[-1] + return string(self.p_str) + + def set_p_str(self): + super(PowerlinePrompt1, self).set_p_str() + self.nrspaces = len(self.rspace.search(self.p_str_nocolor).group()) + self.powerline_last_in['nrspaces'] = self.nrspaces + + def auto_rewrite(self): + return RewriteResult(self.powerline.render( + is_prompt=False, + side='left', + matcher_info='rewrite', + segment_info=self.powerline_segment_info) + (' ' * self.nrspaces) + ) + + +class PowerlinePromptOut(PowerlinePrompt): + powerline_prompt_type = 'out' + powerline_is_prompt = False + + def set_p_str(self): + super(PowerlinePromptOut, self).set_p_str() + spaces = ' ' * self.powerline_last_in['nrspaces'] + self.p_str += spaces + self.p_str_nocolor += spaces + + +class PowerlinePrompt2(PowerlinePromptOut): + powerline_prompt_type = 'in2' + powerline_is_prompt = True + + +class ConfigurableIPythonPowerline(IPythonPowerline): + def init(self, config_overrides=None, theme_overrides={}, paths=None): + self.config_overrides = config_overrides + self.theme_overrides = theme_overrides + self.paths = paths + super(ConfigurableIPythonPowerline, self).init() + + def ipython_magic(self, ip, parameter_s=''): + if parameter_s == 'reload': + self.reload() + else: + raise ValueError('Expected `reload`, but got {0}'.format(parameter_s)) + + def do_setup(self, ip, shutdown_hook): + last_in = {'nrspaces': 0} + for attr, prompt_class in ( + ('prompt1', PowerlinePrompt1), + ('prompt2', PowerlinePrompt2), + ('prompt_out', PowerlinePromptOut) + ): + old_prompt = getattr(ip.IP.outputcache, attr) + prompt = prompt_class(self, last_in, old_prompt) + setattr(ip.IP.outputcache, attr, prompt) + ip.expose_magic('powerline', self.ipython_magic) + shutdown_hook.powerline = ref(self) + + +class ShutdownHook(object): + powerline = lambda: None + + def __call__(self): + from IPython.ipapi import TryNext + powerline = self.powerline() + if powerline is not None: + powerline.shutdown() + raise TryNext() + + +def setup(**kwargs): + ip = get_ipython() + + powerline = ConfigurableIPythonPowerline(**kwargs) + shutdown_hook = ShutdownHook() + + def late_startup_hook(): + powerline.setup(ip, shutdown_hook) + raise TryNext() + + ip.IP.hooks.late_startup_hook.add(late_startup_hook) + ip.IP.hooks.shutdown_hook.add(shutdown_hook) diff --git a/powerline/bindings/qtile/__init__.py b/powerline/bindings/qtile/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline/bindings/qtile/widget.py b/powerline/bindings/qtile/widget.py new file mode 100644 index 00000000..83acd9fa --- /dev/null +++ b/powerline/bindings/qtile/widget.py @@ -0,0 +1,37 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from libqtile import bar +from libqtile.widget import base + +from powerline import Powerline as PowerlineCore + + +class Powerline(base._TextBox): + def __init__(self, timeout=2, text=" ", width=bar.CALCULATED, **config): + base._TextBox.__init__(self, text, width, **config) + self.timeout_add(timeout, self.update) + self.powerline = PowerlineCore(ext='wm', renderer_module='pango_markup') + + def update(self): + if not self.configured: + return True + self.text = self.powerline.render(side='right') + self.bar.draw() + return True + + def cmd_update(self, text): + self.update(text) + + def cmd_get(self): + return self.text + + def _configure(self, qtile, bar): + base._TextBox._configure(self, qtile, bar) + self.layout = self.drawer.textlayout( + self.text, + self.foreground, + self.font, + self.fontsize, + self.fontshadow, + markup=True) diff --git a/powerline/bindings/shell/powerline.sh b/powerline/bindings/shell/powerline.sh new file mode 100644 index 00000000..8940c028 --- /dev/null +++ b/powerline/bindings/shell/powerline.sh @@ -0,0 +1,235 @@ +_POWERLINE_SOURCED="$_" +_powerline_columns_fallback() { + if which stty >/dev/null ; then + # Ksh does not have “local” built-in + _powerline_cols="$(stty size 2>/dev/null)" + if ! test -z "$_powerline_cols" ; then + echo "${_powerline_cols#* }" + return 0 + fi + fi + echo 0 + return 0 +} + +_powerline_has_jobs_in_subshell() { + if test -n "$_POWERLINE_HAS_JOBS_IN_SUBSHELL" ; then + return $_POWERLINE_HAS_JOBS_IN_SUBSHELL + elif test -z "$1" ; then + sleep 1 & + # Check whether shell outputs anything in a subshell when using jobs + # built-in. Shells like dash will not output anything meaning that + # I have to bother with temporary files. + test "$(jobs -p|wc -l)" -gt 0 + else + case "$1" in + dash|bb|ash) return 1 ;; + mksh|ksh|bash) return 0 ;; + *) _powerline_has_jobs_in_subshell ;; + esac + fi + _POWERLINE_HAS_JOBS_IN_SUBSHELL=$? + return $_POWERLINE_HAS_JOBS_IN_SUBSHELL +} + +_powerline_set_append_trap() { + if _powerline_has_jobs_in_subshell "$@" ; then + _powerline_append_trap() { + # Arguments: command, signal + # Ksh does not have “local” built-in + _powerline_traps="$(trap)" + if echo "$_powerline_traps" | grep -cm1 $2'$' >/dev/null ; then + _powerline_traps="$(echo "$_powerline_traps" | sed "s/ $2/'\\n$1' $2/")" + eval "$_powerline_traps" + else + trap "$1" $2 + fi + } + else + _powerline_append_trap() { + # Arguments: command, signal + _powerline_create_temp + trap > $_POWERLINE_TEMP + if grep -cm1 $2'$' $_POWERLINE_TEMP >/dev/null ; then + sed -i -e "s/ $2/'\\n$1' $2/" + . $_POWERLINE_TEMP + else + trap "$1" $2 + fi + echo -n > $_POWERLINE_TEMP + } + fi + _powerline_set_append_trap() { + return 0 + } +} + +_powerline_create_temp() { + if test -z "$_POWERLINE_TEMP" || ! test -e "$_POWERLINE_TEMP" ; then + _POWERLINE_TEMP="$(mktemp)" + _powerline_append_trap 'rm $_POWERLINE_TEMP' EXIT + fi +} + +_powerline_set_set_jobs() { + if _powerline_has_jobs_in_subshell "$@" ; then + _powerline_set_jobs() { + _POWERLINE_JOBS="$(jobs -p|wc -l)" + } + else + _powerline_set_append_trap "$@" + _POWERLINE_PID=$$ + _powerline_append_trap '_powerline_do_set_jobs' USR1 + _powerline_do_set_jobs() { + _powerline_create_temp + jobs -p > $_POWERLINE_TEMP + } + # This command will always be launched from a subshell, thus a hack is + # needed to run `jobs -p` outside of the subshell. + _powerline_set_jobs() { + kill -USR1 $_POWERLINE_PID + # Note: most likely this will read data from the previous run. Tests + # show that it is OK for some reasons. + _POWERLINE_JOBS="$(wc -l < $_POWERLINE_TEMP)" + } + fi + _powerline_set_set_jobs() { + return 0 + } +} + +_powerline_set_command() { + if test -z "${POWERLINE_COMMAND}" ; then + POWERLINE_COMMAND="$("$POWERLINE_CONFIG" shell command)" + fi +} + +_powerline_tmux_setenv() { + TMUX="$_POWERLINE_TMUX" tmux setenv -g TMUX_"$1"_`tmux display -p "#D" | tr -d %` "$2" + TMUX="$_POWERLINE_TMUX" tmux refresh -S +} + +_powerline_tmux_set_pwd() { + if test "x$_POWERLINE_SAVED_PWD" != "x$PWD" ; then + _POWERLINE_SAVED_PWD="$PWD" + _powerline_tmux_setenv PWD "$PWD" + fi +} + +_powerline_tmux_set_columns() { + _powerline_tmux_setenv COLUMNS "${COLUMNS:-$(_powerline_columns_fallback)}" +} + +_powerline_set_renderer_arg() { + case "$1" in + bb|ash) _POWERLINE_RENDERER_ARG="-r .bash" ;; + mksh|ksh) _POWERLINE_RENDERER_ARG="-r .ksh" ;; + bash|dash) _POWERLINE_RENDERER_ARG= ;; + esac +} + +_powerline_set_jobs() { + _powerline_set_set_jobs + _powerline_set_jobs +} + +_powerline_local_prompt() { + # Arguments: side, exit_code, local theme + _powerline_set_jobs + $POWERLINE_COMMAND shell $1 \ + $_POWERLINE_RENDERER_ARG \ + --renderer_arg="client_id=$$" \ + --last_exit_code=$2 \ + --jobnum=$_POWERLINE_JOBS \ + --renderer_arg="local_theme=$3" +} + +_powerline_prompt() { + # Arguments: side, exit_code + _powerline_set_jobs + $POWERLINE_COMMAND shell $1 \ + --width="${COLUMNS:-$(_powerline_columns_fallback)}" \ + $_POWERLINE_RENDERER_ARG \ + --renderer_arg="client_id=$$" \ + --last_exit_code=$2 \ + --jobnum=$_POWERLINE_JOBS + _powerline_update_psN +} + +_powerline_setup_psN() { + case "$1" in + mksh|ksh|bash) + _POWERLINE_PID=$$ + _powerline_update_psN() { + kill -USR1 $_POWERLINE_PID + } + # No command substitution in PS2 and PS3 + _powerline_set_psN() { + if test -n "$POWERLINE_SHELL_CONTINUATION" ; then + PS2="$(_powerline_local_prompt left $? continuation)" + fi + if test -n "$POWERLINE_SHELL_SELECT" ; then + PS3="$(_powerline_local_prompt left $? select)" + fi + } + _powerline_append_trap '_powerline_set_psN' USR1 + _powerline_set_psN + ;; + bb|ash|dash) + _powerline_update_psN() { + # Do nothing + return + } + PS2='$(_powerline_local_prompt left $? continuation)' + # No select support + ;; + esac +} + +_powerline_setup_prompt() { + VIRTUAL_ENV_DISABLE_PROMPT=1 + _powerline_set_append_trap "$@" + _powerline_set_set_jobs "$@" + _powerline_set_command "$@" + _powerline_set_renderer_arg "$@" + PS1='$(_powerline_prompt aboveleft $?)' + PS2="$(_powerline_local_prompt left 0 continuation)" + PS3="$(_powerline_local_prompt left 0 select)" + _powerline_setup_psN "$@" +} + +_powerline_init_tmux_support() { + # Dash does not have &>/dev/null + if test -n "$TMUX" && tmux refresh -S >/dev/null 2>/dev/null ; then + # TMUX variable may be unset to create new tmux session inside this one + _POWERLINE_TMUX="$TMUX" + + _powerline_set_append_trap "$@" + + # If _powerline_tmux_set_pwd is used before _powerline_prompt it sets $? + # to zero in ksh. + PS1="$PS1"'$(_powerline_tmux_set_pwd)' + _powerline_append_trap '_powerline_tmux_set_columns' WINCH + _powerline_tmux_set_columns + fi +} + +if test -z "${POWERLINE_CONFIG}" ; then + if which powerline-config >/dev/null ; then + POWERLINE_CONFIG=powerline-config + else + POWERLINE_CONFIG="$(dirname "$_POWERLINE_SOURCED")/../../../scripts/powerline-config" + fi +fi + +# Strips the leading `-`: it may be present when shell is a login shell +_POWERLINE_USED_SHELL=${0#-} +_POWERLINE_USED_SHELL=${_POWERLINE_USED_SHELL#/usr} +_POWERLINE_USED_SHELL=${_POWERLINE_USED_SHELL#/bin/} + +if "${POWERLINE_CONFIG}" shell uses tmux ; then + _powerline_init_tmux_support $_POWERLINE_USED_SHELL +fi +if "${POWERLINE_CONFIG}" shell --shell=bash uses prompt ; then + _powerline_setup_prompt $_POWERLINE_USED_SHELL +fi diff --git a/powerline/bindings/tcsh/powerline.tcsh b/powerline/bindings/tcsh/powerline.tcsh new file mode 100644 index 00000000..fafdb39b --- /dev/null +++ b/powerline/bindings/tcsh/powerline.tcsh @@ -0,0 +1,47 @@ +# http://unix.stackexchange.com/questions/4650/determining-path-to-sourced-shell-script: +# > In tcsh, $_ at the beginning of the script will contain the location if the +# > file was sourced and $0 contains it if it was run. +# +# Guess this relies on `$_` being set as to last argument to previous command +# which must be `.` or `source` in this case +set POWERLINE_SOURCED=($_) +if ! $?POWERLINE_CONFIG then + if ( { which powerline-config > /dev/null } ) then + set POWERLINE_CONFIG="powerline-config" + else + set POWERLINE_CONFIG="$POWERLINE_SOURCED[2]:h:h:h:h/scripts/powerline-config" + endif +else + if "$POWERLINE_CONFIG" == "" then + if ( { which powerline-config > /dev/null } ) then + set POWERLINE_CONFIG="powerline-config" + else + set POWERLINE_CONFIG="$POWERLINE_SOURCED[2]:h:h:h:h/scripts/powerline-config" + endif + endif +endif +if ( { $POWERLINE_CONFIG shell --shell=tcsh uses tmux } ) then + alias _powerline_tmux_set_pwd 'if ( $?TMUX && { tmux refresh -S >&/dev/null } ) tmux setenv -g TMUX_PWD_`tmux display -p "#D" | tr -d %` $PWD:q ; if ( $?TMUX ) tmux refresh -S >&/dev/null' + alias cwdcmd "`alias cwdcmd` ; _powerline_tmux_set_pwd" +endif +if ( { $POWERLINE_CONFIG shell --shell=tcsh uses prompt } ) then + if ! $?POWERLINE_COMMAND then + set POWERLINE_COMMAND="`$POWERLINE_CONFIG:q shell command`" + else + if "$POWERLINE_COMMAND" == "" then + set POWERLINE_COMMAND="`$POWERLINE_CONFIG:q shell command`" + endif + endif + + if ( $?POWERLINE_NO_TCSH_ABOVE || $?POWERLINE_NO_SHELL_ABOVE ) then + alias _powerline_above true + else + alias _powerline_above '$POWERLINE_COMMAND shell above --renderer_arg=client_id=$$ --last_exit_code=$POWERLINE_STATUS --width=$POWERLINE_COLUMNS' + endif + + alias _powerline_set_prompt 'set prompt="`$POWERLINE_COMMAND shell left -r .tcsh --renderer_arg=client_id=$$ --last_exit_code=$POWERLINE_STATUS --width=$POWERLINE_COLUMNS`"' + alias _powerline_set_rprompt 'set rprompt="`$POWERLINE_COMMAND shell right -r .tcsh --renderer_arg=client_id=$$ --last_exit_code=$POWERLINE_STATUS --width=$POWERLINE_COLUMNS` "' + alias _powerline_set_columns 'set POWERLINE_COLUMNS=`stty size|cut -d" " -f2` ; set POWERLINE_COLUMNS=`expr $POWERLINE_COLUMNS - 2`' + + alias precmd 'set POWERLINE_STATUS=$? ; '"`alias precmd`"' ; _powerline_set_columns ; _powerline_above ; _powerline_set_prompt ; _powerline_set_rprompt' +endif diff --git a/powerline/bindings/tmux/__init__.py b/powerline/bindings/tmux/__init__.py new file mode 100644 index 00000000..d56abde4 --- /dev/null +++ b/powerline/bindings/tmux/__init__.py @@ -0,0 +1,52 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re +import os +import subprocess + +from collections import namedtuple + +from powerline.lib.shell import run_cmd + + +TmuxVersionInfo = namedtuple('TmuxVersionInfo', ('major', 'minor', 'suffix')) + + +def get_tmux_executable_name(): + '''Returns tmux executable name + + It should be defined in POWERLINE_TMUX_EXE environment variable, otherwise + it is simply “tmux”. + ''' + + return os.environ.get('POWERLINE_TMUX_EXE', 'tmux') + + +def _run_tmux(runner, args): + return runner([get_tmux_executable_name()] + list(args)) + + +def run_tmux_command(*args): + '''Run tmux command, ignoring the output''' + _run_tmux(subprocess.check_call, args) + + +def get_tmux_output(pl, *args): + '''Run tmux command and return its output''' + return _run_tmux(lambda cmd: run_cmd(pl, cmd), args) + + +NON_DIGITS = re.compile('[^0-9]+') +DIGITS = re.compile('[0-9]+') +NON_LETTERS = re.compile('[^a-z]+') + + +def get_tmux_version(pl): + version_string = get_tmux_output(pl, '-V') + _, version_string = version_string.split(' ') + version_string = version_string.strip() + major, minor = version_string.split('.') + suffix = DIGITS.subn('', minor)[0] or None + minor = NON_DIGITS.subn('', minor)[0] + return TmuxVersionInfo(int(major), int(minor), suffix) diff --git a/powerline/bindings/tmux/powerline.conf b/powerline/bindings/tmux/powerline.conf new file mode 100644 index 00000000..3fb316a3 --- /dev/null +++ b/powerline/bindings/tmux/powerline.conf @@ -0,0 +1,21 @@ +if-shell 'test -z "$POWERLINE_CONFIG_COMMAND"' 'set-environment -g POWERLINE_CONFIG_COMMAND powerline-config' + +# Don't version-check for this core functionality -- anything too old to +# support these options likely won't work well with powerline +set -g status on +set -g status-utf8 on +set -g status-interval 2 +set -g status-left-length 20 +set -g status-right '#(eval $POWERLINE_COMMAND tmux right -R pane_id=`tmux display -p "#D"`)' +set -g status-right-length 150 +set -g window-status-format "#[fg=colour244,bg=colour234] #I #[fg=colour240] #[default]#W " +set -g window-status-current-format "#[fg=colour234,bg=colour31]#[fg=colour117,bg=colour31] #I  #[fg=colour231,bold]#W #[fg=colour31,bg=colour234,nobold]" + +# Legacy status-left definition to be overwritten for tmux Versions 1.8+ +set -g status-left '#[fg=colour16,bg=colour254,bold] #S #[fg=colour254,bg=colour234,nobold]#(eval $POWERLINE_COMMAND tmux left)' + +# Simplify tmux version checking by using multiple config files. Source these +# config files based on the version in which tmux features were added and/or +# deprecated. By splitting these configuration options into separate files, +run-shell 'eval $POWERLINE_CONFIG_COMMAND tmux source' +# vim: ft=tmux diff --git a/powerline/bindings/tmux/powerline_tmux_1.8.conf b/powerline/bindings/tmux/powerline_tmux_1.8.conf new file mode 100644 index 00000000..720206be --- /dev/null +++ b/powerline/bindings/tmux/powerline_tmux_1.8.conf @@ -0,0 +1,5 @@ +# powerline_tmux_1.8.conf +# tmux Version 1.8 introduces window-status-last-{attr,bg,fg}, which is +# deprecated for versions 1.9+, thus only applicable to version 1.8. +set -qg window-status-last-fg colour31 +# vim: ft=tmux diff --git a/powerline/bindings/tmux/powerline_tmux_1.8_minus.conf b/powerline/bindings/tmux/powerline_tmux_1.8_minus.conf new file mode 100644 index 00000000..6baed844 --- /dev/null +++ b/powerline/bindings/tmux/powerline_tmux_1.8_minus.conf @@ -0,0 +1,11 @@ +# powerline_tmux_legacy_common.conf +# tmux Version 1.8 and earlier (legacy) common options. The foo-{attr,bg,fg} +# options are deprecated starting with tmux Version 1.9. +set -g status-fg colour231 +set -g status-bg colour234 +set-window-option -g window-status-fg colour249 +set-window-option -g window-status-activity-attr none +set-window-option -g window-status-bell-attr none +set-window-option -g window-status-activity-fg yellow +set-window-option -g window-status-bell-fg red +# vim: ft=tmux diff --git a/powerline/bindings/tmux/powerline_tmux_1.8_plus.conf b/powerline/bindings/tmux/powerline_tmux_1.8_plus.conf new file mode 100644 index 00000000..1ad9cdb3 --- /dev/null +++ b/powerline/bindings/tmux/powerline_tmux_1.8_plus.conf @@ -0,0 +1,5 @@ +# powerline_tmux_1.8_plus.conf +# tmux Version 1.8 introduces the 'client_prefix' format variable, applicable +# for versions 1.8+ +set -qg status-left '#{?client_prefix,#[fg=colour254]#[bg=colour31]#[bold],#[fg=colour16]#[bg=colour254]#[bold]} #S #{?client_prefix,#[fg=colour31]#[bg=colour234]#[nobold],#[fg=colour254]#[bg=colour234]#[nobold]}#(eval $POWERLINE_COMMAND tmux left)' +# vim: ft=tmux diff --git a/powerline/bindings/tmux/powerline_tmux_1.9_plus.conf b/powerline/bindings/tmux/powerline_tmux_1.9_plus.conf new file mode 100644 index 00000000..b053b6ee --- /dev/null +++ b/powerline/bindings/tmux/powerline_tmux_1.9_plus.conf @@ -0,0 +1,8 @@ +# powerline_tmux_1.9_plus.conf +# Version 1.9 introduces the foo-style options, applicable to version 1.9+ +set -qg status-style fg=colour231,bg=colour234 +set -qg window-status-last-style fg=colour31 +set-window-option -qg window-status-style fg=colour249 +set-window-option -qg window-status-activity-style fg=yellow,none +set-window-option -qg window-status-bell-style fg=red,none +# vim: ft=tmux diff --git a/powerline/bindings/vim/__init__.py b/powerline/bindings/vim/__init__.py new file mode 100644 index 00000000..4b38897a --- /dev/null +++ b/powerline/bindings/vim/__init__.py @@ -0,0 +1,315 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import codecs + +try: + import vim +except ImportError: + vim = {} + +if not hasattr(vim, 'bindeval'): + import json + + +if hasattr(vim, 'bindeval'): + def vim_get_func(f, rettype=None): + '''Return a vim function binding.''' + try: + func = vim.bindeval('function("' + f + '")') + if sys.version_info >= (3,) and rettype is str: + return (lambda *args, **kwargs: func(*args, **kwargs).decode('utf-8', errors='replace')) + return func + except vim.error: + return None +else: + class VimFunc(object): + '''Evaluate a vim function using vim.eval(). + + This is a fallback class for older vim versions. + ''' + __slots__ = ('f', 'rettype') + + def __init__(self, f, rettype=None): + self.f = f + self.rettype = rettype + + def __call__(self, *args): + r = vim.eval(self.f + '(' + json.dumps(args)[1:-1] + ')') + if self.rettype: + return self.rettype(r) + return r + + vim_get_func = VimFunc + + +_getbufvar = vim_get_func('getbufvar') + + +# It may crash on some old vim versions and I do not remember in which patch +# I fixed this crash. +if hasattr(vim, 'vvars') and vim.vvars['version'] > 703: + _vim_to_python_types = { + getattr(vim, 'Dictionary', None) or type(vim.bindeval('{}')): + lambda value: dict(((key, _vim_to_python(value[key])) for key in value.keys())), + getattr(vim, 'List', None) or type(vim.bindeval('[]')): + lambda value: [_vim_to_python(item) for item in value], + getattr(vim, 'Function', None) or type(vim.bindeval('function("mode")')): + lambda _: None, + } + + def vim_getvar(varname): + return _vim_to_python(vim.vars[str(varname)]) + + def bufvar_exists(buffer, varname): + buffer = buffer or vim.current.buffer + return varname in buffer.vars + + def vim_getwinvar(segment_info, varname): + return _vim_to_python(segment_info['window'].vars[str(varname)]) +else: + _vim_to_python_types = { + dict: (lambda value: dict(((k, _vim_to_python(v)) for k, v in value.items()))), + list: (lambda value: [_vim_to_python(i) for i in value]), + } + + _vim_exists = vim_get_func('exists', rettype=int) + + def vim_getvar(varname): + varname = 'g:' + varname + if _vim_exists(varname): + return vim.eval(varname) + else: + raise KeyError(varname) + + def bufvar_exists(buffer, varname): + if not buffer or buffer.number == vim.current.buffer.number: + return int(vim.eval('exists("b:{0}")'.format(varname))) + else: + return int(vim.eval( + 'has_key(getbufvar({0}, ""), {1})'.format(buffer.number, varname) + )) + + def vim_getwinvar(segment_info, varname): + result = vim.eval('getwinvar({0}, "{1}")'.format(segment_info['winnr'], varname)) + if result == '': + if not int(vim.eval('has_key(getwinvar({0}, ""), "{1}")'.format(segment_info['winnr'], varname))): + raise KeyError(varname) + return result + + +if sys.version_info < (3,): + getbufvar = _getbufvar +else: + _vim_to_python_types[bytes] = lambda value: value.decode('utf-8') + + def getbufvar(*args): + return _vim_to_python(_getbufvar(*args)) + + +_id = lambda value: value + + +def _vim_to_python(value): + return _vim_to_python_types.get(type(value), _id)(value) + + +if hasattr(vim, 'options'): + def vim_getbufoption(info, option): + return info['buffer'].options[str(option)] + + def vim_getoption(option): + return vim.options[str(option)] + + def vim_setoption(option, value): + vim.options[str(option)] = value +else: + def vim_getbufoption(info, option): + return getbufvar(info['bufnr'], '&' + option) + + def vim_getoption(option): + return vim.eval('&g:' + option) + + def vim_setoption(option, value): + vim.command('let &g:{option} = {value}'.format( + option=option, value=json.encode(value))) + + +if hasattr(vim, 'tabpages'): + current_tabpage = lambda: vim.current.tabpage + list_tabpages = lambda: vim.tabpages + + def list_tabpage_buffers_segment_info(segment_info): + return ( + {'buffer': window.buffer, 'bufnr': window.buffer.number} + for window in segment_info['tabpage'].windows + ) +else: + class FalseObject(object): + @staticmethod + def __nonzero__(): + return False + + __bool__ = __nonzero__ + + def get_buffer(number): + for buffer in vim.buffers: + if buffer.number == number: + return buffer + raise KeyError(number) + + class WindowVars(object): + __slots__ = ('tabnr', 'winnr') + + def __init__(self, window): + self.tabnr = window.tabnr + self.winnr = window.number + + def __getitem__(self, key): + has_key = vim.eval('has_key(gettabwinvar({0}, {1}, ""), "{2}")'.format(self.tabnr, self.winnr, key)) + if has_key == '0': + raise KeyError + return vim.eval('gettabwinvar({0}, {1}, "{2}")'.format(self.tabnr, self.winnr, key)) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + class Window(FalseObject): + __slots__ = ('tabnr', 'number', '_vars') + + def __init__(self, tabnr, number): + self.tabnr = tabnr + self.number = number + self.vars = WindowVars(self) + + @property + def buffer(self): + return get_buffer(int(vim.eval('tabpagebuflist({0})[{1}]'.format(self.tabnr, self.number - 1)))) + + class Tabpage(FalseObject): + __slots__ = ('number',) + + def __init__(self, number): + self.number = number + + def __eq__(self, tabpage): + if not isinstance(tabpage, Tabpage): + raise NotImplementedError + return self.number == tabpage.number + + @property + def window(self): + return Window(self.number, int(vim.eval('tabpagewinnr({0})'.format(self.number)))) + + def _last_tab_nr(): + return int(vim.eval('tabpagenr("$")')) + + def current_tabpage(): + return Tabpage(int(vim.eval('tabpagenr()'))) + + def list_tabpages(): + return [Tabpage(nr) for nr in range(1, _last_tab_nr() + 1)] + + class TabBufSegmentInfo(dict): + def __getitem__(self, key): + try: + return super(TabBufSegmentInfo, self).__getitem__(key) + except KeyError: + if key != 'buffer': + raise + else: + buffer = get_buffer(super(TabBufSegmentInfo, self).__getitem__('bufnr')) + self['buffer'] = buffer + return buffer + + def list_tabpage_buffers_segment_info(segment_info): + return ( + TabBufSegmentInfo(bufnr=int(bufnrstr)) + for bufnrstr in vim.eval('tabpagebuflist({0})'.format(segment_info['tabnr'])) + ) + + +class VimEnviron(object): + @staticmethod + def __getitem__(key): + return vim.eval('$' + key) + + @staticmethod + def get(key, default=None): + return vim.eval('$' + key) or default + + @staticmethod + def __setitem__(key, value): + return vim.command( + 'let ${0}="{1}"'.format( + key, + value.replace('"', '\\"') + .replace('\\', '\\\\') + .replace('\n', '\\n') + .replace('\0', '') + ) + ) + + +if sys.version_info < (3,): + def buffer_name(buf): + return buf.name +else: + vim_bufname = vim_get_func('bufname') + + def buffer_name(buf): + try: + name = buf.name + except UnicodeDecodeError: + return vim_bufname(buf.number) + else: + return name.encode('utf-8') if name else None + + +vim_strtrans = vim_get_func('strtrans') + + +def powerline_vim_strtrans_error(e): + if not isinstance(e, UnicodeDecodeError): + raise NotImplementedError + # Assuming &encoding is utf-8 strtrans should not return anything but ASCII + # under current circumstances + text = vim_strtrans(e.object[e.start:e.end]).decode() + return (text, e.end) + + +codecs.register_error('powerline_vim_strtrans_error', powerline_vim_strtrans_error) + + +did_autocmd = False +buffer_caches = [] + + +def register_buffer_cache(cachedict): + global did_autocmd + global buffer_caches + from powerline.vim import get_default_pycmd, pycmd + if not did_autocmd: + import __main__ + __main__.powerline_on_bwipe = on_bwipe + vim.command('augroup Powerline') + vim.command(' autocmd! BufWipeout * :{pycmd} powerline_on_bwipe()'.format( + pycmd=(pycmd or get_default_pycmd()))) + vim.command('augroup END') + did_autocmd = True + buffer_caches.append(cachedict) + return cachedict + + +def on_bwipe(): + global buffer_caches + bufnr = int(vim.eval('expand("")')) + for cachedict in buffer_caches: + cachedict.pop(bufnr, None) + + +environ = VimEnviron() diff --git a/powerline/bindings/vim/autoload/powerline/debug.vim b/powerline/bindings/vim/autoload/powerline/debug.vim new file mode 100644 index 00000000..244319ac --- /dev/null +++ b/powerline/bindings/vim/autoload/powerline/debug.vim @@ -0,0 +1,20 @@ +python import cProfile +python powerline_pr = cProfile.Profile() + +function powerline#debug#profile_pyeval(s) + python powerline_pr.enable() + try + let ret = pyeval(a:s) + finally + python powerline_pr.disable() + endtry + return ret +endfunction + +function powerline#debug#write_profile(fname) + python import vim + python powerline_pr.dump_stats(vim.eval('a:fname')) + python powerline_pr = cProfile.Profile() +endfunction + +command -nargs=1 -complete=file WriteProfiling :call powerline#debug#write_profile() diff --git a/powerline/bindings/vim/plugin/powerline.vim b/powerline/bindings/vim/plugin/powerline.vim new file mode 100644 index 00000000..5ec007c0 --- /dev/null +++ b/powerline/bindings/vim/plugin/powerline.vim @@ -0,0 +1,154 @@ +if exists('g:powerline_loaded') + finish +endif +let g:powerline_loaded = 1 + +if exists('g:powerline_pycmd') + let s:pycmd = substitute(g:powerline_pycmd, '\v\C^(py)%[thon](3?)$', '\1\2', '') + if s:pycmd is# 'py' + let s:has_python = has('python') + let s:pyeval = get(g:, 'powerline_pyeval', 'pyeval') + elseif s:pycmd is# 'py3' + let s:has_python = has('python3') + let s:pyeval = 'py3eval' + let s:pyeval = get(g:, 'powerline_pyeval', 'py3eval') + else + if !exists('g:powerline_pyeval') + echohl ErrorMsg + echomsg 'g:powerline_pycmd was set to an unknown values, but g:powerline_pyeval' + echomsg 'was not set. You should either set g:powerline_pycmd to "py3" or "py",' + echomsg 'specify g:powerline_pyeval explicitly or unset both and let powerline' + echomsg 'figure them out.' + echohl None + unlet s:pycmd + finish + endif + let s:pyeval = g:powerline_pyeval + let s:has_python = 1 + endif +elseif has('python') + let s:has_python = 1 + let s:pycmd = 'py' + let s:pyeval = get(g:, 'powerline_pyeval', 'pyeval') +elseif has('python3') + let s:has_python = 1 + let s:pycmd = 'py3' + let s:pyeval = get(g:, 'powerline_pyeval', 'py3eval') +else + let s:has_python = 0 +endif + +if !s:has_python + if !exists('g:powerline_no_python_error') + echohl ErrorMsg + echomsg 'You need vim compiled with Python 2.6, 2.7 or 3.2 and later support' + echomsg 'for Powerline to work. Please consult the documentation for more' + echomsg 'details.' + echohl None + endif + unlet s:has_python + finish +endif +unlet s:has_python + +let s:import_cmd = 'from powerline.vim import VimPowerline' +function s:rcmd(s) + if !exists('s:pystr') + let s:pystr = a:s . "\n" + else + let s:pystr = s:pystr . a:s . "\n" + endif +endfunction +try + let s:can_replace_pyeval = !exists('g:powerline_pyeval') + call s:rcmd("try:") + call s:rcmd(" powerline_appended_path = None") + call s:rcmd(" try:") + call s:rcmd(" ".s:import_cmd."") + call s:rcmd(" except ImportError:") + call s:rcmd(" import sys, vim") + call s:rcmd(" powerline_appended_path = vim.eval('expand(\":h:h:h:h:h\")')") + call s:rcmd(" sys.path.append(powerline_appended_path)") + call s:rcmd(" ".s:import_cmd."") + call s:rcmd(" import vim") + call s:rcmd(" VimPowerline().setup(pyeval=vim.eval('s:pyeval'), pycmd=vim.eval('s:pycmd'), can_replace_pyeval=int(vim.eval('s:can_replace_pyeval')))") + call s:rcmd(" del VimPowerline") + call s:rcmd("except Exception:") + call s:rcmd(" import traceback, sys") + call s:rcmd(" traceback.print_exc(file=sys.stdout)") + call s:rcmd(" raise") + execute s:pycmd s:pystr + unlet s:pystr + let s:launched = 1 +finally + unlet s:can_replace_pyeval + unlet s:import_cmd + if !exists('s:launched') + unlet s:pystr + echohl ErrorMsg + echomsg 'An error occurred while importing powerline package.' + echomsg 'This could be caused by invalid sys.path setting,' + echomsg 'or by an incompatible Python version (powerline requires' + echomsg 'Python 2.6, 2.7 or 3.2 and later to work). Please consult' + echomsg 'the troubleshooting section in the documentation for' + echomsg 'possible solutions.' + if s:pycmd is# 'py' && has('python3') + echomsg 'If powerline on your system is installed for python 3 only you' + echomsg 'should set g:powerline_pycmd to "py3" to make it load correctly.' + endif + echohl None + call s:rcmd("def powerline_troubleshoot():") + call s:rcmd(" import sys") + call s:rcmd(" import vim") + call s:rcmd(" if sys.version_info < (2, 6):") + call s:rcmd(" print('Too old python version: ' + sys.version + ' (first supported is 2.6)')") + call s:rcmd(" elif sys.version_info[0] == 3 and sys.version_info[1] < 2:") + call s:rcmd(" print('Too old python 3 version: ' + sys.version + ' (first supported is 3.2)')") + call s:rcmd(" try:") + call s:rcmd(" import powerline") + call s:rcmd(" except ImportError:") + call s:rcmd(" print('Unable to import powerline, is it installed?')") + call s:rcmd(" else:") + call s:rcmd(" if not vim.eval('expand(\"\")').startswith('/usr/'):") + call s:rcmd(" import os") + call s:rcmd(" powerline_dir = os.path.realpath(os.path.normpath(powerline.__file__))") + call s:rcmd(" powerline_dir = os.path.dirname(powerline.__file__)") + call s:rcmd(" this_dir = os.path.realpath(os.path.normpath(vim.eval('expand(\":p\")')))") + call s:rcmd(" this_dir = os.path.dirname(this_dir)") " powerline/bindings/vim/plugin + call s:rcmd(" this_dir = os.path.dirname(this_dir)") " powerline/bindings/vim + call s:rcmd(" this_dir = os.path.dirname(this_dir)") " powerline/bindings + call s:rcmd(" this_dir = os.path.dirname(this_dir)") " powerline + call s:rcmd(" if os.path.basename(this_dir) != 'powerline':") + call s:rcmd(" print('Check your installation:')") + call s:rcmd(" print('this script is not in powerline[/bindings/vim/plugin] directory,')") + call s:rcmd(" print('neither it is installed system-wide')") + call s:rcmd(" real_powerline_dir = os.path.realpath(powerline_dir)") + call s:rcmd(" real_this_dir = os.path.realpath(this_dir)") + call s:rcmd(" this_dir_par = os.path.dirname(real_this_dir)") + call s:rcmd(" powerline_appended_path = globals().get('powerline_appended_path')") + call s:rcmd(" if powerline_appended_path is not None and this_dir_par != powerline_appended_path:") + call s:rcmd(" print('Check your installation: this script is symlinked somewhere')") + call s:rcmd(" print('where powerline is not present: {0!r} != {1!r}.'.format(") + call s:rcmd(" real_this_dir, powerline_appended_path))") + call s:rcmd(" elif real_powerline_dir != real_this_dir:") + call s:rcmd(" print('It appears that you have two powerline versions installed:')") + call s:rcmd(" print('one in ' + real_powerline_dir + ', other in ' + real_this_dir + '.')") + call s:rcmd(" print('You should remove one of this. Check out troubleshooting section,')") + call s:rcmd(" print('it contains some information about the alternatives.')") + call s:rcmd("try:") + call s:rcmd(" powerline_troubleshoot()") + call s:rcmd("finally:") + call s:rcmd(" del powerline_troubleshoot") + execute s:pycmd s:pystr + unlet s:pystr + unlet s:pycmd + unlet s:pyeval + delfunction s:rcmd + finish + else + unlet s:launched + endif + unlet s:pycmd + unlet s:pyeval + delfunction s:rcmd +endtry diff --git a/powerline/bindings/zsh/__init__.py b/powerline/bindings/zsh/__init__.py new file mode 100644 index 00000000..46940c80 --- /dev/null +++ b/powerline/bindings/zsh/__init__.py @@ -0,0 +1,186 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import atexit + +from weakref import WeakValueDictionary, ref + +import zsh + +from powerline.shell import ShellPowerline +from powerline.lib import parsedotval +from powerline.lib.unicode import unicode + + +used_powerlines = WeakValueDictionary() + + +def shutdown(): + for powerline in tuple(used_powerlines.values()): + powerline.shutdown() + + +def get_var_config(var): + try: + return [parsedotval(i) for i in zsh.getvalue(var).items()] + except: + return None + + +class Args(object): + __slots__ = ('last_pipe_status', 'last_exit_code') + ext = ['shell'] + renderer_module = '.zsh' + + @property + def config(self): + try: + return get_var_config('POWERLINE_CONFIG') + except IndexError: + return None + + @property + def theme_option(self): + try: + return get_var_config('POWERLINE_THEME_CONFIG') + except IndexError: + return None + + @property + def config_path(self): + try: + ret = zsh.getvalue('POWERLINE_CONFIG_PATHS') + except IndexError: + return None + else: + if isinstance(ret, (unicode, str, bytes)): + return ret.split(type(ret)(':')) + else: + return ret + + @property + def jobnum(self): + return zsh.getvalue('_POWERLINE_JOBNUM') + + +def string(s): + if type(s) is bytes: + return s.decode('utf-8', 'replace') + else: + return str(s) + + +class Environment(object): + @staticmethod + def __getitem__(key): + try: + return string(zsh.getvalue(key)) + except IndexError as e: + raise KeyError(*e.args) + + @staticmethod + def get(key, default=None): + try: + return string(zsh.getvalue(key)) + except IndexError: + return default + + @staticmethod + def __contains__(key): + try: + zsh.getvalue(key) + return True + except IndexError: + return False + + +environ = Environment() + + +class ZshPowerline(ShellPowerline): + def init(self, **kwargs): + super(ZshPowerline, self).init(Args(), **kwargs) + + def precmd(self): + self.args.last_pipe_status = zsh.pipestatus() + self.args.last_exit_code = zsh.last_exit_code() + + def do_setup(self, zsh_globals): + set_prompt(self, 'PS1', 'left', None, above=True) + set_prompt(self, 'RPS1', 'right', None) + set_prompt(self, 'PS2', 'left', 'continuation') + set_prompt(self, 'RPS2', 'right', 'continuation') + set_prompt(self, 'PS3', 'left', 'select') + used_powerlines[id(self)] = self + zsh_globals['_powerline'] = self + + +class Prompt(object): + __slots__ = ('powerline', 'side', 'savedpsvar', 'savedps', 'args', 'theme', 'above', '__weakref__') + + def __init__(self, powerline, side, theme, savedpsvar=None, savedps=None, above=False): + self.powerline = powerline + self.side = side + self.above = above + self.savedpsvar = savedpsvar + self.savedps = savedps + self.args = powerline.args + self.theme = theme + + def __str__(self): + zsh.eval('_POWERLINE_PARSER_STATE="${(%):-%_}"') + segment_info = { + 'args': self.args, + 'environ': environ, + 'client_id': 1, + 'local_theme': self.theme, + 'parser_state': zsh.getvalue('_POWERLINE_PARSER_STATE'), + 'shortened_path': zsh.getvalue('_POWERLINE_SHORTENED_PATH'), + } + r = '' + if self.above: + for line in self.powerline.render_above_lines( + width=zsh.columns() - 1, + segment_info=segment_info, + ): + r += line + '\n' + r += self.powerline.render( + width=zsh.columns(), + side=self.side, + segment_info=segment_info, + ) + if type(r) is not str: + if type(r) is bytes: + return r.decode('utf-8') + else: + return r.encode('utf-8') + return r + + def __del__(self): + if self.savedps: + zsh.setvalue(self.savedpsvar, self.savedps) + self.powerline.shutdown() + + +def set_prompt(powerline, psvar, side, theme, above=False): + try: + savedps = zsh.getvalue(psvar) + except IndexError: + savedps = None + zpyvar = 'ZPYTHON_POWERLINE_' + psvar + prompt = Prompt(powerline, side, theme, psvar, savedps, above) + zsh.eval('unset ' + zpyvar) + zsh.set_special_string(zpyvar, prompt) + zsh.setvalue(psvar, '${' + zpyvar + '}') + return ref(prompt) + + +def reload(): + for powerline in tuple(used_powerlines.values()): + powerline.reload() + + +def setup(zsh_globals): + powerline = ZshPowerline() + powerline.setup(zsh_globals) + atexit.register(shutdown) diff --git a/powerline/bindings/zsh/powerline.zsh b/powerline/bindings/zsh/powerline.zsh new file mode 100644 index 00000000..0c097b66 --- /dev/null +++ b/powerline/bindings/zsh/powerline.zsh @@ -0,0 +1,204 @@ +_POWERLINE_SOURCED="$0:A" + +_powerline_columns_fallback() { + if which stty &>/dev/null ; then + local cols="$(stty size 2>/dev/null)" + if ! test -z "$cols" ; then + echo "${cols#* }" + return 0 + fi + fi + echo 0 + return 0 +} + +integer _POWERLINE_JOBNUM + +_powerline_init_tmux_support() { + emulate -L zsh + if test -n "$TMUX" && tmux refresh -S &>/dev/null ; then + # TMUX variable may be unset to create new tmux session inside this one + typeset -g _POWERLINE_TMUX="$TMUX" + + function -g _powerline_tmux_setenv() { + emulate -L zsh + local -x TMUX="$_POWERLINE_TMUX" + tmux setenv -g TMUX_"$1"_$(tmux display -p "#D" | tr -d %) "$2" + tmux refresh -S + } + + function -g _powerline_tmux_set_pwd() { + _powerline_tmux_setenv PWD "$PWD" + } + + function -g _powerline_tmux_set_columns() { + _powerline_tmux_setenv COLUMNS "${COLUMNS:-$(_powerline_columns_fallback)}" + } + + chpwd_functions+=( _powerline_tmux_set_pwd ) + trap "_powerline_tmux_set_columns" SIGWINCH + _powerline_tmux_set_columns + _powerline_tmux_set_pwd + fi +} + +_powerline_init_modes_support() { + emulate -L zsh + + test -z "$ZSH_VERSION" && return 0 + + typeset -ga VS + VS=( ${(s:.:)ZSH_VERSION} ) + + # Mode support requires >=zsh-4.3.11 + if (( VS[1] < 4 || (VS[1] == 4 && (VS[2] < 3 || (VS[2] == 3 && VS[3] < 11))) )) ; then + return 0 + fi + + function -g _powerline_get_main_keymap_name() { + REPLY="${${(Q)${${(z)${"$(bindkey -lL main)"}}[3]}}:-.safe}" + } + + function -g _powerline_set_true_keymap_name() { + export _POWERLINE_MODE="${1}" + local plm_bk="$(bindkey -lL ${_POWERLINE_MODE})" + if [[ $plm_bk = 'bindkey -A'* ]] ; then + _powerline_set_true_keymap_name ${(Q)${${(z)plm_bk}[3]}} + fi + } + + function -g _powerline_zle_keymap_select() { + _powerline_set_true_keymap_name $KEYMAP + zle reset-prompt + test -z "$_POWERLINE_SAVE_WIDGET" || zle $_POWERLINE_SAVE_WIDGET + } + + function -g _powerline_set_main_keymap_name() { + local REPLY + _powerline_get_main_keymap_name + _powerline_set_true_keymap_name "$REPLY" + } + + _powerline_add_widget zle-keymap-select _powerline_zle_keymap_select + _powerline_set_main_keymap_name + + if [[ "$_POWERLINE_MODE" != vi* ]] ; then + export _POWERLINE_DEFAULT_MODE="$_POWERLINE_MODE" + fi + + precmd_functions+=( _powerline_set_main_keymap_name ) +} + +_powerline_set_jobnum() { + # If you are wondering why I am not using the same code as I use for bash + # ($(jobs|wc -l)): consider the following test: + # echo abc | less + # + # . This way jobs will print + # [1] + done echo abc | + # suspended less -M + # ([ is in first column). You see: any line counting thingie will return + # wrong number of jobs. You need to filter the lines first. Or not use + # jobs built-in at all. + _POWERLINE_JOBNUM=${(%):-%j} +} + +_powerline_set_shortened_path() { + _POWERLINE_SHORTENED_PATH="${(%):-%~}" +} + +_powerline_update_counter() { + zpython '_powerline.precmd()' +} + +_powerline_setup_prompt() { + emulate -L zsh + + for f in "${precmd_functions[@]}"; do + if [[ "$f" = "_powerline_set_jobnum" ]]; then + return + fi + done + precmd_functions+=( _powerline_set_jobnum ) + chpwd_functions+=( _powerline_set_shortened_path ) + _powerline_set_shortened_path + + VIRTUAL_ENV_DISABLE_PROMPT=1 + + if test -z "${POWERLINE_NO_ZSH_ZPYTHON}" && { zmodload libzpython || zmodload zsh/zpython } &>/dev/null ; then + precmd_functions+=( _powerline_update_counter ) + zpython 'from powerline.bindings.zsh import setup as _powerline_setup' + zpython '_powerline_setup(globals())' + zpython 'del _powerline_setup' + powerline-reload() { + zpython 'from powerline.bindings.zsh import reload as _powerline_reload' + zpython '_powerline_reload()' + zpython 'del _powerline_reload' + } + else + if test -z "${POWERLINE_COMMAND}" ; then + POWERLINE_COMMAND="$($POWERLINE_CONFIG shell command)" + fi + + local add_args='-r .zsh' + add_args+=' --last_exit_code=$?' + add_args+=' --last_pipe_status="$pipestatus"' + add_args+=' --renderer_arg="client_id=$$"' + add_args+=' --renderer_arg="shortened_path=$_POWERLINE_SHORTENED_PATH"' + add_args+=' --jobnum=$_POWERLINE_JOBNUM' + local new_args_2=' --renderer_arg="parser_state=${(%%):-%_}"' + new_args_2+=' --renderer_arg="local_theme=continuation"' + local add_args_3=$add_args' --renderer_arg="local_theme=select"' + local add_args_2=$add_args$new_args_2 + add_args+=' --width=$(( ${COLUMNS:-$(_powerline_columns_fallback)} - 1 ))' + local add_args_r2=$add_args$new_args_2 + PS1='$($=POWERLINE_COMMAND shell aboveleft '$add_args')' + RPS1='$($=POWERLINE_COMMAND shell right '$add_args')' + PS2='$($=POWERLINE_COMMAND shell left '$add_args_2')' + RPS2='$($=POWERLINE_COMMAND shell right '$add_args_r2')' + PS3='$($=POWERLINE_COMMAND shell left '$add_args_3')' + fi +} + +_powerline_add_widget() { + local widget="$1" + local function="$2" + local old_widget_command="$(zle -l -L $widget)" + if [[ "$old_widget_command" = "zle -N $widget $function" ]] ; then + return 0 + elif [[ -z "$old_widget_command" ]] ; then + zle -N $widget $function + else + local save_widget="_powerline_save_$widget" + local -i i=0 + while ! test -z "$(zle -l -L $save_widget)" ; do + save_widget="${save_widget}_$i" + (( i++ )) + done + # If widget was defined with `zle -N widget` (without `function` + # argument) then this function will be handy. + eval "function $save_widget() { emulate -L zsh; $widget \$@ }" + eval "${old_widget_command/$widget/$save_widget}" + zle -N $widget $function + export _POWERLINE_SAVE_WIDGET="$save_widget" + fi +} + +if test -z "${POWERLINE_CONFIG}" ; then + if which powerline-config >/dev/null ; then + export POWERLINE_CONFIG=powerline-config + else + export POWERLINE_CONFIG="$_POWERLINE_SOURCED:h:h:h:h/scripts/powerline-config" + fi +fi + +setopt promptpercent +setopt promptsubst + +if ${POWERLINE_CONFIG} shell --shell=zsh uses prompt ; then + _powerline_setup_prompt + _powerline_init_modes_support +fi +if ${POWERLINE_CONFIG} shell --shell=zsh uses tmux ; then + _powerline_init_tmux_support +fi diff --git a/powerline/colorscheme.py b/powerline/colorscheme.py new file mode 100644 index 00000000..8d718863 --- /dev/null +++ b/powerline/colorscheme.py @@ -0,0 +1,147 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from copy import copy + +from powerline.lib.unicode import unicode + + +DEFAULT_MODE_KEY = None +ATTR_BOLD = 1 +ATTR_ITALIC = 2 +ATTR_UNDERLINE = 4 + + +def get_attr_flag(attributes): + '''Convert an attribute array to a renderer flag.''' + attr_flag = 0 + if 'bold' in attributes: + attr_flag |= ATTR_BOLD + if 'italic' in attributes: + attr_flag |= ATTR_ITALIC + if 'underline' in attributes: + attr_flag |= ATTR_UNDERLINE + return attr_flag + + +def pick_gradient_value(grad_list, gradient_level): + '''Given a list of colors and gradient percent, return a color that should be used. + + Note: gradient level is not checked for being inside [0, 100] interval. + ''' + return grad_list[int(round(gradient_level * (len(grad_list) - 1) / 100))] + + +class Colorscheme(object): + def __init__(self, colorscheme_config, colors_config): + '''Initialize a colorscheme.''' + self.colors = {} + self.gradients = {} + + self.groups = colorscheme_config['groups'] + self.translations = colorscheme_config.get('mode_translations', {}) + + # Create a dict of color tuples with both a cterm and hex value + for color_name, color in colors_config['colors'].items(): + try: + self.colors[color_name] = (color[0], int(color[1], 16)) + except TypeError: + self.colors[color_name] = (color, cterm_to_hex[color]) + + # Create a dict of gradient names with two lists: for cterm and hex + # values. Two lists in place of one list of pairs were chosen because + # true colors allow more precise gradients. + for gradient_name, gradient in colors_config['gradients'].items(): + if len(gradient) == 2: + self.gradients[gradient_name] = ( + (gradient[0], [int(color, 16) for color in gradient[1]])) + else: + self.gradients[gradient_name] = ( + (gradient[0], [cterm_to_hex[color] for color in gradient[0]])) + + def get_gradient(self, gradient, gradient_level): + if gradient in self.gradients: + return tuple((pick_gradient_value(grad_list, gradient_level) for grad_list in self.gradients[gradient])) + else: + return self.colors[gradient] + + def get_group_props(self, mode, trans, group, translate_colors=True): + if isinstance(group, (str, unicode)): + try: + group_props = trans['groups'][group] + except KeyError: + try: + group_props = self.groups[group] + except KeyError: + return None + else: + return self.get_group_props(mode, trans, group_props, True) + else: + return self.get_group_props(mode, trans, group_props, False) + else: + if translate_colors: + group_props = copy(group) + try: + ctrans = trans['colors'] + except KeyError: + pass + else: + for key in ('fg', 'bg'): + try: + group_props[key] = ctrans[group_props[key]] + except KeyError: + pass + return group_props + else: + return group + + def get_highlighting(self, groups, mode, gradient_level=None): + trans = self.translations.get(mode, {}) + for group in groups: + group_props = self.get_group_props(mode, trans, group) + if group_props: + break + else: + raise KeyError('Highlighting groups not found in colorscheme: ' + ', '.join(groups)) + + if gradient_level is None: + pick_color = self.colors.__getitem__ + else: + pick_color = lambda gradient: self.get_gradient(gradient, gradient_level) + + return { + 'fg': pick_color(group_props['fg']), + 'bg': pick_color(group_props['bg']), + 'attr': get_attr_flag(group_props.get('attr', [])), + } + + +# 0 1 2 3 4 5 6 7 8 9 +cterm_to_hex = ( + 0x000000, 0xc00000, 0x008000, 0x804000, 0x0000c0, 0xc000c0, 0x008080, 0xc0c0c0, 0x808080, 0xff6060, # 0 + 0x00ff00, 0xffff00, 0x8080ff, 0xff40ff, 0x00ffff, 0xffffff, 0x000000, 0x00005f, 0x000087, 0x0000af, # 1 + 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, 0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, # 2 + 0x008787, 0x0087af, 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, # 3 + 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, 0x00ff87, 0x00ffaf, # 4 + 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, # 5 + 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, # 6 + 0x5faf00, 0x5faf5f, 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, # 7 + 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, 0x870000, 0x87005f, # 8 + 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, 0x875f87, 0x875faf, 0x875fd7, 0x875fff, # 9 + 0x878700, 0x87875f, 0x878787, 0x8787af, 0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, # 10 + 0x87afd7, 0x87afff, 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, # 11 + 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff, # 12 + 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, # 13 + 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, # 14 + 0xafd787, 0xafd7af, 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, # 15 + 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, 0xd75f87, 0xd75faf, # 16 + 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, # 17 + 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, # 18 + 0xd7ff00, 0xd7ff5f, 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, # 19 + 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, 0xff8700, 0xff875f, # 20 + 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, # 21 + 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, 0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, # 22 + 0xffffd7, 0xffffff, 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, # 23 + 0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, 0xa8a8a8, 0xb2b2b2, # 24 + 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee # 25 +) diff --git a/powerline/config.py b/powerline/config.py new file mode 100644 index 00000000..edcf921f --- /dev/null +++ b/powerline/config.py @@ -0,0 +1,10 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + + +POWERLINE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BINDINGS_DIRECTORY = os.path.join(POWERLINE_ROOT, 'powerline', 'bindings') +TMUX_CONFIG_DIRECTORY = os.path.join(BINDINGS_DIRECTORY, 'tmux') +DEFAULT_SYSTEM_CONFIG_DIR = None diff --git a/powerline/config_files/colors.json b/powerline/config_files/colors.json new file mode 100644 index 00000000..6bfadf7b --- /dev/null +++ b/powerline/config_files/colors.json @@ -0,0 +1,117 @@ +{ + "colors": { + "black": 16, + "white": 231, + + "darkestgreen": 22, + "darkgreen": 28, + "mediumgreen": 70, + "brightgreen": 148, + + "darkestcyan": 23, + "darkcyan": 74, + "mediumcyan": 117, + "brightcyan": 159, + + "darkestblue": 24, + "darkblue": 31, + + "red": 1, + "darkestred": 52, + "darkred": 88, + "mediumred": 124, + "brightred": 160, + "brightestred": 196, + + "darkestpurple": 55, + "mediumpurple": 98, + "brightpurple": 189, + + "darkorange": 94, + "mediumorange": 166, + "brightorange": 208, + "brightestorange": 214, + + "brightyellow": 220, + + "gray0": 233, + "gray1": 235, + "gray2": 236, + "gray3": 239, + "gray4": 240, + "gray5": 241, + "gray6": 244, + "gray7": 245, + "gray8": 247, + "gray9": 250, + "gray10": 252, + + "lightyellowgreen": 106, + "gold3": 178, + "orangered": 202, + + "steelblue": 67, + "darkorange3": 166, + "skyblue1": 117, + "khaki1": 228, + + "solarized:base03": [8, "002b36"], + "solarized:base02": [0, "073642"], + "solarized:base01": [10, "586e75"], + "solarized:base00": [11, "657b83"], + "solarized:base0": [12, "839496"], + "solarized:base1": [14, "93a1a1"], + "solarized:base2": [7, "eee8d5"], + "solarized:base3": [15, "fdf6e3"], + "solarized:yellow": [3, "b58900"], + "solarized:orange": [9, "cb4b16"], + "solarized:red": [1, "dc322f"], + "solarized:magenta": [5, "d33682"], + "solarized:violet": [13, "6c71c4"], + "solarized:blue": [4, "268bd2"], + "solarized:cyan": [6, "2aa198"], + "solarized:green": [2, "859900"] + }, + "gradients": { + "dark_GREEN_Orange_red": [ + [22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 94, 94, 94, 94, 94, 94, 94, 88, 52], + ["006000", "006000", "006000", "006000", "006000", "006000", "006000", "006000", "006000", "036000", "076000", "0a6000", "0d6000", "106000", "126000", "146000", "166000", "186000", "1a6000", "1b6000", "1d6000", "1e6000", "206000", "216000", "236000", "246000", "256000", "266000", "286000", "296000", "2a6000", "2b6000", "2c6100", "2d6100", "2f6100", "306100", "316100", "326100", "336100", "346100", "356100", "366100", "376100", "386100", "386100", "396100", "3a6100", "3b6100", "3c6100", "3d6100", "3e6100", "3f6100", "406100", "406100", "416100", "426000", "436000", "446000", "456000", "456000", "466000", "476000", "486000", "496000", "496000", "4a6000", "4b6000", "4c6000", "4d6000", "4d6000", "4e6000", "4f6000", "506000", "506000", "516000", "526000", "536000", "536000", "546000", "556000", "566000", "566000", "576000", "586000", "596000", "596000", "5a6000", "5d6000", "616000", "646000", "686000", "6b6000", "6f6000", "726000", "766000", "796000", "7d6000", "806000", "7e5500", "6f3105", "5d0001"] + ], + "GREEN_Orange_red": [ + [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1], + ["005f00", "015f00", "025f00", "035f00", "045f00", "055f00", "065f00", "075f00", "085f00", "095f00", "0b5f00", "0c5f00", "0d5f00", "0e5f00", "0f5f00", "105f00", "115f00", "125f00", "135f00", "145f00", "165f00", "175f00", "185f00", "195f00", "1a5f00", "1b5f00", "1c5f00", "1d5f00", "1e5f00", "1f5f00", "215f00", "225f00", "235f00", "245f00", "255f00", "265f00", "275f00", "285f00", "295f00", "2a5f00", "2c5f00", "2d5f00", "2e5f00", "2f5f00", "305f00", "315f00", "325f00", "335f00", "345f00", "355f00", "375f00", "385f00", "395f00", "3a5f00", "3b5f00", "3c5f00", "3d5f00", "3e5f00", "3f5f00", "415f00", "425f00", "435f00", "445f00", "455f00", "465f00", "475f00", "485f00", "495f00", "4a5f00", "4c5f00", "4d5f00", "4e5f00", "4f5f00", "505f00", "515f00", "525f00", "535f00", "545f00", "555f00", "575f00", "585f00", "595f00", "5a5f00", "5b5f00", "5c5f00", "5d5f00", "5e5f00", "615f00", "655f00", "685f00", "6c5f00", "6f5f00", "735f00", "765f00", "7a5f00", "7d5f00", "815f00", "845f00", "815200", "702900"] + ], + "green_yellow_red": [ + [190, 184, 178, 172, 166, 160], + ["8ae71c", "8ce71c", "8fe71c", "92e71c", "95e71d", "98e71d", "9ae71d", "9de71d", "a0e71e", "a3e71e", "a6e71e", "a8e71e", "abe71f", "aee71f", "b1e71f", "b4e71f", "b6e720", "b9e720", "bce720", "bfe720", "c2e821", "c3e721", "c5e621", "c7e521", "c9e522", "cbe422", "cde322", "cfe222", "d1e223", "d3e123", "d5e023", "d7df23", "d9df24", "dbde24", "dddd24", "dfdc24", "e1dc25", "e3db25", "e5da25", "e7d925", "e9d926", "e9d626", "e9d426", "e9d126", "e9cf27", "e9cc27", "e9ca27", "e9c727", "e9c528", "e9c228", "e9c028", "e9bd28", "e9bb29", "e9b829", "e9b629", "e9b329", "e9b12a", "e9ae2a", "e9ac2a", "e9a92a", "eaa72b", "eaa42b", "eaa22b", "ea9f2b", "ea9d2c", "ea9b2c", "ea982c", "ea962c", "ea942d", "ea912d", "ea8f2d", "ea8d2d", "ea8a2e", "ea882e", "ea862e", "ea832e", "ea812f", "ea7f2f", "ea7c2f", "ea7a2f", "eb7830", "eb7530", "eb7330", "eb7130", "eb6f31", "eb6c31", "eb6a31", "eb6831", "eb6632", "eb6332", "eb6132", "eb5f32", "eb5d33", "eb5a33", "eb5833", "eb5633", "eb5434", "eb5134", "eb4f34", "eb4d34", "ec4b35"] + ], + "green_yellow_orange_red": [ + [2, 3, 9, 1], + ["719e07", "739d06", "759c06", "779c06", "799b06", "7b9a05", "7d9a05", "7f9905", "819805", "839805", "859704", "879704", "899604", "8b9504", "8d9504", "8f9403", "919303", "949303", "969203", "989102", "9a9102", "9c9002", "9e9002", "a08f02", "a28e01", "a48e01", "a68d01", "a88c01", "aa8c01", "ac8b00", "ae8a00", "b08a00", "b28900", "b58900", "b58700", "b68501", "b78302", "b78102", "b87f03", "b97d04", "b97b04", "ba7905", "bb7806", "bb7606", "bc7407", "bd7208", "bd7008", "be6e09", "bf6c0a", "bf6a0a", "c0690b", "c1670c", "c1650c", "c2630d", "c3610e", "c35f0e", "c45d0f", "c55b10", "c55a10", "c65811", "c75612", "c75412", "c85213", "c95014", "c94e14", "ca4c15", "cb4b16", "cb4a16", "cc4917", "cc4818", "cd4719", "cd4719", "ce461a", "ce451b", "cf441c", "cf441c", "d0431d", "d0421e", "d1411f", "d1411f", "d24020", "d23f21", "d33e22", "d33e22", "d43d23", "d43c24", "d53b25", "d53b25", "d63a26", "d63927", "d73828", "d73828", "d83729", "d8362a", "d9352b", "d9352b", "da342c", "da332d", "db322e", "dc322f"] + ], + "yellow_red": [ + [220, 178, 172, 166, 160], + ["ffd700", "fdd500", "fbd300", "fad200", "f8d000", "f7cf00", "f5cd00", "f3cb00", "f2ca00", "f0c800", "efc700", "edc500", "ebc300", "eac200", "e8c000", "e7bf00", "e5bd00", "e3bb00", "e2ba00", "e0b800", "dfb700", "ddb500", "dbb300", "dab200", "d8b000", "d7af00", "d7ad00", "d7ab00", "d7aa00", "d7a800", "d7a700", "d7a500", "d7a300", "d7a200", "d7a000", "d79f00", "d79d00", "d79b00", "d79a00", "d79800", "d79700", "d79500", "d79300", "d79200", "d79000", "d78f00", "d78d00", "d78b00", "d78a00", "d78800", "d78700", "d78500", "d78300", "d78200", "d78000", "d77f00", "d77d00", "d77b00", "d77a00", "d77800", "d77700", "d77500", "d77300", "d77200", "d77000", "d76f00", "d76d00", "d76b00", "d76a00", "d76800", "d76700", "d76500", "d76300", "d76200", "d76000", "d75f00", "d75b00", "d75700", "d75300", "d74f00", "d74c00", "d74800", "d74400", "d74000", "d73c00", "d73900", "d73500", "d73100", "d72d00", "d72900", "d72600", "d72200", "d71e00", "d71a00", "d71600", "d71300", "d70f00", "d70b00", "d70700"] + ], + "yellow_orange_red": [ + [3, 9, 1], + ["b58900", "b58700", "b58600", "b68501", "b68401", "b78202", "b78102", "b88003", "b87f03", "b87d03", "b97c04", "b97b04", "ba7a05", "ba7805", "bb7706", "bb7606", "bc7507", "bc7307", "bc7207", "bd7108", "bd7008", "be6e09", "be6d09", "bf6c0a", "bf6b0a", "c06a0b", "c0680b", "c0670b", "c1660c", "c1650c", "c2630d", "c2620d", "c3610e", "c3600e", "c35e0e", "c45d0f", "c45c0f", "c55b10", "c55910", "c65811", "c65711", "c75612", "c75412", "c75312", "c85213", "c85113", "c94f14", "c94e14", "ca4d15", "ca4c15", "cb4b16", "cb4a16", "cb4a17", "cc4917", "cc4918", "cc4818", "cd4819", "cd4719", "cd471a", "ce461a", "ce461b", "ce451b", "cf451c", "cf441c", "cf441d", "d0431d", "d0431e", "d0421e", "d1421f", "d1411f", "d14120", "d24020", "d24021", "d23f21", "d33f22", "d33e22", "d33e23", "d43d23", "d43d24", "d43c24", "d53c25", "d53b25", "d53b26", "d63a26", "d63a27", "d63927", "d73928", "d73828", "d73829", "d83729", "d8372a", "d8362a", "d9362b", "d9352b", "d9352c", "da342c", "da342d", "da332d", "db332e"] + ], + "blue_red": [ + [39, 74, 68, 67, 103, 97, 96, 132, 131, 167, 203, 197], + ["19b4fe", "1bb2fc", "1db1fa", "1faff8", "22aef6", "24adf4", "26abf2", "29aaf0", "2ba9ee", "2da7ec", "30a6ea", "32a5e8", "34a3e6", "36a2e4", "39a0e2", "3b9fe1", "3d9edf", "409cdd", "429bdb", "449ad9", "4798d7", "4997d5", "4b96d3", "4d94d1", "5093cf", "5292cd", "5490cb", "578fc9", "598dc7", "5b8cc6", "5e8bc4", "6089c2", "6288c0", "6487be", "6785bc", "6984ba", "6b83b8", "6e81b6", "7080b4", "727eb2", "757db0", "777cae", "797aac", "7b79ab", "7e78a9", "8076a7", "8275a5", "8574a3", "8772a1", "89719f", "8c709d", "8e6e9b", "906d99", "926b97", "956a95", "976993", "996791", "9c668f", "9e658e", "a0638c", "a3628a", "a56188", "a75f86", "a95e84", "ac5c82", "ae5b80", "b05a7e", "b3587c", "b5577a", "b75678", "ba5476", "bc5374", "be5273", "c05071", "c34f6f", "c54e6d", "c74c6b", "ca4b69", "cc4967", "ce4865", "d14763", "d34561", "d5445f", "d7435d", "da415b", "dc4059", "de3f58", "e13d56", "e33c54", "e53a52", "e83950", "ea384e", "ec364c", "ee354a", "f13448", "f33246", "f53144", "f83042", "fa2e40"] + ], + "white_red": [ + [231, 255, 223, 216, 209, 202, 196], + ["ffffff", "fefefe", "fdfdfd", "fdfdfd", "fcfcfc", "fbfbfb", "fafafa", "fafafa", "f9f9f9", "f8f8f8", "f7f7f7", "f7f7f7", "f6f6f6", "f5f5f5", "f4f4f4", "f4f3f4", "f3f3f3", "f2f2f2", "f1f1f1", "f0f0f0", "f0f0f0", "efefef", "eeeeee", "efecea", "f1eae4", "f2e8de", "f3e6d8", "f5e4d3", "f6e2cd", "f7e0c7", "f8dec2", "f9dcbc", "fadab6", "fad8b1", "fbd5ac", "fbd2a9", "fbcea5", "fbcaa1", "fbc79e", "fbc39a", "fbc097", "fbbc93", "fbb88f", "fbb58c", "fab188", "faad85", "faaa81", "fba67e", "fba37a", "fb9f76", "fb9c73", "fb986f", "fb946c", "fb9168", "fa8d65", "fa8961", "fa865c", "fa8256", "fb7f4f", "fb7b48", "fb7841", "fb743a", "fb7133", "fb6d2c", "fa6a23", "fa661a", "fa620e", "fa5f03", "fa5d03", "fa5b03", "fa5a03", "fa5803", "fa5703", "fa5503", "fa5303", "fa5103", "fa4f03", "fa4e03", "fa4c03", "fa4a04", "fa4804", "fa4604", "fa4404", "fa4204", "fa3f04", "fa3d04", "fa3b04", "fa3805", "fa3605", "fa3305", "fb3105", "fb2e05", "fb2a05", "fb2705", "fb2306", "fb1f06", "fb1b06", "fb1506", "fb0e06", "fa0506", "fa0007"] + ], + "dark_green_gray": [ + [70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 107, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 108, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 247, 247, 247, 247, 247, 247, 247, 247, 247, 247, 247], + ["51b000", "52b000", "54b000", "55b002", "56b007", "57b00d", "58b011", "59af15", "5aaf18", "5caf1b", "5daf1e", "5eaf21", "5faf23", "60ae25", "61ae27", "62ae2a", "63ae2c", "64ae2e", "65ae30", "66ae31", "67ad33", "68ad35", "69ad37", "69ad38", "6aad3a", "6bad3c", "6cac3d", "6dac3f", "6eac40", "6fac42", "70ac44", "70ac45", "71ab47", "72ab48", "73ab49", "74ab4b", "75ab4c", "75ab4e", "76aa4f", "77aa51", "78aa52", "79aa53", "79aa55", "7aaa56", "7ba957", "7ca959", "7ca95a", "7da95b", "7ea95d", "7fa95e", "7fa85f", "80a861", "81a862", "81a863", "82a865", "83a766", "83a767", "84a768", "85a76a", "85a76b", "86a66c", "87a66d", "87a66f", "88a670", "89a671", "89a672", "8aa574", "8ba575", "8ba576", "8ca577", "8da579", "8da47a", "8ea47b", "8ea47c", "8fa47d", "90a47f", "90a380", "91a381", "91a382", "92a384", "93a385", "93a286", "94a287", "94a288", "95a28a", "95a18b", "96a18c", "97a18d", "97a18e", "98a190", "98a091", "99a092", "99a093", "9aa094", "9aa096", "9b9f97", "9b9f98", "9c9f99", "9c9f9a", "9d9e9c", "9d9e9d"] + ], + "light_green_gray": [ + [148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 148, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 149, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 186, 187, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250], + ["a3d900", "a4d800", "a4d800", "a5d805", "a5d80d", "a6d714", "a6d719", "a6d71d", "a7d621", "a7d625", "a8d628", "a8d62b", "a8d52e", "a9d531", "a9d533", "aad536", "aad438", "aad43a", "abd43d", "abd33f", "abd341", "acd343", "acd345", "acd247", "add249", "add24b", "add14d", "aed14f", "aed151", "aed152", "afd054", "afd056", "afd058", "b0d059", "b0cf5b", "b0cf5d", "b1cf5e", "b1ce60", "b1ce62", "b1ce63", "b2ce65", "b2cd67", "b2cd68", "b3cd6a", "b3cc6b", "b3cc6d", "b3cc6e", "b4cc70", "b4cb71", "b4cb73", "b4cb75", "b5ca76", "b5ca78", "b5ca79", "b5ca7a", "b6c97c", "b6c97d", "b6c97f", "b6c880", "b6c882", "b7c883", "b7c885", "b7c786", "b7c788", "b7c789", "b8c68a", "b8c68c", "b8c68d", "b8c68f", "b8c590", "b9c591", "b9c593", "b9c494", "b9c496", "b9c497", "b9c498", "bac39a", "bac39b", "bac39d", "bac29e", "bac29f", "bac2a1", "bac2a2", "bac1a4", "bbc1a5", "bbc1a6", "bbc0a8", "bbc0a9", "bbc0aa", "bbc0ac", "bbbfad", "bbbfae", "bbbfb0", "bbbeb1", "bcbeb3", "bcbeb4", "bcbdb5", "bcbdb7", "bcbdb8", "bcbdb9", "bcbcbb"] + ] + } +} diff --git a/powerline/config_files/colorschemes/default.json b/powerline/config_files/colorschemes/default.json new file mode 100644 index 00000000..7f234506 --- /dev/null +++ b/powerline/config_files/colorschemes/default.json @@ -0,0 +1,44 @@ +{ + "name": "Default", + "groups": { + "background:divider": { "fg": "gray5", "bg": "gray0", "attr": [] }, + "session": { "fg": "black", "bg": "gray10", "attr": ["bold"] }, + "date": { "fg": "gray8", "bg": "gray2", "attr": [] }, + "time": { "fg": "gray10", "bg": "gray2", "attr": ["bold"] }, + "time:divider": { "fg": "gray5", "bg": "gray2", "attr": [] }, + "email_alert": { "fg": "white", "bg": "brightred", "attr": ["bold"] }, + "email_alert_gradient": { "fg": "white", "bg": "yellow_orange_red", "attr": ["bold"] }, + "hostname": { "fg": "black", "bg": "gray10", "attr": ["bold"] }, + "weather": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "weather_temp_gradient": { "fg": "blue_red", "bg": "gray0", "attr": [] }, + "weather_condition_hot": { "fg": "khaki1", "bg": "gray0", "attr": [] }, + "weather_condition_snowy": { "fg": "skyblue1", "bg": "gray0", "attr": [] }, + "weather_condition_rainy": { "fg": "skyblue1", "bg": "gray0", "attr": [] }, + "uptime": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "external_ip": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "internal_ip": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "network_load": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "network_load_gradient": { "fg": "green_yellow_orange_red", "bg": "gray0", "attr": [] }, + "system_load": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "system_load_gradient": { "fg": "green_yellow_orange_red", "bg": "gray0", "attr": [] }, + "environment": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "cpu_load_percent": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "cpu_load_percent_gradient": { "fg": "green_yellow_orange_red", "bg": "gray0", "attr": [] }, + "battery": { "fg": "gray8", "bg": "gray0", "attr": [] }, + "battery_gradient": { "fg": "white_red", "bg": "gray0", "attr": [] }, + "battery_full": { "fg": "red", "bg": "gray0", "attr": [] }, + "battery_empty": { "fg": "white", "bg": "gray0", "attr": [] }, + "now_playing": { "fg": "gray10", "bg": "black", "attr": [] }, + "user": { "fg": "white", "bg": "darkblue", "attr": ["bold"] }, + "superuser": { "fg": "white", "bg": "brightred", "attr": ["bold"] }, + "branch": { "fg": "gray9", "bg": "gray2", "attr": [] }, + "branch_dirty": { "fg": "brightyellow", "bg": "gray2", "attr": [] }, + "branch_clean": { "fg": "gray9", "bg": "gray2", "attr": [] }, + "branch:divider": { "fg": "gray7", "bg": "gray2", "attr": [] }, + "cwd": { "fg": "gray9", "bg": "gray4", "attr": [] }, + "cwd:current_folder": { "fg": "gray10", "bg": "gray4", "attr": ["bold"] }, + "cwd:divider": { "fg": "gray7", "bg": "gray4", "attr": [] }, + "virtualenv": { "fg": "white", "bg": "darkcyan", "attr": [] }, + "attached_clients": { "fg": "gray8", "bg": "gray0", "attr": [] } + } +} diff --git a/powerline/config_files/colorschemes/ipython/default.json b/powerline/config_files/colorschemes/ipython/default.json new file mode 100644 index 00000000..54987c48 --- /dev/null +++ b/powerline/config_files/colorschemes/ipython/default.json @@ -0,0 +1,7 @@ +{ + "name": "Default color scheme for IPython prompt", + "groups": { + "prompt": { "fg": "gray9", "bg": "gray4", "attr": [] }, + "prompt_count": { "fg": "white", "bg": "gray4", "attr": [] } + } +} diff --git a/powerline/config_files/colorschemes/shell/__main__.json b/powerline/config_files/colorschemes/shell/__main__.json new file mode 100644 index 00000000..c8c2aaa2 --- /dev/null +++ b/powerline/config_files/colorschemes/shell/__main__.json @@ -0,0 +1,6 @@ +{ + "groups": { + "continuation": "cwd", + "continuation:current": "cwd:current_folder" + } +} diff --git a/powerline/config_files/colorschemes/shell/default.json b/powerline/config_files/colorschemes/shell/default.json new file mode 100644 index 00000000..bb9e0d59 --- /dev/null +++ b/powerline/config_files/colorschemes/shell/default.json @@ -0,0 +1,19 @@ +{ + "name": "Default color scheme for shell prompts", + "groups": { + "hostname": { "fg": "brightyellow", "bg": "mediumorange", "attr": [] }, + "jobnum": { "fg": "brightyellow", "bg": "mediumorange", "attr": [] }, + "exit_fail": { "fg": "white", "bg": "darkestred", "attr": [] }, + "exit_success": { "fg": "white", "bg": "darkestgreen", "attr": [] }, + "environment": { "fg": "white", "bg": "darkestgreen", "attr": [] }, + "mode": { "fg": "darkestgreen", "bg": "brightgreen", "attr": ["bold"] }, + "attached_clients": { "fg": "white", "bg": "darkestgreen", "attr": [] } + }, + "mode_translations": { + "vicmd": { + "groups": { + "mode": {"fg": "darkestcyan", "bg": "white", "attr": ["bold"]} + } + } + } +} diff --git a/powerline/config_files/colorschemes/shell/solarized.json b/powerline/config_files/colorschemes/shell/solarized.json new file mode 100644 index 00000000..e9059bd6 --- /dev/null +++ b/powerline/config_files/colorschemes/shell/solarized.json @@ -0,0 +1,16 @@ +{ + "name": "Solarized dark for shell", + "groups": { + "jobnum": { "fg": "solarized:base3", "bg": "solarized:base01", "attr": [] }, + "exit_fail": { "fg": "solarized:base3", "bg": "solarized:red", "attr": [] }, + "exit_success": { "fg": "solarized:base3", "bg": "solarized:green", "attr": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:green", "attr": ["bold"] } + }, + "mode_translations": { + "vicmd": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attr": ["bold"] } + } + } + } +} diff --git a/powerline/config_files/colorschemes/solarized.json b/powerline/config_files/colorschemes/solarized.json new file mode 100644 index 00000000..9c015ccd --- /dev/null +++ b/powerline/config_files/colorschemes/solarized.json @@ -0,0 +1,18 @@ +{ + "name": "Solarized dark", + "groups": { + "background": { "fg": "solarized:base3", "bg": "solarized:base02", "attr": [] }, + "user": { "fg": "solarized:base3", "bg": "solarized:blue", "attr": ["bold"] }, + "superuser": { "fg": "solarized:base3", "bg": "solarized:red", "attr": ["bold"] }, + "virtualenv": { "fg": "solarized:base3", "bg": "solarized:green", "attr": [] }, + "branch": { "fg": "solarized:base1", "bg": "solarized:base02", "attr": [] }, + "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base02", "attr": [] }, + "branch_clean": { "fg": "solarized:base1", "bg": "solarized:base02", "attr": [] }, + "cwd": { "fg": "solarized:base2", "bg": "solarized:base01", "attr": [] }, + "cwd:current_folder": { "fg": "solarized:base3", "bg": "solarized:base01", "attr": ["bold"] }, + "cwd:divider": { "fg": "solarized:base1", "bg": "solarized:base01", "attr": [] }, + "hostname": { "fg": "solarized:base3", "bg": "solarized:base01", "attr": [] }, + "environment": { "fg": "solarized:base3", "bg": "solarized:green", "attr": [] }, + "attached_clients": { "fg": "solarized:base3", "bg": "solarized:green", "attr": [] } + } +} diff --git a/powerline/config_files/colorschemes/vim/__main__.json b/powerline/config_files/colorschemes/vim/__main__.json new file mode 100644 index 00000000..6adb61ce --- /dev/null +++ b/powerline/config_files/colorschemes/vim/__main__.json @@ -0,0 +1,31 @@ +{ + "groups": { + "branch_clean": "branch", + "environment": "information:unimportant", + "file_size": "information:unimportant", + "file_format": "information:unimportant", + "file_encoding": "file_format", + "file_type": "file_format", + "branch": "information:additional", + "file_scheme": "file_name", + "file_directory": "information:additional", + "file_name_empty": "file_directory", + "line_percent": "information:additional", + "line_count": "line_current", + "position": "information:additional", + "single_tab": "line_current", + "many_tabs": "line_current", + "bufnr": "file_directory", + "winnr": "information:unimportant", + "tabnr": "file_directory", + + "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:modified_indicator": "tab_nc:modified_indicator" + } +} diff --git a/powerline/config_files/colorschemes/vim/default.json b/powerline/config_files/colorschemes/vim/default.json new file mode 100644 index 00000000..dd62fdb0 --- /dev/null +++ b/powerline/config_files/colorschemes/vim/default.json @@ -0,0 +1,100 @@ +{ + "name": "Default color scheme", + "groups": { + "information:unimportant": { "fg": "gray8", "bg": "gray2", "attr": [] }, + "information:additional": { "fg": "gray9", "bg": "gray4", "attr": [] }, + "background": { "fg": "white", "bg": "gray2", "attr": [] }, + "background:divider": { "fg": "gray6", "bg": "gray2", "attr": [] }, + "mode": { "fg": "darkestgreen", "bg": "brightgreen", "attr": ["bold"] }, + "visual_range": { "fg": "brightestorange", "bg": "darkorange", "attr": ["bold"] }, + "modified_indicator": { "fg": "brightyellow", "bg": "gray4", "attr": ["bold"] }, + "paste_indicator": { "fg": "white", "bg": "mediumorange", "attr": ["bold"] }, + "readonly_indicator": { "fg": "brightestred", "bg": "gray4", "attr": [] }, + "branch_dirty": { "fg": "brightyellow", "bg": "gray4", "attr": [] }, + "branch:divider": { "fg": "gray7", "bg": "gray4", "attr": [] }, + "file_name": { "fg": "white", "bg": "gray4", "attr": ["bold"] }, + "window_title": { "fg": "white", "bg": "gray4", "attr": [] }, + "file_name_no_file": { "fg": "gray9", "bg": "gray4", "attr": ["bold"] }, + "file_vcs_status": { "fg": "brightestred", "bg": "gray4", "attr": [] }, + "file_vcs_status_M": { "fg": "brightyellow", "bg": "gray4", "attr": [] }, + "file_vcs_status_A": { "fg": "brightgreen", "bg": "gray4", "attr": [] }, + "line_percent": { "fg": "gray9", "bg": "gray4", "attr": [] }, + "line_percent_gradient": { "fg": "dark_green_gray", "bg": "gray4", "attr": [] }, + "position": { "fg": "gray9", "bg": "gray4", "attr": [] }, + "position_gradient": { "fg": "green_yellow_red", "bg": "gray4", "attr": [] }, + "line_current": { "fg": "gray1", "bg": "gray10", "attr": ["bold"] }, + "line_current_symbol": { "fg": "gray1", "bg": "gray10", "attr": [] }, + "virtcol_current_gradient": { "fg": "dark_GREEN_Orange_red", "bg": "gray10", "attr": [] }, + "col_current": { "fg": "gray6", "bg": "gray10", "attr": [] }, + "modified_buffers": { "fg": "brightyellow", "bg": "gray2", "attr": [] }, + "attached_clients": { "fg": "gray8", "bg": "gray2", "attr": [] }, + "error": { "fg": "brightestred", "bg": "darkred", "attr": ["bold"] }, + "warning": { "fg": "brightyellow", "bg": "darkorange", "attr": ["bold"] }, + "current_tag": { "fg": "gray9", "bg": "gray2", "attr": [] }, + + "tab_nc:modified_indicator": { "fg": "brightyellow", "bg": "gray2", "attr": ["bold"] } + }, + "mode_translations": { + "nc": { + "colors": { + "brightyellow": "darkorange", + "brightestred": "darkred", + "gray0": "gray0", + "gray1": "gray0", + "gray2": "gray0", + "gray3": "gray1", + "gray4": "gray1", + "gray5": "gray1", + "gray6": "gray1", + "gray7": "gray4", + "gray8": "gray4", + "gray9": "gray4", + "gray10": "gray5", + "white": "gray6", + "dark_green_gray": "gray5" + } + }, + "i": { + "colors": { + "gray0": "darkestblue", + "gray1": "darkestblue", + "gray2": "darkestblue", + "gray3": "darkblue", + "gray4": "darkblue", + "gray5": "darkestcyan", + "gray6": "darkestcyan", + "gray7": "darkestcyan", + "gray8": "mediumcyan", + "gray9": "mediumcyan", + "gray10": "mediumcyan", + "green_yellow_red": "gray5", + "dark_green_gray": "light_green_gray" + }, + "groups": { + "mode": { "fg": "darkestcyan", "bg": "white", "attr": ["bold"] }, + "background:divider": { "fg": "darkcyan", "bg": "darkestblue", "attr": [] }, + "branch:divider": { "fg": "darkcyan", "bg": "darkblue", "attr": [] } + } + }, + "v": { + "groups": { + "mode": { "fg": "darkorange", "bg": "brightestorange", "attr": ["bold"] } + } + }, + "V": { + "groups": { + "mode": { "fg": "darkorange", "bg": "brightestorange", "attr": ["bold"] } + } + }, + "^V": { + "groups": { + "mode": { "fg": "darkorange", "bg": "brightestorange", "attr": ["bold"] } + } + }, + "R": { + "groups": { + "mode": { "fg": "white", "bg": "brightred", "attr": ["bold"] } + } + } + } +} diff --git a/powerline/config_files/colorschemes/vim/solarized.json b/powerline/config_files/colorschemes/vim/solarized.json new file mode 100644 index 00000000..b0fa497d --- /dev/null +++ b/powerline/config_files/colorschemes/vim/solarized.json @@ -0,0 +1,96 @@ +{ + "name": "Solarized dark for vim", + "groups": { + "information:additional": { "fg": "solarized:base2", "bg": "solarized:base01", "attr": [] }, + "information:unimportant": { "fg": "solarized:base3", "bg": "solarized:base01", "attr": [] }, + "background": { "fg": "solarized:base3", "bg": "solarized:base02", "attr": [] }, + "background:divider": { "fg": "solarized:base00", "bg": "solarized:base02", "attr": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:green", "attr": ["bold"] }, + "visual_range": { "fg": "solarized:green", "bg": "solarized:base3", "attr": ["bold"] }, + "modified_indicator": { "fg": "solarized:yellow", "bg": "solarized:base01", "attr": ["bold"] }, + "paste_indicator": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] }, + "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base01", "attr": [] }, + "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base01", "attr": [] }, + "branch:divider": { "fg": "solarized:base1", "bg": "solarized:base01", "attr": [] }, + "file_name": { "fg": "solarized:base3", "bg": "solarized:base01", "attr": ["bold"] }, + "window_title": { "fg": "solarized:base3", "bg": "solarized:base01", "attr": [] }, + "file_name_no_file": { "fg": "solarized:base3", "bg": "solarized:base01", "attr": ["bold"] }, + "file_format": { "fg": "solarized:base1", "bg": "solarized:base02", "attr": [] }, + "file_vcs_status": { "fg": "solarized:red", "bg": "solarized:base01", "attr": [] }, + "file_vcs_status_M": { "fg": "solarized:yellow", "bg": "solarized:base01", "attr": [] }, + "file_vcs_status_A": { "fg": "solarized:green", "bg": "solarized:base01", "attr": [] }, + "line_percent": { "fg": "solarized:base3", "bg": "solarized:base00", "attr": [] }, + "line_percent_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base00", "attr": [] }, + "position": { "fg": "solarized:base3", "bg": "solarized:base00", "attr": [] }, + "position_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base00", "attr": [] }, + "line_current": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": ["bold"] }, + "line_current_symbol": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": [] }, + "virtcol_current_gradient": { "fg": "GREEN_Orange_red", "bg": "solarized:base2", "attr": [] }, + "col_current": { "fg": "solarized:base0", "bg": "solarized:base2", "attr": [] }, + "environment": { "fg": "solarized:base1", "bg": "solarized:base02", "attr": [] }, + "attached_clients": { "fg": "solarized:base1", "bg": "solarized:base02", "attr": [] }, + "error": { "fg": "solarized:base3", "bg": "solarized:red", "attr": ["bold"] }, + "warning": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] }, + "current_tag": { "fg": "solarized:base3", "bg": "solarized:base02", "attr": ["bold"] } + }, + "mode_translations": { + "nc": { + "colors": { + "solarized:base01": "solarized:base02", + "solarized:base00": "solarized:base02", + "solarized:base0": "solarized:base01", + "solarized:base1": "solarized:base00", + "solarized:base2": "solarized:base0", + "solarized:base3": "solarized:base1" + } + }, + "i": { + "groups": { + "background": { "fg": "solarized:base3", "bg": "solarized:base01", "attr": [] }, + "background:divider": { "fg": "solarized:base2", "bg": "solarized:base01", "attr": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attr": ["bold"] }, + "modified_indicator": { "fg": "solarized:yellow", "bg": "solarized:base2", "attr": ["bold"] }, + "paste_indicator": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] }, + "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base2", "attr": [] }, + "branch": { "fg": "solarized:base01", "bg": "solarized:base2", "attr": [] }, + "branch:divider": { "fg": "solarized:base00", "bg": "solarized:base2", "attr": [] }, + "file_directory": { "fg": "solarized:base01", "bg": "solarized:base2", "attr": [] }, + "file_name": { "fg": "solarized:base02", "bg": "solarized:base2", "attr": ["bold"] }, + "file_size": { "fg": "solarized:base02", "bg": "solarized:base2", "attr": [] }, + "file_name_no_file": { "fg": "solarized:base02", "bg": "solarized:base2", "attr": ["bold"] }, + "file_name_empty": { "fg": "solarized:base02", "bg": "solarized:base2", "attr": [] }, + "file_format": { "fg": "solarized:base2", "bg": "solarized:base01", "attr": [] }, + "file_vcs_status": { "fg": "solarized:red", "bg": "solarized:base2", "attr": [] }, + "file_vcs_status_M": { "fg": "solarized:yellow", "bg": "solarized:base2", "attr": [] }, + "file_vcs_status_A": { "fg": "solarized:green", "bg": "solarized:base2", "attr": [] }, + "line_percent": { "fg": "solarized:base3", "bg": "solarized:base1", "attr": [] }, + "line_percent_gradient": { "fg": "solarized:base3", "bg": "solarized:base1", "attr": [] }, + "position": { "fg": "solarized:base3", "bg": "solarized:base1", "attr": [] }, + "position_gradient": { "fg": "solarized:base3", "bg": "solarized:base1", "attr": [] }, + "line_current": { "fg": "solarized:base03", "bg": "solarized:base3", "attr": ["bold"] }, + "line_current_symbol": { "fg": "solarized:base03", "bg": "solarized:base3", "attr": [] }, + "col_current": { "fg": "solarized:base0", "bg": "solarized:base3", "attr": [] } + } + }, + "v": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] } + } + }, + "V": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] } + } + }, + "^V": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] } + } + }, + "R": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:red", "attr": ["bold"] } + } + } + } +} diff --git a/powerline/config_files/colorschemes/vim/solarizedlight.json b/powerline/config_files/colorschemes/vim/solarizedlight.json new file mode 100644 index 00000000..f6c1e178 --- /dev/null +++ b/powerline/config_files/colorschemes/vim/solarizedlight.json @@ -0,0 +1,96 @@ +{ + "name": "Solarized light for vim", + "groups": { + "information:additional": { "fg": "solarized:base02", "bg": "solarized:base2", "attr": [] }, + "information:unimportant": { "fg": "solarized:base1", "bg": "solarized:base01", "attr": [] }, + "background": { "fg": "solarized:base03", "bg": "solarized:base01", "attr": [] }, + "background:divider": { "fg": "solarized:base0", "bg": "solarized:base01", "attr": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:green", "attr": ["bold"] }, + "visual_range": { "fg": "solarized:green", "bg": "solarized:base3", "attr": ["bold"] }, + "modified_indicator": { "fg": "solarized:yellow", "bg": "solarized:base2", "attr": ["bold"] }, + "paste_indicator": { "fg": "solarized:red", "bg": "solarized:base2", "attr": ["bold"] }, + "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base2", "attr": [] }, + "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base2", "attr": [] }, + "branch:divider": { "fg": "solarized:base1", "bg": "solarized:base2", "attr": [] }, + "file_name": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": ["bold"] }, + "window_title": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": [] }, + "file_size": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": [] }, + "file_name_no_file": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": ["bold"] }, + "file_name_empty": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": [] }, + "file_vcs_status": { "fg": "solarized:red", "bg": "solarized:base2", "attr": [] }, + "file_vcs_status_M": { "fg": "solarized:yellow", "bg": "solarized:base2", "attr": [] }, + "file_vcs_status_A": { "fg": "solarized:green", "bg": "solarized:base2", "attr": [] }, + "line_percent": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": [] }, + "line_percent_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base2", "attr": [] }, + "position": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": [] }, + "position_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base2", "attr": [] }, + "line_current": { "fg": "solarized:base3", "bg": "solarized:base02", "attr": ["bold"] }, + "line_current_symbol": { "fg": "solarized:base3", "bg": "solarized:base02", "attr": [] }, + "virtcol_current_gradient": { "fg": "yellow_orange_red", "bg": "solarized:base02", "attr": [] }, + "col_current": { "fg": "solarized:base00", "bg": "solarized:base02", "attr": [] }, + "error": { "fg": "solarized:base03", "bg": "solarized:red", "attr": ["bold"] }, + "warning": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": ["bold"] }, + "current_tag": { "fg": "solarized:base03", "bg": "solarized:base01", "attr": ["bold"] } + }, + "mode_translations": { + "nc": { + "colors": { + "solarized:base2": "solarized:base01", + "solarized:base0": "solarized:base01", + "solarized:base00": "solarized:base2", + "solarized:base1": "solarized:base0", + "solarized:base02": "solarized:base00", + "solarized:base03": "solarized:base1" + } + }, + "i": { + "groups": { + "background": { "fg": "solarized:base03", "bg": "solarized:base2", "attr": [] }, + "background:divider": { "fg": "solarized:base02", "bg": "solarized:base2", "attr": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attr": ["bold"] }, + "modified_indicator": { "fg": "solarized:yellow", "bg": "solarized:base02", "attr": ["bold"] }, + "paste_indicator": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] }, + "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base02", "attr": [] }, + "branch": { "fg": "solarized:base2", "bg": "solarized:base02", "attr": [] }, + "branch:divider": { "fg": "solarized:base0", "bg": "solarized:base02", "attr": [] }, + "file_directory": { "fg": "solarized:base2", "bg": "solarized:base02", "attr": [] }, + "file_name": { "fg": "solarized:base01", "bg": "solarized:base02", "attr": ["bold"] }, + "file_size": { "fg": "solarized:base01", "bg": "solarized:base02", "attr": [] }, + "file_name_no_file": { "fg": "solarized:base01", "bg": "solarized:base02", "attr": ["bold"] }, + "file_name_empty": { "fg": "solarized:base01", "bg": "solarized:base02", "attr": [] }, + "file_format": { "fg": "solarized:base02", "bg": "solarized:base2", "attr": [] }, + "file_vcs_status": { "fg": "solarized:red", "bg": "solarized:base02", "attr": [] }, + "file_vcs_status_M": { "fg": "solarized:yellow", "bg": "solarized:base02", "attr": [] }, + "file_vcs_status_A": { "fg": "solarized:green", "bg": "solarized:base02", "attr": [] }, + "line_percent": { "fg": "solarized:base03", "bg": "solarized:base1", "attr": [] }, + "line_percent_gradient": { "fg": "solarized:base03", "bg": "solarized:base1", "attr": [] }, + "position": { "fg": "solarized:base03", "bg": "solarized:base1", "attr": [] }, + "position_gradient": { "fg": "solarized:base03", "bg": "solarized:base1", "attr": [] }, + "line_current": { "fg": "solarized:base3", "bg": "solarized:base03", "attr": ["bold"] }, + "line_current_symbol": { "fg": "solarized:base3", "bg": "solarized:base03", "attr": [] }, + "virtcol_current_gradient": { "fg": "yellow_orange_red", "bg": "solarized:base03", "attr": [] }, + "col_current": { "fg": "solarized:base00", "bg": "solarized:base03", "attr": [] } + } + }, + "v": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] } + } + }, + "V": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] } + } + }, + "^V": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attr": ["bold"] } + } + }, + "R": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:red", "attr": ["bold"] } + } + } + } +} diff --git a/powerline/config_files/config.json b/powerline/config_files/config.json new file mode 100644 index 00000000..a5389b6c --- /dev/null +++ b/powerline/config_files/config.json @@ -0,0 +1,48 @@ +{ + "common": { + "term_truecolor": false + }, + "ext": { + "ipython": { + "colorscheme": "default", + "theme": "in", + "local_themes": { + "rewrite": "rewrite", + "out": "out", + "in2": "in2" + } + }, + "shell": { + "colorscheme": "default", + "theme": "default", + "local_themes": { + "continuation": "continuation", + "select": "select" + } + }, + "tmux": { + "colorscheme": "default", + "theme": "default" + }, + "vim": { + "colorscheme": "default", + "theme": "default", + "local_themes": { + "__tabline__": "tabline", + + "cmdwin": "cmdwin", + "help": "help", + "quickfix": "quickfix", + + "powerline.matchers.vim.plugin.nerdtree.nerdtree": "plugin_nerdtree", + "powerline.matchers.vim.plugin.ctrlp.ctrlp": "plugin_ctrlp", + "powerline.matchers.vim.plugin.gundo.gundo": "plugin_gundo", + "powerline.matchers.vim.plugin.gundo.gundo_preview": "plugin_gundo-preview" + } + }, + "wm": { + "colorscheme": "default", + "theme": "default" + } + } +} diff --git a/powerline/config_files/themes/ascii.json b/powerline/config_files/themes/ascii.json new file mode 100644 index 00000000..7875f219 --- /dev/null +++ b/powerline/config_files/themes/ascii.json @@ -0,0 +1,134 @@ +{ + "use_non_breaking_spaces": false, + "dividers": { + "left": { + "hard": " ", + "soft": "| " + }, + "right": { + "hard": " ", + "soft": " |" + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": "BR " + }, + "cwd": { + "args": { + "ellipsis": "..." + } + }, + + "line_current_symbol": { + "contents": "LN " + }, + + "time": { + "before": "" + }, + + "powerline.segments.common.network_load": { + "args": { + "recv_format": "DL {value:>8}", + "sent_format": "UL {value:>8}" + } + }, + "powerline.segments.common.now_playing": { + "args": { + "state_symbols": { + "fallback": "", + "play": ">", + "pause": "~", + "stop": "X" + } + } + }, + "powerline.segments.common.battery": { + "args": { + "full_heart": "O", + "empty_heart": "O" + } + }, + "powerline.segments.common.uptime": { + "before": "UP " + }, + "powerline.segments.common.email_imap_alert": { + "before": "MAIL " + }, + "powerline.segments.common.virtualenv": { + "before": "(e) " + }, + "powerline.segments.common.hostname": { + "before": "H " + }, + "powerline.segments.common.weather": { + "args": { + "icons": { + "day": "DAY", + "blustery": "WIND", + "rainy": "RAIN", + "cloudy": "CLOUDS", + "snowy": "SNOW", + "stormy": "STORM", + "foggy": "FOG", + "sunny": "SUN", + "night": "NIGHT", + "windy": "WINDY", + "not_available": "NA", + "unknown": "UKN" + }, + "temp_format": "{temp:.0f} C" + } + }, + "powerline.segments.common.fuzzy_time": { + "args": { + "unicode_text": false + } + }, + + "powerline.segments.vim.mode": { + "args": { + "override": { + "n": "NORMAL", + "no": "N-OPER", + "v": "VISUAL", + "V": "V-LINE", + "^V": "V-BLCK", + "s": "SELECT", + "S": "S-LINE", + "^S": "S-BLCK", + "i": "INSERT", + "R": "REPLACE", + "Rv": "V-RPLCE", + "c": "COMMND", + "cv": "VIM EX", + "ce": "EX", + "r": "PROMPT", + "rm": "MORE", + "r?": "CONFIRM", + "!": "SHELL" + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "{rows} x {vcols}", + "v_text_oneline": "C:{vcols}", + "v_text_multiline": "L:{rows}", + "V_text": "L:{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "RO" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + } + } +} diff --git a/powerline/config_files/themes/ipython/in.json b/powerline/config_files/themes/ipython/in.json new file mode 100644 index 00000000..6218b3ab --- /dev/null +++ b/powerline/config_files/themes/ipython/in.json @@ -0,0 +1,26 @@ +{ + "default_module": "powerline.segments.common", + "segments": { + "left": [ + { + "function": "virtualenv", + "priority": 10 + }, + { + "type": "string", + "contents": "In [", + "draw_soft_divider": false, + "highlight_group": ["prompt"] + }, + { + "function": "powerline.segments.ipython.prompt_count", + "draw_soft_divider": false + }, + { + "type": "string", + "contents": "]", + "highlight_group": ["prompt"] + } + ] + } +} diff --git a/powerline/config_files/themes/ipython/in2.json b/powerline/config_files/themes/ipython/in2.json new file mode 100644 index 00000000..601fc9e5 --- /dev/null +++ b/powerline/config_files/themes/ipython/in2.json @@ -0,0 +1,13 @@ +{ + "default_module": "powerline.segments.common", + "segments": { + "left": [ + { + "type": "string", + "contents": "", + "width": "auto", + "highlight_group": ["prompt"] + } + ] + } +} diff --git a/powerline/config_files/themes/ipython/out.json b/powerline/config_files/themes/ipython/out.json new file mode 100644 index 00000000..f7c27adf --- /dev/null +++ b/powerline/config_files/themes/ipython/out.json @@ -0,0 +1,24 @@ +{ + "default_module": "powerline.segments.ipython", + "segments": { + "left": [ + { + "type": "string", + "contents": "Out[", + "draw_soft_divider": false, + "width": "auto", + "align": "r", + "highlight_group": ["prompt"] + }, + { + "function": "prompt_count", + "draw_soft_divider": false + }, + { + "type": "string", + "contents": "]", + "highlight_group": ["prompt"] + } + ] + } +} diff --git a/powerline/config_files/themes/ipython/rewrite.json b/powerline/config_files/themes/ipython/rewrite.json new file mode 100644 index 00000000..35b39024 --- /dev/null +++ b/powerline/config_files/themes/ipython/rewrite.json @@ -0,0 +1,23 @@ +{ + "default_module": "powerline.segments.ipython", + "segments": { + "left": [ + { + "type": "string", + "contents": "", + "draw_soft_divider": false, + "width": "auto", + "highlight_group": ["prompt"] + }, + { + "function": "prompt_count", + "draw_soft_divider": false + }, + { + "type": "string", + "contents": ">", + "highlight_group": ["prompt"] + } + ] + } +} diff --git a/powerline/config_files/themes/powerline.json b/powerline/config_files/themes/powerline.json new file mode 100644 index 00000000..859d8341 --- /dev/null +++ b/powerline/config_files/themes/powerline.json @@ -0,0 +1,132 @@ +{ + "dividers": { + "left": { + "hard": " ", + "soft": " " + }, + "right": { + "hard": " ", + "soft": " " + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": " " + }, + "cwd": { + "args": { + "ellipsis": "⋯" + } + }, + + "line_current_symbol": { + "contents": " " + }, + + "time": { + "before": "⌚ " + }, + + "powerline.segments.common.network_load": { + "args": { + "recv_format": "⬇ {value:>8}", + "sent_format": "⬆ {value:>8}" + } + }, + "powerline.segments.common.now_playing": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + "powerline.segments.common.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥" + } + }, + "powerline.segments.common.uptime": { + "before": "⇑ " + }, + "powerline.segments.common.email_imap_alert": { + "before": "✉ " + }, + "powerline.segments.common.virtualenv": { + "before": "ⓔ " + }, + "powerline.segments.common.hostname": { + "before": " " + }, + "powerline.segments.common.weather": { + "args": { + "icons": { + "day": "〇", + "blustery": "⚑", + "rainy": "☔", + "cloudy": "☁", + "snowy": "❅", + "stormy": "☈", + "foggy": "≡", + "sunny": "☼", + "night": "☾", + "windy": "☴", + "not_available": "�", + "unknown": "⚠" + } + } + }, + "powerline.segments.common.fuzzy_time": { + "args": { + "unicode_text": true + } + }, + + "powerline.segments.vim.mode": { + "args": { + "override": { + "n": "NORMAL", + "no": "N·OPER", + "v": "VISUAL", + "V": "V·LINE", + "^V": "V·BLCK", + "s": "SELECT", + "S": "S·LINE", + "^S": "S·BLCK", + "i": "INSERT", + "R": "REPLACE", + "Rv": "V·RPLCE", + "c": "COMMND", + "cv": "VIM EX", + "ce": "EX", + "r": "PROMPT", + "rm": "MORE", + "r?": "CONFIRM", + "!": "SHELL" + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "{rows} × {vcols}", + "v_text_oneline": "C:{vcols}", + "v_text_multiline": "L:{rows}", + "V_text": "L:{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + } + } +} diff --git a/powerline/config_files/themes/shell/__main__.json b/powerline/config_files/themes/shell/__main__.json new file mode 100644 index 00000000..13ae942b --- /dev/null +++ b/powerline/config_files/themes/shell/__main__.json @@ -0,0 +1,14 @@ +{ + "segment_data": { + "hostname": { + "args": { + "only_if_ssh": true + } + }, + "cwd": { + "args": { + "dir_limit_depth": 3 + } + } + } +} diff --git a/powerline/config_files/themes/shell/continuation.json b/powerline/config_files/themes/shell/continuation.json new file mode 100644 index 00000000..9307fc0e --- /dev/null +++ b/powerline/config_files/themes/shell/continuation.json @@ -0,0 +1,12 @@ +{ + "default_module": "powerline.segments.shell", + "segments": { + "left": [ + { + "function": "continuation" + } + ], + "right": [ + ] + } +} diff --git a/powerline/config_files/themes/shell/default.json b/powerline/config_files/themes/shell/default.json new file mode 100644 index 00000000..6ba1ba6d --- /dev/null +++ b/powerline/config_files/themes/shell/default.json @@ -0,0 +1,40 @@ +{ + "default_module": "powerline.segments.common", + "segments": { + "left": [ + { + "function": "powerline.segments.shell.mode" + }, + { + "function": "hostname", + "priority": 10 + }, + { + "function": "user", + "priority": 30 + }, + { + "function": "virtualenv", + "priority": 50 + }, + { + "function": "powerline.segments.shell.cwd", + "priority": 10 + }, + { + "function": "powerline.segments.shell.jobnum", + "priority": 20 + } + ], + "right": [ + { + "function": "powerline.segments.shell.last_pipe_status", + "priority": 10 + }, + { + "function": "branch", + "priority": 40 + } + ] + } +} diff --git a/powerline/config_files/themes/shell/default_leftonly.json b/powerline/config_files/themes/shell/default_leftonly.json new file mode 100644 index 00000000..018847ba --- /dev/null +++ b/powerline/config_files/themes/shell/default_leftonly.json @@ -0,0 +1,35 @@ +{ + "default_module": "powerline.segments.common", + "segments": { + "left": [ + { + "function": "hostname", + "priority": 10 + }, + { + "function": "user", + "priority": 30 + }, + { + "function": "virtualenv", + "priority": 50 + }, + { + "function": "branch", + "priority": 40 + }, + { + "function": "powerline.segments.shell.cwd", + "priority": 10 + }, + { + "function": "powerline.segments.shell.jobnum", + "priority": 20 + }, + { + "function": "powerline.segments.shell.last_status", + "priority": 10 + } + ] + } +} diff --git a/powerline/config_files/themes/shell/select.json b/powerline/config_files/themes/shell/select.json new file mode 100644 index 00000000..172912f1 --- /dev/null +++ b/powerline/config_files/themes/shell/select.json @@ -0,0 +1,13 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "contents": "Select variant", + "width": "auto", + "align": "r", + "highlight_group": ["continuation:current"] + } + ] + } +} diff --git a/powerline/config_files/themes/tmux/default.json b/powerline/config_files/themes/tmux/default.json new file mode 100644 index 00000000..780f34f2 --- /dev/null +++ b/powerline/config_files/themes/tmux/default.json @@ -0,0 +1,29 @@ +{ + "default_module": "powerline.segments.common", + "segments": { + "right": [ + { + "function": "uptime", + "priority": 50 + }, + { + "function": "system_load", + "priority": 50 + }, + { + "function": "date" + }, + { + "function": "date", + "name": "time", + "args": { + "format": "%H:%M", + "istime": true + } + }, + { + "function": "hostname" + } + ] + } +} diff --git a/powerline/config_files/themes/unicode.json b/powerline/config_files/themes/unicode.json new file mode 100644 index 00000000..a3b5a365 --- /dev/null +++ b/powerline/config_files/themes/unicode.json @@ -0,0 +1,132 @@ +{ + "dividers": { + "left": { + "hard": "▌ ", + "soft": "│ " + }, + "right": { + "hard": " ▐", + "soft": " │" + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": "⎇ " + }, + "cwd": { + "args": { + "ellipsis": "⋯" + } + }, + + "line_current_symbol": { + "contents": "␤ " + }, + + "time": { + "before": "⌚ " + }, + + "powerline.segments.common.network_load": { + "args": { + "recv_format": "⬇ {value:>8}", + "sent_format": "⬆ {value:>8}" + } + }, + "powerline.segments.common.now_playing": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + "powerline.segments.common.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥" + } + }, + "powerline.segments.common.uptime": { + "before": "⇑ " + }, + "powerline.segments.common.email_imap_alert": { + "before": "✉ " + }, + "powerline.segments.common.virtualenv": { + "before": "ⓔ " + }, + "powerline.segments.common.hostname": { + "before": "⌂ " + }, + "powerline.segments.common.weather": { + "args": { + "icons": { + "day": "〇", + "blustery": "⚑", + "rainy": "☔", + "cloudy": "☁", + "snowy": "❅", + "stormy": "☈", + "foggy": "≡", + "sunny": "☼", + "night": "☾", + "windy": "☴", + "not_available": "�", + "unknown": "⚠" + } + } + }, + "powerline.segments.common.fuzzy_time": { + "args": { + "unicode_text": true + } + }, + + "powerline.segments.vim.mode": { + "args": { + "override": { + "n": "NORMAL", + "no": "N·OPER", + "v": "VISUAL", + "V": "V·LINE", + "^V": "V·BLCK", + "s": "SELECT", + "S": "S·LINE", + "^S": "S·BLCK", + "i": "INSERT", + "R": "REPLACE", + "Rv": "V·RPLCE", + "c": "COMMND", + "cv": "VIM EX", + "ce": "EX", + "r": "PROMPT", + "rm": "MORE", + "r?": "CONFIRM", + "!": "SHELL" + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "{rows} × {vcols}", + "v_text_oneline": "C:{vcols}", + "v_text_multiline": "L:{rows}", + "V_text": "L:{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "⊗" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + } + } +} diff --git a/powerline/config_files/themes/unicode_terminus.json b/powerline/config_files/themes/unicode_terminus.json new file mode 100644 index 00000000..e435ea93 --- /dev/null +++ b/powerline/config_files/themes/unicode_terminus.json @@ -0,0 +1,132 @@ +{ + "dividers": { + "left": { + "hard": "▌ ", + "soft": "│ " + }, + "right": { + "hard": " ▐", + "soft": " │" + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": "BR " + }, + "cwd": { + "args": { + "ellipsis": "…" + } + }, + + "line_current_symbol": { + "contents": "␤ " + }, + + "time": { + "before": "" + }, + + "powerline.segments.common.network_load": { + "args": { + "recv_format": "⇓ {value:>8}", + "sent_format": "⇑ {value:>8}" + } + }, + "powerline.segments.common.now_playing": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + "powerline.segments.common.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥" + } + }, + "powerline.segments.common.uptime": { + "before": "↑ " + }, + "powerline.segments.common.email_imap_alert": { + "before": "MAIL " + }, + "powerline.segments.common.virtualenv": { + "before": "(e) " + }, + "powerline.segments.common.hostname": { + "before": "⌂ " + }, + "powerline.segments.common.weather": { + "args": { + "icons": { + "day": "DAY", + "blustery": "WIND", + "rainy": "RAIN", + "cloudy": "CLOUDS", + "snowy": "SNOW", + "stormy": "STORM", + "foggy": "FOG", + "sunny": "SUN", + "night": "NIGHT", + "windy": "WINDY", + "not_available": "NA", + "unknown": "UKN" + } + } + }, + "powerline.segments.common.fuzzy_time": { + "args": { + "unicode_text": true + } + }, + + "powerline.segments.vim.mode": { + "args": { + "override": { + "n": "NORMAL", + "no": "N·OPER", + "v": "VISUAL", + "V": "V·LINE", + "^V": "V·BLCK", + "s": "SELECT", + "S": "S·LINE", + "^S": "S·BLCK", + "i": "INSERT", + "R": "REPLACE", + "Rv": "V·RPLCE", + "c": "COMMND", + "cv": "VIM EX", + "ce": "EX", + "r": "PROMPT", + "rm": "MORE", + "r?": "CONFIRM", + "!": "SHELL" + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "{rows} × {vcols}", + "v_text_oneline": "C:{vcols}", + "v_text_multiline": "L:{rows}", + "V_text": "L:{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "RO" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + } + } +} diff --git a/powerline/config_files/themes/unicode_terminus_condensed.json b/powerline/config_files/themes/unicode_terminus_condensed.json new file mode 100644 index 00000000..c4266ee6 --- /dev/null +++ b/powerline/config_files/themes/unicode_terminus_condensed.json @@ -0,0 +1,133 @@ +{ + "dividers": { + "left": { + "hard": "▌", + "soft": "│" + }, + "right": { + "hard": "▐", + "soft": "│" + } + }, + "spaces": 0, + "segment_data": { + "branch": { + "before": "B " + }, + "cwd": { + "args": { + "use_path_separator": true, + "ellipsis": "…" + } + }, + + "line_current_symbol": { + "contents": "␤" + }, + + "time": { + "before": "" + }, + + "powerline.segments.common.network_load": { + "args": { + "recv_format": "⇓{value:>8}", + "sent_format": "⇑{value:>8}" + } + }, + "powerline.segments.common.now_playing": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + "powerline.segments.common.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥" + } + }, + "powerline.segments.common.uptime": { + "before": "↑" + }, + "powerline.segments.common.email_imap_alert": { + "before": "M " + }, + "powerline.segments.common.virtualenv": { + "before": "E " + }, + "powerline.segments.common.hostname": { + "before": "⌂" + }, + "powerline.segments.common.weather": { + "args": { + "icons": { + "day": "D", + "blustery": "W", + "rainy": "R", + "cloudy": "c", + "snowy": "*", + "stormy": "S", + "foggy": "f", + "sunny": "s", + "night": "N", + "windy": "w", + "not_available": "-", + "unknown": "!" + } + } + }, + "powerline.segments.common.fuzzy_time": { + "args": { + "unicode_text": true + } + }, + + "powerline.segments.vim.mode": { + "args": { + "override": { + "n": "NML", + "no": "NOP", + "v": "VIS", + "V": "VLN", + "^V": "VBL", + "s": "SEL", + "S": "SLN", + "^S": "SBL", + "i": "INS", + "R": "REP", + "Rv": "VRP", + "c": "CMD", + "cv": "VEX", + "ce": " EX", + "r": "PRT", + "rm": "MOR", + "r?": "CON", + "!": " SH" + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "{rows}×{vcols}", + "v_text_oneline": "C:{vcols}", + "v_text_multiline": "L:{rows}", + "V_text": "L:{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "RO" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + } + } +} diff --git a/powerline/config_files/themes/vim/__main__.json b/powerline/config_files/themes/vim/__main__.json new file mode 100644 index 00000000..7cd33055 --- /dev/null +++ b/powerline/config_files/themes/vim/__main__.json @@ -0,0 +1,10 @@ +{ + "segment_data": { + "line_percent": { + "args": { + "gradient": true + }, + "after": "%" + } + } +} diff --git a/powerline/config_files/themes/vim/cmdwin.json b/powerline/config_files/themes/vim/cmdwin.json new file mode 100644 index 00000000..c300d948 --- /dev/null +++ b/powerline/config_files/themes/vim/cmdwin.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "contents": "Command Line", + "highlight_group": ["file_name"] + }, + { + "type": "string", + "highlight_group": ["background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ] + } +} diff --git a/powerline/config_files/themes/vim/default.json b/powerline/config_files/themes/vim/default.json new file mode 100644 index 00000000..a71c2136 --- /dev/null +++ b/powerline/config_files/themes/vim/default.json @@ -0,0 +1,119 @@ +{ + "segments": { + "left": [ + { + "function": "mode", + "exclude_modes": ["nc"] + }, + { + "function": "visual_range", + "include_modes": ["v", "V", "^V", "s", "S", "^S"], + "priority": 10 + }, + { + "function": "paste_indicator", + "exclude_modes": ["nc"], + "priority": 10 + }, + { + "function": "branch", + "exclude_modes": ["nc"], + "priority": 30 + }, + { + "function": "readonly_indicator", + "draw_soft_divider": false, + "after": " " + }, + { + "function": "file_scheme", + "priority": 20 + }, + { + "function": "file_directory", + "priority": 40, + "draw_soft_divider": false + }, + { + "function": "file_name", + "draw_soft_divider": false + }, + { + "function": "file_vcs_status", + "before": " ", + "draw_soft_divider": false + }, + { + "function": "modified_indicator", + "before": " " + }, + { + "exclude_modes": ["i", "R", "Rv"], + "function": "trailing_whitespace", + "display": false, + "priority": 60 + }, + { + "exclude_modes": ["nc"], + "function": "powerline.segments.vim.plugin.syntastic.syntastic", + "priority": 50 + }, + { + "exclude_modes": ["nc"], + "function": "powerline.segments.vim.plugin.tagbar.current_tag", + "draw_soft_divider": false, + "priority": 50 + }, + { + "type": "string", + "highlight_group": ["background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "function": "file_format", + "draw_soft_divider": false, + "exclude_modes": ["nc"], + "priority": 60 + }, + { + "function": "file_encoding", + "exclude_modes": ["nc"], + "priority": 60 + }, + { + "function": "file_type", + "exclude_modes": ["nc"], + "priority": 60 + }, + { + "function": "line_percent", + "priority": 50, + "width": 4, + "align": "r" + }, + { + "type": "string", + "name": "line_current_symbol", + "highlight_group": ["line_current_symbol", "line_current"] + }, + { + "function": "line_current", + "draw_soft_divider": false, + "width": 3, + "align": "r" + }, + { + "function": "virtcol_current", + "draw_soft_divider": false, + "priority": 20, + "before": ":", + "width": 3, + "align": "l" + } + ] + } +} diff --git a/powerline/config_files/themes/vim/help.json b/powerline/config_files/themes/vim/help.json new file mode 100644 index 00000000..aef0c23e --- /dev/null +++ b/powerline/config_files/themes/vim/help.json @@ -0,0 +1,36 @@ +{ + "segments": { + "left": [ + { + "function": "file_name", + "draw_soft_divider": false + }, + { + "type": "string", + "highlight_group": ["background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "function": "line_percent", + "priority": 30, + "width": 4, + "align": "r" + }, + { + "type": "string", + "name": "line_current_symbol", + "highlight_group": ["line_current_symbol", "line_current"] + }, + { + "function": "line_current", + "draw_soft_divider": false, + "width": 3, + "align": "r" + } + ] + } +} diff --git a/powerline/config_files/themes/vim/plugin_ctrlp.json b/powerline/config_files/themes/vim/plugin_ctrlp.json new file mode 100644 index 00000000..25e16e79 --- /dev/null +++ b/powerline/config_files/themes/vim/plugin_ctrlp.json @@ -0,0 +1,28 @@ +{ + "default_module": "powerline.segments.vim.plugin.ctrlp", + "segments": { + "left": [ + { + "function": "ctrlp", + "args": { + "side": "left" + } + }, + { + "type": "string", + "highlight_group": ["ctrlp.background", "background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "function": "ctrlp", + "args": { + "side": "right" + } + } + ] + } +} diff --git a/powerline/config_files/themes/vim/plugin_gundo-preview.json b/powerline/config_files/themes/vim/plugin_gundo-preview.json new file mode 100644 index 00000000..cd3b00fe --- /dev/null +++ b/powerline/config_files/themes/vim/plugin_gundo-preview.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_group": ["gundo.name", "file_name"], + "contents": "Undo diff" + }, + { + "type": "string", + "highlight_group": ["gundo.background", "background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ] + } +} diff --git a/powerline/config_files/themes/vim/plugin_gundo.json b/powerline/config_files/themes/vim/plugin_gundo.json new file mode 100644 index 00000000..0d6a448e --- /dev/null +++ b/powerline/config_files/themes/vim/plugin_gundo.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_group": ["gundo.name", "file_name"], + "contents": "Undo tree" + }, + { + "type": "string", + "highlight_group": ["gundo.background", "background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ] + } +} diff --git a/powerline/config_files/themes/vim/plugin_nerdtree.json b/powerline/config_files/themes/vim/plugin_nerdtree.json new file mode 100644 index 00000000..784523af --- /dev/null +++ b/powerline/config_files/themes/vim/plugin_nerdtree.json @@ -0,0 +1,17 @@ +{ + "default_module": "powerline.segments.vim.plugin.nerdtree", + "segments": { + "left": [ + { + "function": "nerdtree" + }, + { + "type": "string", + "highlight_group": ["background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ] + } +} diff --git a/powerline/config_files/themes/vim/quickfix.json b/powerline/config_files/themes/vim/quickfix.json new file mode 100644 index 00000000..2aa1c0f1 --- /dev/null +++ b/powerline/config_files/themes/vim/quickfix.json @@ -0,0 +1,40 @@ +{ + "segment_data": { + "buffer_name": { + "contents": "Location List" + } + }, + "segments": { + "left": [ + { + "type": "string", + "name": "buffer_name", + "highlight_group": ["file_name"] + }, + { + "function": "window_title", + "draw_soft_divider": false + }, + { + "type": "string", + "highlight_group": ["background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "type": "string", + "name": "line_current_symbol", + "highlight_group": ["line_current_symbol", "line_current"] + }, + { + "function": "line_current", + "draw_soft_divider": false, + "width": 3, + "align": "r" + } + ] + } +} diff --git a/powerline/config_files/themes/vim/tabline.json b/powerline/config_files/themes/vim/tabline.json new file mode 100644 index 00000000..1a7ce6ae --- /dev/null +++ b/powerline/config_files/themes/vim/tabline.json @@ -0,0 +1,84 @@ +{ + "default_module": "powerline.segments.vim", + "segments": { + "left": [ + { + "type": "segment_list", + "function": "powerline.listers.vim.tablister", + "exclude_function": "single_tab", + "segments": [ + { + "function": "tabnr", + "after": " ", + "priority": 5 + }, + { + "function": "file_directory", + "priority": 40 + }, + { + "function": "file_name", + "args": { + "display_no_file": true + }, + "priority": 10 + }, + { + "function": "tab_modified_indicator", + "priority": 5 + } + ] + }, + { + "type": "segment_list", + "function": "powerline.listers.vim.bufferlister", + "include_function": "single_tab", + "segments": [ + { + "function": "bufnr", + "after": " ", + "priority": 5 + }, + { + "function": "file_directory", + "priority": 40 + }, + { + "function": "file_name", + "args": { + "display_no_file": true + }, + "priority": 10 + }, + { + "function": "modified_indicator", + "priority": 5 + } + ] + }, + { + "type": "string", + "highlight_group": ["background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "type": "string", + "contents": "Bufs", + "name": "single_tab", + "highlight_group": ["single_tab"], + "include_function": "single_tab" + }, + { + "type": "string", + "contents": "Tabs", + "name": "many_tabs", + "highlight_group": ["many_tabs"], + "exclude_function": "single_tab" + } + ] + } +} diff --git a/powerline/config_files/themes/wm/default.json b/powerline/config_files/themes/wm/default.json new file mode 100644 index 00000000..009c4924 --- /dev/null +++ b/powerline/config_files/themes/wm/default.json @@ -0,0 +1,30 @@ +{ + "default_module": "powerline.segments.common", + "segments": { + "right": [ + { + "function": "weather", + "priority": 50 + }, + { + "function": "date" + }, + { + "function": "date", + "name": "time", + "args": { + "format": "%H:%M", + "istime": true + } + }, + { + "function": "email_imap_alert", + "priority": 10, + "args": { + "username": "", + "password": "" + } + } + ] + } +} diff --git a/powerline/ipython.py b/powerline/ipython.py new file mode 100644 index 00000000..331f6927 --- /dev/null +++ b/powerline/ipython.py @@ -0,0 +1,58 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline import Powerline +from powerline.lib import mergedicts +from powerline.lib.unicode import string + + +# HACK: ipython tries to only leave us with plain ASCII +class RewriteResult(object): + def __init__(self, prompt): + self.prompt = string(prompt) + + def __str__(self): + return self.prompt + + def __add__(self, s): + if type(s) is not str: + try: + s = s.encode('utf-8') + except AttributeError: + raise NotImplementedError + return RewriteResult(self.prompt + s) + + +class IPythonPowerline(Powerline): + def init(self): + super(IPythonPowerline, self).init( + 'ipython', + use_daemon_threads=True + ) + + def get_config_paths(self): + if self.paths: + return self.paths + else: + return super(IPythonPowerline, self).get_config_paths() + + def get_local_themes(self, local_themes): + return dict(((type, {'config': self.load_theme_config(name)}) for type, name in local_themes.items())) + + def load_main_config(self): + r = super(IPythonPowerline, self).load_main_config() + if self.config_overrides: + mergedicts(r, self.config_overrides) + return r + + def load_theme_config(self, name): + r = super(IPythonPowerline, self).load_theme_config(name) + if name in self.theme_overrides: + mergedicts(r, self.theme_overrides[name]) + return r + + def do_setup(self, wrefs): + for wref in wrefs: + obj = wref() + if obj is not None: + setattr(obj, 'powerline', self) diff --git a/powerline/lib/__init__.py b/powerline/lib/__init__.py new file mode 100644 index 00000000..ee82d216 --- /dev/null +++ b/powerline/lib/__init__.py @@ -0,0 +1,110 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import json + +from functools import wraps + + +REMOVE_THIS_KEY = object() + + +def wraps_saveargs(wrapped): + def dec(wrapper): + r = wraps(wrapped)(wrapper) + r.powerline_origin = getattr(wrapped, 'powerline_origin', wrapped) + return r + return dec + + +def mergedicts(d1, d2): + '''Recursively merge two dictionaries + + First dictionary is modified in-place. + ''' + for k in d2: + if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], dict): + mergedicts(d1[k], d2[k]) + elif d2[k] is REMOVE_THIS_KEY: + d1.pop(k, None) + else: + d1[k] = d2[k] + + +def mergedefaults(d1, d2): + '''Recursively merge two dictionaries, keeping existing values + + First dictionary is modified in-place. + ''' + for k in d2: + if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], dict): + mergedefaults(d1[k], d2[k]) + else: + d1.setdefault(k, d2[k]) + + +def mergedicts_copy(d1, d2): + '''Recursively merge two dictionaries. + + Dictionaries are not modified. Copying happens only if necessary. Assumes + that first dictionary supports .copy() method. + ''' + ret = d1.copy() + for k in d2: + if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], dict): + ret[k] = mergedicts_copy(d1[k], d2[k]) + else: + ret[k] = d2[k] + return ret + + +def add_divider_highlight_group(highlight_group): + def dec(func): + @wraps_saveargs(func) + def f(**kwargs): + r = func(**kwargs) + if r: + return [{ + 'contents': r, + 'divider_highlight_group': highlight_group, + }] + else: + return None + return f + return dec + + +def keyvaluesplit(s): + if '=' not in s: + raise TypeError('Option must look like option=json_value') + if s[0] == '_': + raise ValueError('Option names must not start with `_\'') + idx = s.index('=') + o = s[:idx] + rest = s[idx + 1:] + if not rest: + val = REMOVE_THIS_KEY + elif rest[0] in '"{[0193456789' or rest in ('null', 'true', 'false'): + val = json.loads(s[idx + 1:]) + else: + val = rest + return (o, val) + + +def parsedotval(s): + if type(s) is tuple: + o, val = s + else: + o, val = keyvaluesplit(s) + + keys = o.split('.') + if len(keys) > 1: + r = (keys[0], {}) + rcur = r[1] + for key in keys[1:-1]: + rcur[key] = {} + rcur = rcur[key] + rcur[keys[-1]] = val + return r + else: + return (o, val) diff --git a/powerline/lib/config.py b/powerline/lib/config.py new file mode 100644 index 00000000..0c95e476 --- /dev/null +++ b/powerline/lib/config.py @@ -0,0 +1,218 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import json +import codecs + +from copy import deepcopy +from threading import Event, Lock +from collections import defaultdict + +from powerline.lib.threaded import MultiRunnedThread +from powerline.lib.watcher import create_file_watcher + + +def open_file(path): + return codecs.open(path, encoding='utf-8') + + +def load_json_config(config_file_path, load=json.load, open_file=open_file): + with open_file(config_file_path) as config_file_fp: + return load(config_file_fp) + + +class DummyWatcher(object): + def __call__(self, *args, **kwargs): + return False + + def watch(self, *args, **kwargs): + pass + + +class DeferredWatcher(object): + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.calls = [] + + def __call__(self, *args, **kwargs): + self.calls.append(('__call__', args, kwargs)) + + def watch(self, *args, **kwargs): + self.calls.append(('watch', args, kwargs)) + + def unwatch(self, *args, **kwargs): + self.calls.append(('unwatch', args, kwargs)) + + def transfer_calls(self, watcher): + for attr, args, kwargs in self.calls: + getattr(watcher, attr)(*args, **kwargs) + + +class ConfigLoader(MultiRunnedThread): + def __init__(self, shutdown_event=None, watcher=None, watcher_type=None, load=load_json_config, run_once=False): + super(ConfigLoader, self).__init__() + self.shutdown_event = shutdown_event or Event() + if run_once: + self.watcher = DummyWatcher() + self.watcher_type = 'dummy' + else: + self.watcher = watcher or DeferredWatcher() + if watcher: + if not watcher_type: + raise ValueError('When specifying watcher you must also specify watcher type') + self.watcher_type = watcher_type + else: + self.watcher_type = 'deferred' + self._load = load + + self.pl = None + self.interval = None + + self.lock = Lock() + + self.watched = defaultdict(set) + self.missing = defaultdict(set) + self.loaded = {} + + def set_watcher(self, watcher_type, force=False): + if watcher_type == self.watcher_type: + return + watcher = create_file_watcher(self.pl, watcher_type) + with self.lock: + if self.watcher_type == 'deferred': + self.watcher.transfer_calls(watcher) + self.watcher = watcher + self.watcher_type = watcher_type + + def set_pl(self, pl): + self.pl = pl + + def set_interval(self, interval): + self.interval = interval + + def register(self, function, path): + '''Register function that will be run when file changes. + + :param function function: + Function that will be called when file at the given path changes. + :param str path: + Path that will be watched for. + ''' + with self.lock: + self.watched[path].add(function) + self.watcher.watch(path) + + def register_missing(self, condition_function, function, key): + '''Register any function that will be called with given key each + interval seconds (interval is defined at __init__). Its result is then + passed to ``function``, but only if the result is true. + + :param function condition_function: + Function which will be called each ``interval`` seconds. All + exceptions from it will be logged and ignored. IOError exception + will be ignored without logging. + :param function function: + Function which will be called if condition_function returns + something that is true. Accepts result of condition_function as an + argument. + :param str key: + Any value, it will be passed to condition_function on each call. + + Note: registered functions will be automatically removed if + condition_function results in something true. + ''' + with self.lock: + self.missing[key].add((condition_function, function)) + + def unregister_functions(self, removed_functions): + '''Unregister files handled by these functions. + + :param set removed_functions: + Set of functions previously passed to ``.register()`` method. + ''' + with self.lock: + for path, functions in list(self.watched.items()): + functions -= removed_functions + if not functions: + self.watched.pop(path) + self.loaded.pop(path, None) + + def unregister_missing(self, removed_functions): + '''Unregister files handled by these functions. + + :param set removed_functions: + Set of pairs (2-tuples) representing ``(condition_function, + function)`` function pairs previously passed as an arguments to + ``.register_missing()`` method. + ''' + with self.lock: + for key, functions in list(self.missing.items()): + functions -= removed_functions + if not functions: + self.missing.pop(key) + + def load(self, path): + try: + # No locks: GIL does what we need + return deepcopy(self.loaded[path]) + except KeyError: + r = self._load(path) + self.loaded[path] = deepcopy(r) + return r + + def update(self): + toload = [] + with self.lock: + for path, functions in self.watched.items(): + for function in functions: + try: + modified = self.watcher(path) + except OSError as e: + modified = True + self.exception('Error while running watcher for path {0}: {1}', path, str(e)) + else: + if modified: + toload.append(path) + if modified: + function(path) + with self.lock: + for key, functions in list(self.missing.items()): + for condition_function, function in list(functions): + try: + path = condition_function(key) + except IOError: + pass + except Exception as e: + self.exception('Error while running condition function for key {0}: {1}', key, str(e)) + else: + if path: + toload.append(path) + function(path) + functions.remove((condition_function, function)) + if not functions: + self.missing.pop(key) + for path in toload: + try: + self.loaded[path] = deepcopy(self._load(path)) + except Exception as e: + self.exception('Error while loading {0}: {1}', path, str(e)) + try: + self.loaded.pop(path) + except KeyError: + pass + try: + self.loaded.pop(path) + except KeyError: + pass + + def run(self): + while self.interval is not None and not self.shutdown_event.is_set(): + self.update() + self.shutdown_event.wait(self.interval) + + def exception(self, msg, *args, **kwargs): + if self.pl: + self.pl.exception(msg, prefix='config_loader', *args, **kwargs) + else: + raise diff --git a/powerline/lib/debug.py b/powerline/lib/debug.py new file mode 100755 index 00000000..478b9ed8 --- /dev/null +++ b/powerline/lib/debug.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import gc +import sys + +from types import FrameType +from itertools import chain + + +# From http://code.activestate.com/recipes/523004-find-cyclical-references/ +def print_cycles(objects, outstream=sys.stdout, show_progress=False): + '''Find reference cycles + + :param list objects: + A list of objects to find cycles in. It is often useful to pass in + gc.garbage to find the cycles that are preventing some objects from + being garbage collected. + :param file outstream: + The stream for output. + :param bool show_progress: + If True, print the number of objects reached as they are found. + ''' + def print_path(path): + for i, step in enumerate(path): + # next "wraps around" + next = path[(i + 1) % len(path)] + + outstream.write(" %s -- " % str(type(step))) + written = False + if isinstance(step, dict): + for key, val in step.items(): + if val is next: + outstream.write("[%s]" % repr(key)) + written = True + break + if key is next: + outstream.write("[key] = %s" % repr(val)) + written = True + break + elif isinstance(step, (list, tuple)): + for i, item in enumerate(step): + if item is next: + outstream.write("[%d]" % i) + written = True + elif getattr(type(step), '__getattribute__', None) in (object.__getattribute__, type.__getattribute__): + for attr in chain(dir(step), getattr(step, '__dict__', ())): + if getattr(step, attr, None) is next: + try: + outstream.write('%r.%s' % (step, attr)) + except TypeError: + outstream.write('.%s' % (step, attr)) + written = True + break + if not written: + outstream.write(repr(step)) + outstream.write(" ->\n") + outstream.write("\n") + + def recurse(obj, start, all, current_path): + if show_progress: + outstream.write("%d\r" % len(all)) + + all[id(obj)] = None + + referents = gc.get_referents(obj) + for referent in referents: + # If we've found our way back to the start, this is + # a cycle, so print it out + if referent is start: + try: + outstream.write('Cyclic reference: %r\n' % referent) + except TypeError: + try: + outstream.write('Cyclic reference: %i (%r)\n' % (id(referent), type(referent))) + except TypeError: + outstream.write('Cyclic reference: %i\n' % id(referent)) + print_path(current_path) + + # Don't go back through the original list of objects, or + # through temporary references to the object, since those + # are just an artifact of the cycle detector itself. + elif referent is objects or isinstance(referent, FrameType): + continue + + # We haven't seen this object before, so recurse + elif id(referent) not in all: + recurse(referent, start, all, current_path + (obj,)) + + for obj in objects: + # We are not interested in non-powerline cyclic references + try: + if not type(obj).__module__.startswith('powerline'): + continue + except AttributeError: + continue + recurse(obj, obj, {}, ()) diff --git a/powerline/lib/humanize_bytes.py b/powerline/lib/humanize_bytes.py new file mode 100644 index 00000000..c98a1170 --- /dev/null +++ b/powerline/lib/humanize_bytes.py @@ -0,0 +1,25 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from math import log + + +unit_list = tuple(zip(['', 'k', 'M', 'G', 'T', 'P'], [0, 0, 1, 2, 2, 2])) + + +def humanize_bytes(num, suffix='B', si_prefix=False): + '''Return a human friendly byte representation. + + Modified version from http://stackoverflow.com/questions/1094841 + ''' + if num == 0: + return '0 ' + suffix + div = 1000 if si_prefix else 1024 + exponent = min(int(log(num, div)) if num else 0, len(unit_list) - 1) + quotient = float(num) / div ** exponent + unit, decimals = unit_list[exponent] + if unit and not si_prefix: + unit = unit.upper() + 'i' + return ('{{quotient:.{decimals}f}} {{unit}}{{suffix}}' + .format(decimals=decimals) + .format(quotient=quotient, unit=unit, suffix=suffix)) diff --git a/powerline/lib/inotify.py b/powerline/lib/inotify.py new file mode 100644 index 00000000..510008ed --- /dev/null +++ b/powerline/lib/inotify.py @@ -0,0 +1,184 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os +import errno +import ctypes +import struct + +from ctypes.util import find_library + + +__copyright__ = '2013, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +class INotifyError(Exception): + pass + + +_inotify = None + + +def load_inotify(): + ''' Initialize the inotify library ''' + global _inotify + if _inotify is None: + if hasattr(sys, 'getwindowsversion'): + # On windows abort before loading the C library. Windows has + # multiple, incompatible C runtimes, and we have no way of knowing + # if the one chosen by ctypes is compatible with the currently + # loaded one. + raise INotifyError('INotify not available on windows') + if sys.platform == 'darwin': + raise INotifyError('INotify not available on OS X') + if not hasattr(ctypes, 'c_ssize_t'): + raise INotifyError('You need python >= 2.7 to use inotify') + name = find_library('c') + if not name: + raise INotifyError('Cannot find C library') + libc = ctypes.CDLL(name, use_errno=True) + for function in ("inotify_add_watch", "inotify_init1", "inotify_rm_watch"): + if not hasattr(libc, function): + raise INotifyError('libc is too old') + # inotify_init1() + prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, use_errno=True) + init1 = prototype(('inotify_init1', libc), ((1, "flags", 0),)) + + # inotify_add_watch() + prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32, use_errno=True) + add_watch = prototype(('inotify_add_watch', libc), ( + (1, "fd"), (1, "pathname"), (1, "mask"))) + + # inotify_rm_watch() + prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int, use_errno=True) + rm_watch = prototype(('inotify_rm_watch', libc), ( + (1, "fd"), (1, "wd"))) + + # read() + prototype = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, use_errno=True) + read = prototype(('read', libc), ( + (1, "fd"), (1, "buf"), (1, "count"))) + _inotify = (init1, add_watch, rm_watch, read) + return _inotify + + +class INotify(object): + + # See for the flags defined below + + # Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH. + ACCESS = 0x00000001 # File was accessed. + MODIFY = 0x00000002 # File was modified. + ATTRIB = 0x00000004 # Metadata changed. + CLOSE_WRITE = 0x00000008 # Writtable file was closed. + CLOSE_NOWRITE = 0x00000010 # Unwrittable file closed. + OPEN = 0x00000020 # File was opened. + MOVED_FROM = 0x00000040 # File was moved from X. + MOVED_TO = 0x00000080 # File was moved to Y. + CREATE = 0x00000100 # Subfile was created. + DELETE = 0x00000200 # Subfile was deleted. + DELETE_SELF = 0x00000400 # Self was deleted. + MOVE_SELF = 0x00000800 # Self was moved. + + # Events sent by the kernel. + UNMOUNT = 0x00002000 # Backing fs was unmounted. + Q_OVERFLOW = 0x00004000 # Event queued overflowed. + IGNORED = 0x00008000 # File was ignored. + + # Helper events. + CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE) # Close. + MOVE = (MOVED_FROM | MOVED_TO) # Moves. + + # Special flags. + ONLYDIR = 0x01000000 # Only watch the path if it is a directory. + DONT_FOLLOW = 0x02000000 # Do not follow a sym link. + EXCL_UNLINK = 0x04000000 # Exclude events on unlinked objects. + MASK_ADD = 0x20000000 # Add to the mask of an already existing watch. + ISDIR = 0x40000000 # Event occurred against dir. + ONESHOT = 0x80000000 # Only send event once. + + # All events which a program can wait on. + ALL_EVENTS = ( + ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE | OPEN | + MOVED_FROM | MOVED_TO | CREATE | DELETE | DELETE_SELF | MOVE_SELF + ) + + # See + CLOEXEC = 0x80000 + NONBLOCK = 0x800 + + def __init__(self, cloexec=True, nonblock=True): + self._init1, self._add_watch, self._rm_watch, self._read = load_inotify() + flags = 0 + if cloexec: + flags |= self.CLOEXEC + if nonblock: + flags |= self.NONBLOCK + self._inotify_fd = self._init1(flags) + if self._inotify_fd == -1: + raise INotifyError(os.strerror(ctypes.get_errno())) + + self._buf = ctypes.create_string_buffer(5000) + self.fenc = sys.getfilesystemencoding() or 'utf-8' + self.hdr = struct.Struct(b'iIII') + if self.fenc == 'ascii': + self.fenc = 'utf-8' + # We keep a reference to os to prevent it from being deleted + # during interpreter shutdown, which would lead to errors in the + # __del__ method + self.os = os + + def handle_error(self): + eno = ctypes.get_errno() + extra = '' + if eno == errno.ENOSPC: + extra = 'You may need to increase the inotify limits on your system, via /proc/sys/inotify/max_user_*' + raise OSError(eno, self.os.strerror(eno) + str(extra)) + + def __del__(self): + # This method can be called during interpreter shutdown, which means we + # must do the absolute minimum here. Note that there could be running + # daemon threads that are trying to call other methods on this object. + try: + self.os.close(self._inotify_fd) + except (AttributeError, TypeError): + pass + + def close(self): + if hasattr(self, '_inotify_fd'): + self.os.close(self._inotify_fd) + del self.os + del self._add_watch + del self._rm_watch + del self._inotify_fd + + def read(self, get_name=True): + buf = [] + while True: + num = self._read(self._inotify_fd, self._buf, len(self._buf)) + if num == 0: + break + if num < 0: + en = ctypes.get_errno() + if en == errno.EAGAIN: + break # No more data + if en == errno.EINTR: + continue # Interrupted, try again + raise OSError(en, self.os.strerror(en)) + buf.append(self._buf.raw[:num]) + raw = b''.join(buf) + pos = 0 + lraw = len(raw) + while lraw - pos >= self.hdr.size: + wd, mask, cookie, name_len = self.hdr.unpack_from(raw, pos) + pos += self.hdr.size + name = None + if get_name: + name = raw[pos:pos + name_len].rstrip(b'\0').decode(self.fenc) + pos += name_len + self.process_event(wd, mask, cookie, name) + + def process_event(self, *args): + raise NotImplementedError() diff --git a/powerline/lib/memoize.py b/powerline/lib/memoize.py new file mode 100644 index 00000000..cedbe45e --- /dev/null +++ b/powerline/lib/memoize.py @@ -0,0 +1,42 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from functools import wraps + +from powerline.lib.monotonic import monotonic + + +def default_cache_key(**kwargs): + return frozenset(kwargs.items()) + + +class memoize(object): + '''Memoization decorator with timeout.''' + def __init__(self, timeout, cache_key=default_cache_key, cache_reg_func=None): + self.timeout = timeout + self.cache_key = cache_key + self.cache = {} + self.cache_reg_func = cache_reg_func + + def __call__(self, func): + @wraps(func) + def decorated_function(**kwargs): + if self.cache_reg_func: + self.cache_reg_func(self.cache) + self.cache_reg_func = None + + key = self.cache_key(**kwargs) + try: + cached = self.cache.get(key, None) + except TypeError: + return func(**kwargs) + # Handle case when time() appears to be less then cached['time'] due + # to clock updates. Not applicable for monotonic clock, but this + # case is currently rare. + if cached is None or not (cached['time'] < monotonic() < cached['time'] + self.timeout): + cached = self.cache[key] = { + 'result': func(**kwargs), + 'time': monotonic(), + } + return cached['result'] + return decorated_function diff --git a/powerline/lib/monotonic.py b/powerline/lib/monotonic.py new file mode 100644 index 00000000..cd7c4141 --- /dev/null +++ b/powerline/lib/monotonic.py @@ -0,0 +1,100 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + try: + # >=python-3.3, Unix + from time import clock_gettime + try: + # >={kernel}-sources-2.6.28 + from time import CLOCK_MONOTONIC_RAW as CLOCK_ID + except ImportError: + from time import CLOCK_MONOTONIC as CLOCK_ID + + monotonic = lambda: clock_gettime(CLOCK_ID) + except ImportError: + # >=python-3.3 + from time import monotonic +except ImportError: + import ctypes + import sys + + try: + if sys.platform == 'win32': + # Windows only + GetTickCount64 = ctypes.windll.kernel32.GetTickCount64 + GetTickCount64.restype = ctypes.c_ulonglong + + def monotonic(): + return GetTickCount64() / 1000 + + elif sys.platform == 'darwin': + # Mac OS X + from ctypes.util import find_library + + libc_name = find_library('c') + if not libc_name: + raise OSError + + libc = ctypes.CDLL(libc_name, use_errno=True) + + mach_absolute_time = libc.mach_absolute_time + mach_absolute_time.argtypes = () + mach_absolute_time.restype = ctypes.c_uint64 + + class mach_timebase_info_data_t(ctypes.Structure): + _fields_ = ( + ('numer', ctypes.c_uint32), + ('denom', ctypes.c_uint32), + ) + mach_timebase_info_data_p = ctypes.POINTER(mach_timebase_info_data_t) + + _mach_timebase_info = libc.mach_timebase_info + _mach_timebase_info.argtypes = (mach_timebase_info_data_p,) + _mach_timebase_info.restype = ctypes.c_int + + def mach_timebase_info(): + timebase = mach_timebase_info_data_t() + _mach_timebase_info(ctypes.byref(timebase)) + return (timebase.numer, timebase.denom) + + timebase = mach_timebase_info() + factor = timebase[0] / timebase[1] * 1e-9 + + def monotonic(): + return mach_absolute_time() * factor + else: + # linux only (no librt on OS X) + import os + + # See + CLOCK_MONOTONIC = 1 + CLOCK_MONOTONIC_RAW = 4 + + class timespec(ctypes.Structure): + _fields_ = ( + ('tv_sec', ctypes.c_long), + ('tv_nsec', ctypes.c_long) + ) + tspec = timespec() + + librt = ctypes.CDLL('librt.so.1', use_errno=True) + clock_gettime = librt.clock_gettime + clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)] + + if clock_gettime(CLOCK_MONOTONIC_RAW, ctypes.pointer(tspec)) == 0: + # >={kernel}-sources-2.6.28 + clock_id = CLOCK_MONOTONIC_RAW + elif clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(tspec)) == 0: + clock_id = CLOCK_MONOTONIC + else: + raise OSError + + def monotonic(): + if clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(tspec)) != 0: + errno_ = ctypes.get_errno() + raise OSError(errno_, os.strerror(errno_)) + return tspec.tv_sec + tspec.tv_nsec / 1e9 + + except: + from time import time as monotonic # NOQA diff --git a/powerline/lib/path.py b/powerline/lib/path.py new file mode 100644 index 00000000..a55d3e0a --- /dev/null +++ b/powerline/lib/path.py @@ -0,0 +1,8 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + + +def realpath(path): + return os.path.abspath(os.path.realpath(path)) diff --git a/powerline/lib/shell.py b/powerline/lib/shell.py new file mode 100644 index 00000000..3359c843 --- /dev/null +++ b/powerline/lib/shell.py @@ -0,0 +1,134 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os + +from subprocess import Popen, PIPE +from locale import getlocale, getdefaultlocale, LC_MESSAGES +from functools import partial + + +if sys.platform.startswith('win32'): + # Prevent windows from launching consoles when calling commands + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx + Popen = partial(Popen, creationflags=0x08000000) + + +def _get_shell_encoding(): + return getlocale(LC_MESSAGES)[1] or getdefaultlocale()[1] or 'utf-8' + + +def run_cmd(pl, cmd, stdin=None): + '''Run command and return its stdout, stripped + + If running command fails returns None and logs failure to ``pl`` argument. + + :param PowerlineLogger pl: + Logger used to log failures. + :param list cmd: + Command which will be run. + :param str stdin: + String passed to command. May be None. + ''' + try: + p = Popen(cmd, shell=False, stdout=PIPE, stdin=PIPE) + except OSError as e: + pl.exception('Could not execute command ({0}): {1}', e, cmd) + return None + else: + stdout, err = p.communicate(stdin) + stdout = stdout.decode(_get_shell_encoding()) + return stdout.strip() + + +def asrun(pl, ascript): + '''Run the given AppleScript and return the standard output and error.''' + return run_cmd(pl, ['osascript', '-'], ascript) + + +def readlines(cmd, cwd): + '''Run command and read its output, line by line + + :param list cmd: + Command which will be run. + :param str cwd: + Working directory of the command which will be run. + ''' + p = Popen(cmd, shell=False, stdout=PIPE, stderr=PIPE, cwd=cwd) + encoding = _get_shell_encoding() + p.stderr.close() + with p.stdout: + for line in p.stdout: + yield line[:-1].decode(encoding) + + +try: + from shutil import which +except ImportError: + # shutil.which was added in python-3.3. Here is what was added: + # Lib/shutil.py, commit 5abe28a9c8fe701ba19b1db5190863384e96c798 + def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path which + conforms to the given mode on the PATH, or None if there is no such + file. + + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result + of os.environ.get("PATH"), or can be overridden with a custom search + path. + + """ + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on Windows + # directories pass the os.access check. + def _access_check(fn, mode): + return ( + os.path.exists(fn) + and os.access(fn, mode) + and not os.path.isdir(fn) + ) + + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to the + # current directory, e.g. ./script + if os.path.dirname(cmd): + if _access_check(cmd, mode): + return cmd + return None + + if path is None: + path = os.environ.get("PATH", os.defpath) + if not path: + return None + path = path.split(os.pathsep) + + if sys.platform == "win32": + # The current directory takes precedence on Windows. + if os.curdir not in path: + path.insert(0, os.curdir) + + # PATHEXT is necessary to check on Windows. + pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + # See if the given file matches any of the expected path extensions. + # This will allow us to short circuit when given "python.exe". + # If it does match, only test that one, otherwise we have to try + # others. + if any(cmd.lower().endswith(ext.lower()) for ext in pathext): + files = [cmd] + else: + files = [cmd + ext for ext in pathext] + else: + # On other platforms you don't have things like PATHEXT to tell you + # what file suffixes are executable, so just pass on cmd as-is. + files = [cmd] + + seen = set() + for dir in path: + normdir = os.path.normcase(dir) + if normdir not in seen: + seen.add(normdir) + for thefile in files: + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name + return None diff --git a/powerline/lib/threaded.py b/powerline/lib/threaded.py new file mode 100644 index 00000000..5e30988a --- /dev/null +++ b/powerline/lib/threaded.py @@ -0,0 +1,257 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from threading import Thread, Lock, Event +from types import MethodType + +from powerline.lib.monotonic import monotonic +from powerline.segments import Segment + + +class MultiRunnedThread(object): + daemon = True + + def __init__(self): + self.thread = None + + def is_alive(self): + return self.thread and self.thread.is_alive() + + def start(self): + self.shutdown_event.clear() + self.thread = Thread(target=self.run) + self.thread.daemon = self.daemon + self.thread.start() + + def join(self, *args, **kwargs): + if self.thread: + return self.thread.join(*args, **kwargs) + return None + + +class ThreadedSegment(Segment, MultiRunnedThread): + min_sleep_time = 0.1 + update_first = True + interval = 1 + daemon = False + + argmethods = ('render', 'set_state') + + def __init__(self): + super(ThreadedSegment, self).__init__() + self.run_once = True + self.crashed = False + self.crashed_value = None + self.update_value = None + self.updated = False + + def __call__(self, pl, update_first=True, **kwargs): + if self.run_once: + self.pl = pl + self.set_state(**kwargs) + update_value = self.get_update_value(True) + elif not self.is_alive(): + # Without this we will not have to wait long until receiving bug “I + # opened vim, but branch information is only shown after I move + # cursor”. + # + # If running once .update() is called in __call__. + self.start() + update_value = self.get_update_value(self.do_update_first) + else: + update_value = self.get_update_value(not self.updated) + + if self.crashed: + return self.crashed_value + + return self.render(update_value, update_first=update_first, pl=pl, **kwargs) + + def set_update_value(self): + try: + self.update_value = self.update(self.update_value) + except Exception as e: + self.exception('Exception while updating: {0}', str(e)) + self.crashed = True + except KeyboardInterrupt: + self.warn('Caught keyboard interrupt while updating') + self.crashed = True + else: + self.crashed = False + self.updated = True + + def get_update_value(self, update=False): + if update: + self.set_update_value() + return self.update_value + + def run(self): + if self.do_update_first: + start_time = monotonic() + while True: + self.shutdown_event.wait(max(self.interval - (monotonic() - start_time), self.min_sleep_time)) + if self.shutdown_event.is_set(): + break + start_time = monotonic() + self.set_update_value() + else: + while not self.shutdown_event.is_set(): + start_time = monotonic() + self.set_update_value() + self.shutdown_event.wait(max(self.interval - (monotonic() - start_time), self.min_sleep_time)) + + def shutdown(self): + self.shutdown_event.set() + if self.daemon and self.is_alive(): + # Give the worker thread a chance to shutdown, but don't block for + # too long + self.join(0.01) + + def set_interval(self, interval=None): + # Allowing “interval” keyword in configuration. + # Note: Here **kwargs is needed to support foreign data, in subclasses + # it can be seen in a number of places in order to support + # .set_interval(). + interval = interval or getattr(self, 'interval') + self.interval = interval + + def set_state(self, interval=None, update_first=True, shutdown_event=None, **kwargs): + self.set_interval(interval) + self.shutdown_event = shutdown_event or Event() + self.do_update_first = update_first and self.update_first + self.updated = self.updated or (not self.do_update_first) + + def startup(self, pl, **kwargs): + self.run_once = False + self.pl = pl + self.daemon = pl.use_daemon_threads + + self.set_state(**kwargs) + + if not self.is_alive(): + self.start() + + def critical(self, *args, **kwargs): + self.pl.critical(prefix=self.__class__.__name__, *args, **kwargs) + + def exception(self, *args, **kwargs): + self.pl.exception(prefix=self.__class__.__name__, *args, **kwargs) + + def info(self, *args, **kwargs): + self.pl.info(prefix=self.__class__.__name__, *args, **kwargs) + + def error(self, *args, **kwargs): + self.pl.error(prefix=self.__class__.__name__, *args, **kwargs) + + def warn(self, *args, **kwargs): + self.pl.warn(prefix=self.__class__.__name__, *args, **kwargs) + + def debug(self, *args, **kwargs): + self.pl.debug(prefix=self.__class__.__name__, *args, **kwargs) + + def argspecobjs(self): + for name in self.argmethods: + try: + yield name, getattr(self, name) + except AttributeError: + pass + + def additional_args(self): + return (('interval', self.interval),) + + def omitted_args(self, name, method): + if isinstance(getattr(self, name, None), MethodType): + omitted_indexes = (0,) + else: + omitted_indexes = () + if name.startswith('render'): + if omitted_indexes: + omitted_indexes += (1,) + else: + omitted_indexes = (0,) + return omitted_indexes + + +class KwThreadedSegment(ThreadedSegment): + update_first = True + + argmethods = ('render', 'set_state', 'key', 'render_one') + + def __init__(self): + super(KwThreadedSegment, self).__init__() + self.updated = True + self.update_value = ({}, set()) + self.write_lock = Lock() + self.new_queries = [] + + @staticmethod + def key(**kwargs): + return frozenset(kwargs.items()) + + def render(self, update_value, update_first, key=None, after_update=False, **kwargs): + queries, crashed = update_value + if key is None: + key = self.key(**kwargs) + if key in crashed: + return self.crashed_value + + try: + update_state = queries[key][1] + except KeyError: + with self.write_lock: + self.new_queries.append(key) + if self.do_update_first or self.run_once: + if after_update: + self.error('internal error: value was not computed even though update_first was set') + update_state = None + else: + return self.render( + update_value=self.get_update_value(True), + update_first=False, + key=key, + after_update=True, + **kwargs + ) + else: + update_state = None + + return self.render_one(update_state, **kwargs) + + def update_one(self, crashed, updates, key): + try: + updates[key] = (monotonic(), self.compute_state(key)) + except Exception as e: + self.exception('Exception while computing state for {0!r}: {1}', key, str(e)) + crashed.add(key) + except KeyboardInterrupt: + self.warn('Interrupt while computing state for {0!r}', key) + crashed.add(key) + + def update(self, old_update_value): + updates = {} + crashed = set() + update_value = (updates, crashed) + queries = old_update_value[0] + + new_queries = self.new_queries + with self.write_lock: + self.new_queries = [] + + for key, (last_query_time, state) in queries.items(): + if last_query_time < monotonic() < last_query_time + self.interval: + updates[key] = (last_query_time, state) + else: + self.update_one(crashed, updates, key) + + for key in new_queries: + self.update_one(crashed, updates, key) + + return update_value + + def set_state(self, interval=None, update_first=True, shutdown_event=None, **kwargs): + self.set_interval(interval) + self.do_update_first = update_first and self.update_first + self.shutdown_event = shutdown_event or Event() + + @staticmethod + def render_one(update_state, **kwargs): + return update_state diff --git a/powerline/lib/unicode.py b/powerline/lib/unicode.py new file mode 100644 index 00000000..cea8b05f --- /dev/null +++ b/powerline/lib/unicode.py @@ -0,0 +1,71 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from locale import getpreferredencoding + + +try: + from __builtin__ import unicode +except ImportError: + unicode = str + + +try: + from __builtin__ import unichr +except ImportError: + unichr = chr + + +def u(s): + '''Return unicode instance assuming UTF-8 encoded string. + ''' + if type(s) is unicode: + return s + else: + return unicode(s, 'utf-8') + + +def safe_unicode(s): + '''Return unicode instance without raising an exception. + + Order of assumptions: + * ASCII string or unicode object + * UTF-8 string + * Object with __str__() or __repr__() method that returns UTF-8 string or + unicode object (depending on python version) + * String in locale.getpreferredencoding() encoding + * If everything failed use safe_unicode on last exception with which + everything failed + ''' + try: + try: + return unicode(s) + except UnicodeDecodeError: + try: + return unicode(s, 'utf-8') + except TypeError: + return unicode(str(s), 'utf-8') + except UnicodeDecodeError: + return unicode(s, getpreferredencoding()) + except Exception as e: + return safe_unicode(e) + + +class FailedUnicode(unicode): + '''Builtin ``unicode`` (``str`` in python 3) subclass indicating fatal + error. + + If your code for some reason wants to determine whether `.render()` method + failed it should check returned string for being a FailedUnicode instance. + Alternatively you could subclass Powerline and override `.render()` method + to do what you like in place of catching the exception and returning + FailedUnicode. + ''' + pass + + +def string(s): + if type(s) is not str: + return s.encode('utf-8') + else: + return s diff --git a/powerline/lib/url.py b/powerline/lib/url.py new file mode 100644 index 00000000..f25919cd --- /dev/null +++ b/powerline/lib/url.py @@ -0,0 +1,17 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + from urllib.error import HTTPError # NOQA + from urllib.request import urlopen # NOQA + from urllib.parse import urlencode as urllib_urlencode # NOQA +except ImportError: + from urllib2 import urlopen, HTTPError # NOQA + from urllib import urlencode as urllib_urlencode # NOQA + + +def urllib_read(url): + try: + return urlopen(url, timeout=10).read().decode('utf-8') + except HTTPError: + return diff --git a/powerline/lib/vcs/__init__.py b/powerline/lib/vcs/__init__.py new file mode 100644 index 00000000..96dee683 --- /dev/null +++ b/powerline/lib/vcs/__init__.py @@ -0,0 +1,268 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import errno + +from threading import Lock +from collections import defaultdict + +from powerline.lib.watcher import create_tree_watcher + + +def generate_directories(path): + if os.path.isdir(path): + yield path + while True: + if os.path.ismount(path): + break + old_path = path + path = os.path.dirname(path) + if path == old_path or not path: + break + yield path + + +_file_watcher = None + + +def file_watcher(create_watcher): + global _file_watcher + if _file_watcher is None: + _file_watcher = create_watcher() + return _file_watcher + + +_branch_watcher = None + + +def branch_watcher(create_watcher): + global _branch_watcher + if _branch_watcher is None: + _branch_watcher = create_watcher() + return _branch_watcher + + +branch_name_cache = {} +branch_lock = Lock() +file_status_lock = Lock() + + +def get_branch_name(directory, config_file, get_func, create_watcher): + global branch_name_cache + with branch_lock: + # Check if the repo directory was moved/deleted + fw = branch_watcher(create_watcher) + is_watched = fw.is_watching(directory) + try: + changed = fw(directory) + except OSError as e: + if getattr(e, 'errno', None) != errno.ENOENT: + raise + changed = True + if changed: + branch_name_cache.pop(config_file, None) + # Remove the watches for this repo + if is_watched: + fw.unwatch(directory) + fw.unwatch(config_file) + else: + # Check if the config file has changed + try: + changed = fw(config_file) + except OSError as e: + if getattr(e, 'errno', None) != errno.ENOENT: + raise + # Config file does not exist (happens for mercurial) + if config_file not in branch_name_cache: + branch_name_cache[config_file] = get_func(directory, config_file) + if changed: + # Config file has changed or was not tracked + branch_name_cache[config_file] = get_func(directory, config_file) + return branch_name_cache[config_file] + + +class FileStatusCache(dict): + def __init__(self): + self.dirstate_map = defaultdict(set) + self.ignore_map = defaultdict(set) + self.keypath_ignore_map = {} + + def update_maps(self, keypath, directory, dirstate_file, ignore_file_name, extra_ignore_files): + parent = keypath + ignore_files = set() + while parent != directory: + nparent = os.path.dirname(keypath) + if nparent == parent: + break + parent = nparent + ignore_files.add(os.path.join(parent, ignore_file_name)) + for f in extra_ignore_files: + ignore_files.add(f) + self.keypath_ignore_map[keypath] = ignore_files + for ignf in ignore_files: + self.ignore_map[ignf].add(keypath) + self.dirstate_map[dirstate_file].add(keypath) + + def invalidate(self, dirstate_file=None, ignore_file=None): + for keypath in self.dirstate_map[dirstate_file]: + self.pop(keypath, None) + for keypath in self.ignore_map[ignore_file]: + self.pop(keypath, None) + + def ignore_files(self, keypath): + for ignf in self.keypath_ignore_map[keypath]: + yield ignf + + +file_status_cache = FileStatusCache() + + +def get_file_status(directory, dirstate_file, file_path, ignore_file_name, get_func, create_watcher, extra_ignore_files=()): + global file_status_cache + keypath = file_path if os.path.isabs(file_path) else os.path.join(directory, file_path) + file_status_cache.update_maps(keypath, directory, dirstate_file, ignore_file_name, extra_ignore_files) + + with file_status_lock: + # Optimize case of keypath not being cached + if keypath not in file_status_cache: + file_status_cache[keypath] = ans = get_func(directory, file_path) + return ans + + # Check if any relevant files have changed + file_changed = file_watcher(create_watcher) + changed = False + # Check if dirstate has changed + try: + changed = file_changed(dirstate_file) + except OSError as e: + if getattr(e, 'errno', None) != errno.ENOENT: + raise + # The .git index file does not exist for a new git repo + return get_func(directory, file_path) + + if changed: + # Remove all cached values for files that depend on this + # dirstate_file + file_status_cache.invalidate(dirstate_file=dirstate_file) + else: + # Check if the file itself has changed + try: + changed ^= file_changed(keypath) + except OSError as e: + if getattr(e, 'errno', None) != errno.ENOENT: + raise + # Do not call get_func again for a non-existant file + if keypath not in file_status_cache: + file_status_cache[keypath] = get_func(directory, file_path) + return file_status_cache[keypath] + + if changed: + file_status_cache.pop(keypath, None) + else: + # Check if one of the ignore files has changed + for ignf in file_status_cache.ignore_files(keypath): + try: + changed ^= file_changed(ignf) + except OSError as e: + if getattr(e, 'errno', None) != errno.ENOENT: + raise + if changed: + # Invalidate cache for all files that might be affected + # by this ignore file + file_status_cache.invalidate(ignore_file=ignf) + break + + try: + return file_status_cache[keypath] + except KeyError: + file_status_cache[keypath] = ans = get_func(directory, file_path) + return ans + + +class TreeStatusCache(dict): + def __init__(self, pl): + self.tw = create_tree_watcher(pl) + self.pl = pl + + def cache_and_get(self, key, status): + ans = self.get(key, self) + if ans is self: + ans = self[key] = status() + return ans + + def __call__(self, repo): + key = repo.directory + try: + if self.tw(key, ignore_event=getattr(repo, 'ignore_event', None)): + self.pop(key, None) + except OSError as e: + self.pl.warn('Failed to check {0} for changes, with error: {1}', key, str(e)) + return self.cache_and_get(key, repo.status) + + +_tree_status_cache = None + + +def tree_status(repo, pl): + global _tree_status_cache + if _tree_status_cache is None: + _tree_status_cache = TreeStatusCache(pl) + return _tree_status_cache(repo) + + +vcs_props = ( + ('git', '.git', os.path.exists), + ('mercurial', '.hg', os.path.isdir), + ('bzr', '.bzr', os.path.isdir), +) + + +def guess(path, create_watcher): + for directory in generate_directories(path): + for vcs, vcs_dir, check in vcs_props: + repo_dir = os.path.join(directory, vcs_dir) + if check(repo_dir): + if os.path.isdir(repo_dir) and not os.access(repo_dir, os.X_OK): + continue + try: + if vcs not in globals(): + globals()[vcs] = getattr(__import__(str('powerline.lib.vcs'), fromlist=[str(vcs)]), str(vcs)) + return globals()[vcs].Repository(directory, create_watcher) + except: + pass + return None + + +def get_fallback_create_watcher(): + from powerline.lib.watcher import create_file_watcher + from powerline import get_fallback_logger + from functools import partial + return partial(create_file_watcher, get_fallback_logger(), 'auto') + + +def debug(): + '''Test run guess(), repo.branch() and repo.status() + + To use:: + python -c "from powerline.lib.vcs import debug; debug()" some_file_to_watch. + ''' + import sys + dest = sys.argv[-1] + repo = guess(os.path.abspath(dest), get_fallback_create_watcher) + if repo is None: + print ('%s is not a recognized vcs repo' % dest) + raise SystemExit(1) + print ('Watching %s' % dest) + print ('Press Ctrl-C to exit.') + try: + while True: + if os.path.isdir(dest): + print ('Branch name: %s Status: %s' % (repo.branch(), repo.status())) + else: + print ('File status: %s' % repo.status(dest)) + raw_input('Press Enter to check again: ') + except KeyboardInterrupt: + pass + except EOFError: + pass diff --git a/powerline/lib/vcs/bzr.py b/powerline/lib/vcs/bzr.py new file mode 100644 index 00000000..012f612e --- /dev/null +++ b/powerline/lib/vcs/bzr.py @@ -0,0 +1,109 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os +import re + +from io import StringIO + +from bzrlib import (workingtree, status, library_state, trace, ui) + +from powerline.lib.vcs import get_branch_name, get_file_status + + +class CoerceIO(StringIO): + def write(self, arg): + if isinstance(arg, bytes): + arg = arg.decode('utf-8', 'replace') + return super(CoerceIO, self).write(arg) + + +nick_pat = re.compile(br'nickname\s*=\s*(.+)') + + +def branch_name_from_config_file(directory, config_file): + ans = None + try: + with open(config_file, 'rb') as f: + for line in f: + m = nick_pat.match(line) + if m is not None: + ans = m.group(1).strip().decode('utf-8', 'replace') + break + except Exception: + pass + return ans or os.path.basename(directory) + + +state = None + + +class Repository(object): + def __init__(self, directory, create_watcher): + if isinstance(directory, bytes): + directory = directory.decode(sys.getfilesystemencoding() or sys.getdefaultencoding() or 'utf-8') + self.directory = os.path.abspath(directory) + self.create_watcher = create_watcher + + def status(self, path=None): + '''Return status of repository or file. + + Without file argument: returns status of the repository: + + :"D?": dirty (tracked modified files: added, removed, deleted, modified), + :"?U": untracked-dirty (added, but not tracked files) + :None: clean (status is empty) + + With file argument: returns status of this file: The status codes are + those returned by bzr status -S + ''' + if path is not None: + return get_file_status( + directory=self.directory, + dirstate_file=os.path.join(self.directory, '.bzr', 'checkout', 'dirstate'), + file_path=path, + ignore_file_name='.bzrignore', + get_func=self.do_status, + create_watcher=self.create_watcher, + ) + return self.do_status(self.directory, path) + + def do_status(self, directory, path): + try: + return self._status(self.directory, path) + except Exception: + pass + + def _status(self, directory, path): + global state + if state is None: + state = library_state.BzrLibraryState(ui=ui.SilentUIFactory, trace=trace.DefaultConfig()) + buf = CoerceIO() + w = workingtree.WorkingTree.open(directory) + status.show_tree_status(w, specific_files=[path] if path else None, to_file=buf, short=True) + raw = buf.getvalue() + if not raw.strip(): + return + if path: + ans = raw[:2] + if ans == 'I ': # Ignored + ans = None + return ans + dirtied = untracked = ' ' + for line in raw.splitlines(): + if len(line) > 1 and line[1] in 'ACDMRIN': + dirtied = 'D' + elif line and line[0] == '?': + untracked = 'U' + ans = dirtied + untracked + return ans if ans.strip() else None + + def branch(self): + config_file = os.path.join(self.directory, '.bzr', 'branch', 'branch.conf') + return get_branch_name( + directory=self.directory, + config_file=config_file, + get_func=branch_name_from_config_file, + create_watcher=self.create_watcher, + ) diff --git a/powerline/lib/vcs/git.py b/powerline/lib/vcs/git.py new file mode 100644 index 00000000..21b5e434 --- /dev/null +++ b/powerline/lib/vcs/git.py @@ -0,0 +1,188 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import re + +from powerline.lib.vcs import get_branch_name, get_file_status +from powerline.lib.shell import readlines + + +_ref_pat = re.compile(br'ref:\s*refs/heads/(.+)') + + +def branch_name_from_config_file(directory, config_file): + try: + with open(config_file, 'rb') as f: + raw = f.read() + except EnvironmentError: + return os.path.basename(directory) + m = _ref_pat.match(raw) + if m is not None: + return m.group(1).decode('utf-8', 'replace') + return raw[:7] + + +def git_directory(directory): + path = os.path.join(directory, '.git') + if os.path.isfile(path): + with open(path, 'rb') as f: + raw = f.read() + if not raw.startswith(b'gitdir: '): + raise IOError('invalid gitfile format') + raw = raw[8:].decode(sys.getfilesystemencoding() or 'utf-8') + if raw[-1] == '\n': + raw = raw[:-1] + if not raw: + raise IOError('no path in gitfile') + return os.path.abspath(os.path.join(directory, raw)) + else: + return path + + +class GitRepository(object): + __slots__ = ('directory', 'create_watcher') + + def __init__(self, directory, create_watcher): + self.directory = os.path.abspath(directory) + self.create_watcher = create_watcher + + def status(self, path=None): + '''Return status of repository or file. + + Without file argument: returns status of the repository: + + :First column: working directory status (D: dirty / space) + :Second column: index status (I: index dirty / space) + :Third column: presence of untracked files (U: untracked files / space) + :None: repository clean + + With file argument: returns status of this file. Output is + equivalent to the first two columns of "git status --porcelain" + (except for merge statuses as they are not supported by libgit2). + ''' + if path: + gitd = git_directory(self.directory) + # We need HEAD as without it using fugitive to commit causes the + # current file's status (and only the current file) to not be updated + # for some reason I cannot be bothered to figure out. + return get_file_status( + directory=self.directory, + dirstate_file=os.path.join(gitd, 'index'), + file_path=path, + ignore_file_name='.gitignore', + get_func=self.do_status, + create_watcher=self.create_watcher, + extra_ignore_files=tuple(os.path.join(gitd, x) for x in ('logs/HEAD', 'info/exclude')), + ) + return self.do_status(self.directory, path) + + def branch(self): + directory = git_directory(self.directory) + head = os.path.join(directory, 'HEAD') + return get_branch_name( + directory=directory, + config_file=head, + get_func=branch_name_from_config_file, + create_watcher=self.create_watcher, + ) + + +try: + import pygit2 as git + + class Repository(GitRepository): + @staticmethod + def ignore_event(path, name): + return False + + def do_status(self, directory, path): + if path: + try: + status = git.Repository(directory).status_file(path) + except (KeyError, ValueError): + return None + + if status == git.GIT_STATUS_CURRENT: + return None + else: + if status & git.GIT_STATUS_WT_NEW: + return '??' + if status & git.GIT_STATUS_IGNORED: + return '!!' + + if status & git.GIT_STATUS_INDEX_NEW: + index_status = 'A' + elif status & git.GIT_STATUS_INDEX_DELETED: + index_status = 'D' + elif status & git.GIT_STATUS_INDEX_MODIFIED: + index_status = 'M' + else: + index_status = ' ' + + if status & git.GIT_STATUS_WT_DELETED: + wt_status = 'D' + elif status & git.GIT_STATUS_WT_MODIFIED: + wt_status = 'M' + else: + wt_status = ' ' + + return index_status + wt_status + else: + wt_column = ' ' + index_column = ' ' + untracked_column = ' ' + for status in git.Repository(directory).status().values(): + if status & git.GIT_STATUS_WT_NEW: + untracked_column = 'U' + continue + + if status & (git.GIT_STATUS_WT_DELETED | git.GIT_STATUS_WT_MODIFIED): + wt_column = 'D' + + if status & ( + git.GIT_STATUS_INDEX_NEW + | git.GIT_STATUS_INDEX_MODIFIED + | git.GIT_STATUS_INDEX_DELETED + ): + index_column = 'I' + r = wt_column + index_column + untracked_column + return r if r != ' ' else None +except ImportError: + class Repository(GitRepository): + @staticmethod + def ignore_event(path, name): + # Ignore changes to the index.lock file, since they happen + # frequently and dont indicate an actual change in the working tree + # status + return path.endswith('.git') and name == 'index.lock' + + def _gitcmd(self, directory, *args): + return readlines(('git',) + args, directory) + + def do_status(self, directory, path): + if path: + try: + return next(self._gitcmd(directory, 'status', '--porcelain', '--ignored', '--', path))[:2] + except StopIteration: + return None + else: + wt_column = ' ' + index_column = ' ' + untracked_column = ' ' + for line in self._gitcmd(directory, 'status', '--porcelain'): + if line[0] == '?': + untracked_column = 'U' + continue + elif line[0] == '!': + continue + + if line[0] != ' ': + index_column = 'I' + + if line[1] != ' ': + wt_column = 'D' + + r = wt_column + index_column + untracked_column + return r if r != ' ' else None diff --git a/powerline/lib/vcs/mercurial.py b/powerline/lib/vcs/mercurial.py new file mode 100644 index 00000000..71963dd3 --- /dev/null +++ b/powerline/lib/vcs/mercurial.py @@ -0,0 +1,84 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from mercurial import hg, ui, match + +from powerline.lib.vcs import get_branch_name, get_file_status + + +def branch_name_from_config_file(directory, config_file): + try: + with open(config_file, 'rb') as f: + raw = f.read() + return raw.decode('utf-8', 'replace').strip() + except Exception: + return 'default' + + +class Repository(object): + __slots__ = ('directory', 'ui', 'create_watcher') + + statuses = 'MARDUI' + repo_statuses = (1, 1, 1, 1, 2) + repo_statuses_str = (None, 'D ', ' U', 'DU') + + def __init__(self, directory, create_watcher): + self.directory = os.path.abspath(directory) + self.ui = ui.ui() + self.create_watcher = create_watcher + + def _repo(self, directory): + # Cannot create this object once and use always: when repository updates + # functions emit invalid results + return hg.repository(self.ui, directory) + + def status(self, path=None): + '''Return status of repository or file. + + Without file argument: returns status of the repository: + + :"D?": dirty (tracked modified files: added, removed, deleted, modified), + :"?U": untracked-dirty (added, but not tracked files) + :None: clean (status is empty) + + With file argument: returns status of this file: "M"odified, "A"dded, + "R"emoved, "D"eleted (removed from filesystem, but still tracked), + "U"nknown, "I"gnored, (None)Clean. + ''' + if path: + return get_file_status( + directory=self.directory, + dirstate_file=os.path.join(self.directory, '.hg', 'dirstate'), + file_path=path, + ignore_file_name='.hgignore', + get_func=self.do_status, + create_watcher=self.create_watcher, + ) + return self.do_status(self.directory, path) + + def do_status(self, directory, path): + repo = self._repo(directory) + if path: + m = match.match(None, None, [path], exact=True) + statuses = repo.status(match=m, unknown=True, ignored=True) + for status, paths in zip(self.statuses, statuses): + if paths: + return status + return None + else: + resulting_status = 0 + for status, paths in zip(self.repo_statuses, repo.status(unknown=True)): + if paths: + resulting_status |= status + return self.repo_statuses_str[resulting_status] + + def branch(self): + config_file = os.path.join(self.directory, '.hg', 'branch') + return get_branch_name( + directory=self.directory, + config_file=config_file, + get_func=branch_name_from_config_file, + create_watcher=self.create_watcher, + ) diff --git a/powerline/lib/watcher/__init__.py b/powerline/lib/watcher/__init__.py new file mode 100644 index 00000000..4fe98968 --- /dev/null +++ b/powerline/lib/watcher/__init__.py @@ -0,0 +1,76 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from powerline.lib.watcher.stat import StatFileWatcher +from powerline.lib.watcher.inotify import INotifyFileWatcher +from powerline.lib.watcher.tree import TreeWatcher +from powerline.lib.watcher.uv import UvFileWatcher, UvNotFound +from powerline.lib.inotify import INotifyError + + +def create_file_watcher(pl, watcher_type='auto', expire_time=10): + '''Create an object that can watch for changes to specified files + + Use ``.__call__()`` method of the returned object to start watching the file + or check whether file has changed since last call. + + Use ``.unwatch()`` method of the returned object to stop watching the file. + + Uses inotify if available, then pyuv, otherwise tracks mtimes. expire_time + is the number of minutes after the last query for a given path for the + inotify watch for that path to be automatically removed. This conserves + kernel resources. + + :param PowerlineLogger pl: + Logger. + :param str watcher_type + One of ``inotify`` (linux only), ``uv``, ``stat``, ``auto``. Determines + what watcher will be used. ``auto`` will use ``inotify`` if available, + then ``libuv`` and then fall back to ``stat``. + :param int expire_time: + Number of minutes since last ``.__call__()`` before inotify watcher will + stop watching given file. + ''' + if watcher_type == 'stat': + pl.debug('Using requested stat-based watcher', prefix='watcher') + return StatFileWatcher() + if watcher_type == 'inotify': + # Explicitly selected inotify watcher: do not catch INotifyError then. + pl.debug('Using requested inotify watcher', prefix='watcher') + return INotifyFileWatcher(expire_time=expire_time) + elif watcher_type == 'uv': + pl.debug('Using requested uv watcher', prefix='watcher') + return UvFileWatcher() + + if sys.platform.startswith('linux'): + try: + pl.debug('Trying to use inotify watcher', prefix='watcher') + return INotifyFileWatcher(expire_time=expire_time) + except INotifyError: + pl.info('Failed to create inotify watcher', prefix='watcher') + + try: + pl.debug('Using libuv-based watcher') + return UvFileWatcher() + except UvNotFound: + pl.debug('Failed to import pyuv') + + pl.debug('Using stat-based watcher') + return StatFileWatcher() + + +def create_tree_watcher(pl, watcher_type='auto', expire_time=10): + '''Create an object that can watch for changes in specified directories + + :param PowerlineLogger pl: + Logger. + :param str watcher_type: + Watcher type. Currently the only supported types are ``inotify`` (linux + only), ``uv``, ``dummy`` and ``auto``. + :param int expire_time: + Number of minutes since last ``.__call__()`` before inotify watcher will + stop watching given file. + ''' + return TreeWatcher(pl, watcher_type, expire_time) diff --git a/powerline/lib/watcher/inotify.py b/powerline/lib/watcher/inotify.py new file mode 100644 index 00000000..6708b21e --- /dev/null +++ b/powerline/lib/watcher/inotify.py @@ -0,0 +1,266 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import errno +import os +import ctypes + +from threading import RLock + +from powerline.lib.inotify import INotify +from powerline.lib.monotonic import monotonic +from powerline.lib.path import realpath + + +class INotifyFileWatcher(INotify): + def __init__(self, expire_time=10): + super(INotifyFileWatcher, self).__init__() + self.watches = {} + self.modified = {} + self.last_query = {} + self.lock = RLock() + self.expire_time = expire_time * 60 + + def expire_watches(self): + now = monotonic() + for path, last_query in tuple(self.last_query.items()): + if last_query - now > self.expire_time: + self.unwatch(path) + + def process_event(self, wd, mask, cookie, name): + if wd == -1 and (mask & self.Q_OVERFLOW): + # We missed some INOTIFY events, so we dont + # know the state of any tracked files. + for path in tuple(self.modified): + if os.path.exists(path): + self.modified[path] = True + else: + self.watches.pop(path, None) + self.modified.pop(path, None) + self.last_query.pop(path, None) + return + + for path, num in tuple(self.watches.items()): + if num == wd: + if mask & self.IGNORED: + self.watches.pop(path, None) + self.modified.pop(path, None) + self.last_query.pop(path, None) + else: + if mask & self.ATTRIB: + # The watched file could have had its inode changed, in + # which case we will not get any more events for this + # file, so re-register the watch. For example by some + # other file being renamed as this file. + try: + self.unwatch(path) + except OSError: + pass + try: + self.watch(path) + except OSError as e: + if getattr(e, 'errno', None) != errno.ENOENT: + raise + else: + self.modified[path] = True + else: + self.modified[path] = True + + def unwatch(self, path): + ''' Remove the watch for path. Raises an OSError if removing the watch + fails for some reason. ''' + path = realpath(path) + with self.lock: + self.modified.pop(path, None) + self.last_query.pop(path, None) + wd = self.watches.pop(path, None) + if wd is not None: + if self._rm_watch(self._inotify_fd, wd) != 0: + self.handle_error() + + def watch(self, path): + ''' Register a watch for the file/directory named path. Raises an OSError if path + does not exist. ''' + path = realpath(path) + with self.lock: + if path not in self.watches: + bpath = path if isinstance(path, bytes) else path.encode(self.fenc) + flags = self.MOVE_SELF | self.DELETE_SELF + buf = ctypes.c_char_p(bpath) + # Try watching path as a directory + wd = self._add_watch(self._inotify_fd, buf, flags | self.ONLYDIR) + if wd == -1: + eno = ctypes.get_errno() + if eno != errno.ENOTDIR: + self.handle_error() + # Try watching path as a file + flags |= (self.MODIFY | self.ATTRIB) + wd = self._add_watch(self._inotify_fd, buf, flags) + if wd == -1: + self.handle_error() + self.watches[path] = wd + self.modified[path] = False + + def is_watching(self, path): + with self.lock: + return realpath(path) in self.watches + + def __call__(self, path): + ''' Return True if path has been modified since the last call. Can + raise OSError if the path does not exist. ''' + path = realpath(path) + with self.lock: + self.last_query[path] = monotonic() + self.expire_watches() + if path not in self.watches: + # Try to re-add the watch, it will fail if the file does not + # exist/you dont have permission + self.watch(path) + return True + self.read(get_name=False) + if path not in self.modified: + # An ignored event was received which means the path has been + # automatically unwatched + return True + ans = self.modified[path] + if ans: + self.modified[path] = False + return ans + + def close(self): + with self.lock: + for path in tuple(self.watches): + try: + self.unwatch(path) + except OSError: + pass + super(INotifyFileWatcher, self).close() + + +class NoSuchDir(ValueError): + pass + + +class BaseDirChanged(ValueError): + pass + + +class DirTooLarge(ValueError): + def __init__(self, bdir): + ValueError.__init__(self, 'The directory {0} is too large to monitor. Try increasing the value in /proc/sys/fs/inotify/max_user_watches'.format(bdir)) + + +class INotifyTreeWatcher(INotify): + is_dummy = False + + def __init__(self, basedir, ignore_event=None): + super(INotifyTreeWatcher, self).__init__() + self.basedir = realpath(basedir) + self.watch_tree() + self.modified = True + self.ignore_event = (lambda path, name: False) if ignore_event is None else ignore_event + + def watch_tree(self): + self.watched_dirs = {} + self.watched_rmap = {} + try: + self.add_watches(self.basedir) + except OSError as e: + if e.errno == errno.ENOSPC: + raise DirTooLarge(self.basedir) + + def add_watches(self, base, top_level=True): + ''' Add watches for this directory and all its descendant directories, + recursively. ''' + base = realpath(base) + # There may exist a link which leads to an endless + # add_watches loop or to maximum recursion depth exceeded + if not top_level and base in self.watched_dirs: + return + try: + is_dir = self.add_watch(base) + except OSError as e: + if e.errno == errno.ENOENT: + # The entry could have been deleted between listdir() and + # add_watch(). + if top_level: + raise NoSuchDir('The dir {0} does not exist'.format(base)) + return + if e.errno == errno.EACCES: + # We silently ignore entries for which we dont have permission, + # unless they are the top level dir + if top_level: + raise NoSuchDir('You do not have permission to monitor {0}'.format(base)) + return + raise + else: + if is_dir: + try: + files = os.listdir(base) + except OSError as e: + if e.errno in (errno.ENOTDIR, errno.ENOENT): + # The dir was deleted/replaced between the add_watch() + # and listdir() + if top_level: + raise NoSuchDir('The dir {0} does not exist'.format(base)) + return + raise + for x in files: + self.add_watches(os.path.join(base, x), top_level=False) + elif top_level: + # The top level dir is a file, not good. + raise NoSuchDir('The dir {0} does not exist'.format(base)) + + def add_watch(self, path): + bpath = path if isinstance(path, bytes) else path.encode(self.fenc) + wd = self._add_watch( + self._inotify_fd, + ctypes.c_char_p(bpath), + + # Ignore symlinks and watch only directories + self.DONT_FOLLOW | self.ONLYDIR | + + self.MODIFY | self.CREATE | self.DELETE | + self.MOVE_SELF | self.MOVED_FROM | self.MOVED_TO | + self.ATTRIB | self.DELETE_SELF + ) + if wd == -1: + eno = ctypes.get_errno() + if eno == errno.ENOTDIR: + return False + raise OSError(eno, 'Failed to add watch for: {0}: {1}'.format(path, self.os.strerror(eno))) + self.watched_dirs[path] = wd + self.watched_rmap[wd] = path + return True + + def process_event(self, wd, mask, cookie, name): + if wd == -1 and (mask & self.Q_OVERFLOW): + # We missed some INOTIFY events, so we dont + # know the state of any tracked dirs. + self.watch_tree() + self.modified = True + return + path = self.watched_rmap.get(wd, None) + if path is not None: + if not self.ignore_event(path, name): + self.modified = True + if mask & self.CREATE: + # A new sub-directory might have been created, monitor it. + try: + self.add_watch(os.path.join(path, name)) + except OSError as e: + if e.errno == errno.ENOENT: + # Deleted before add_watch() + pass + elif e.errno == errno.ENOSPC: + raise DirTooLarge(self.basedir) + else: + raise + if (mask & self.DELETE_SELF or mask & self.MOVE_SELF) and path == self.basedir: + raise BaseDirChanged('The directory %s was moved/deleted' % path) + + def __call__(self): + self.read() + ret = self.modified + self.modified = False + return ret diff --git a/powerline/lib/watcher/stat.py b/powerline/lib/watcher/stat.py new file mode 100644 index 00000000..0c089716 --- /dev/null +++ b/powerline/lib/watcher/stat.py @@ -0,0 +1,44 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from threading import RLock + +from powerline.lib.path import realpath + + +class StatFileWatcher(object): + def __init__(self): + self.watches = {} + self.lock = RLock() + + def watch(self, path): + path = realpath(path) + with self.lock: + self.watches[path] = os.path.getmtime(path) + + def unwatch(self, path): + path = realpath(path) + with self.lock: + self.watches.pop(path, None) + + def is_watching(self, path): + with self.lock: + return realpath(path) in self.watches + + def __call__(self, path): + path = realpath(path) + with self.lock: + if path not in self.watches: + self.watches[path] = os.path.getmtime(path) + return True + mtime = os.path.getmtime(path) + if mtime != self.watches[path]: + self.watches[path] = mtime + return True + return False + + def close(self): + with self.lock: + self.watches.clear() diff --git a/powerline/lib/watcher/tree.py b/powerline/lib/watcher/tree.py new file mode 100644 index 00000000..7d2b83f6 --- /dev/null +++ b/powerline/lib/watcher/tree.py @@ -0,0 +1,90 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from powerline.lib.monotonic import monotonic +from powerline.lib.inotify import INotifyError +from powerline.lib.path import realpath +from powerline.lib.watcher.inotify import INotifyTreeWatcher, DirTooLarge, NoSuchDir, BaseDirChanged +from powerline.lib.watcher.uv import UvTreeWatcher, UvNotFound + + +class DummyTreeWatcher(object): + is_dummy = True + + def __init__(self, basedir): + self.basedir = realpath(basedir) + + def __call__(self): + return False + + +class TreeWatcher(object): + def __init__(self, pl, watcher_type, expire_time): + self.watches = {} + self.last_query_times = {} + self.expire_time = expire_time * 60 + self.pl = pl + self.watcher_type = watcher_type + + def get_watcher(self, path, ignore_event): + if self.watcher_type == 'inotify': + return INotifyTreeWatcher(path, ignore_event=ignore_event) + if self.watcher_type == 'uv': + return UvTreeWatcher(path, ignore_event=ignore_event) + if self.watcher_type == 'dummy': + return DummyTreeWatcher(path) + # FIXME + if self.watcher_type == 'stat': + return DummyTreeWatcher(path) + if self.watcher_type == 'auto': + if sys.platform.startswith('linux'): + try: + return INotifyTreeWatcher(path, ignore_event=ignore_event) + except (INotifyError, DirTooLarge) as e: + if not isinstance(e, INotifyError): + self.pl.warn('Failed to watch path: {0} with error: {1}'.format(path, e)) + try: + return UvTreeWatcher(path, ignore_event=ignore_event) + except UvNotFound: + pass + return DummyTreeWatcher(path) + else: + raise ValueError('Unknown watcher type: {0}'.format(self.watcher_type)) + + def watch(self, path, ignore_event=None): + path = realpath(path) + w = self.get_watcher(path, ignore_event) + self.watches[path] = w + return w + + def expire_old_queries(self): + pop = [] + now = monotonic() + for path, lt in self.last_query_times.items(): + if now - lt > self.expire_time: + pop.append(path) + for path in pop: + del self.last_query_times[path] + + def __call__(self, path, ignore_event=None): + path = realpath(path) + self.expire_old_queries() + self.last_query_times[path] = monotonic() + w = self.watches.get(path, None) + if w is None: + try: + self.watch(path, ignore_event=ignore_event) + except NoSuchDir: + pass + return True + try: + return w() + except BaseDirChanged: + self.watches.pop(path, None) + return True + except DirTooLarge as e: + self.pl.warn(str(e)) + self.watches[path] = DummyTreeWatcher(path) + return False diff --git a/powerline/lib/watcher/uv.py b/powerline/lib/watcher/uv.py new file mode 100644 index 00000000..02fdfcc6 --- /dev/null +++ b/powerline/lib/watcher/uv.py @@ -0,0 +1,175 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from collections import defaultdict +from threading import RLock +from functools import partial +from threading import Thread + +from powerline.lib.path import realpath + + +class UvNotFound(NotImplementedError): + pass + + +pyuv = None + + +def import_pyuv(): + global pyuv + if not pyuv: + try: + pyuv = __import__('pyuv') + except ImportError: + raise UvNotFound + + +class UvThread(Thread): + daemon = True + + def __init__(self, loop): + self.uv_loop = loop + super(UvThread, self).__init__() + + def run(self): + while True: + self.uv_loop.run() + + def join(self): + self.uv_loop.stop() + return super(UvThread, self).join() + + +_uv_thread = None + + +def start_uv_thread(): + global _uv_thread + if _uv_thread is None: + loop = pyuv.Loop() + _uv_thread = UvThread(loop) + _uv_thread.start() + return _uv_thread.uv_loop + + +class UvWatcher(object): + def __init__(self): + import_pyuv() + self.watches = {} + self.lock = RLock() + self.loop = start_uv_thread() + + def watch(self, path): + path = realpath(path) + with self.lock: + if path not in self.watches: + try: + self.watches[path] = pyuv.fs.FSEvent( + self.loop, + path, + partial(self._record_event, path), + pyuv.fs.UV_CHANGE | pyuv.fs.UV_RENAME + ) + except pyuv.error.FSEventError as e: + code = e.args[0] + if code == pyuv.errno.UV_ENOENT: + raise OSError('No such file or directory: ' + path) + else: + raise + + def unwatch(self, path): + path = realpath(path) + with self.lock: + try: + watch = self.watches.pop(path) + except KeyError: + return + watch.close(partial(self._stopped_watching, path)) + + def is_watching(self, path): + with self.lock: + return realpath(path) in self.watches + + def __del__(self): + try: + lock = self.lock + except AttributeError: + pass + else: + with lock: + while self.watches: + path, watch = self.watches.popitem() + watch.close(partial(self._stopped_watching, path)) + + +class UvFileWatcher(UvWatcher): + def __init__(self): + super(UvFileWatcher, self).__init__() + self.events = defaultdict(list) + + def _record_event(self, path, fsevent_handle, filename, events, error): + with self.lock: + self.events[path].append(events) + if events | pyuv.fs.UV_RENAME: + if not os.path.exists(path): + self.watches.pop(path).close() + + def _stopped_watching(self, path, *args): + self.events.pop(path, None) + + def __call__(self, path): + path = realpath(path) + with self.lock: + events = self.events.pop(path, None) + if events: + return True + if path not in self.watches: + self.watch(path) + return True + return False + + +class UvTreeWatcher(UvWatcher): + is_dummy = False + + def __init__(self, basedir, ignore_event=None): + super(UvTreeWatcher, self).__init__() + self.ignore_event = ignore_event or (lambda path, name: False) + self.basedir = realpath(basedir) + self.modified = True + self.watch_directory(self.basedir) + + def watch_directory(self, path): + os.path.walk(realpath(path), self.watch_one_directory, None) + + def watch_one_directory(self, arg, dirname, fnames): + try: + self.watch(dirname) + except OSError: + pass + + def _stopped_watching(self, path, *args): + pass + + def _record_event(self, path, fsevent_handle, filename, events, error): + if not self.ignore_event(path, filename): + self.modified = True + if events == pyuv.fs.UV_CHANGE | pyuv.fs.UV_RENAME: + # Stat changes to watched directory are UV_CHANGE|UV_RENAME. It + # is weird. + pass + elif events | pyuv.fs.UV_RENAME: + if not os.path.isdir(path): + self.unwatch(path) + else: + full_name = os.path.join(path, filename) + if os.path.isdir(full_name): + # For some reason mkdir and rmdir both fall into this + # category + self.watch_directory(full_name) + + def __call__(self): + return self.__dict__.pop('modified', False) diff --git a/powerline/lint/__init__.py b/powerline/lint/__init__.py new file mode 100644 index 00000000..6a5d86d5 --- /dev/null +++ b/powerline/lint/__init__.py @@ -0,0 +1,1721 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import itertools +import sys +import os +import re +import logging + +from collections import defaultdict +from copy import copy +from functools import partial + +from powerline.lint.markedjson import load +from powerline import generate_config_finder, get_config_paths, load_config +from powerline.lib.config import ConfigLoader +from powerline.lint.markedjson.error import echoerr, MarkedError, Mark +from powerline.lint.markedjson.markedvalue import MarkedUnicode +from powerline.segments.vim import vim_modes +from powerline.lint.inspect import getconfigargspec +from powerline.lib.threaded import ThreadedSegment +from powerline.lib import mergedicts_copy +from powerline.lib.unicode import unicode + + +def open_file(path): + return open(path, 'rb') + + +EMPTYTUPLE = tuple() + + +class JStr(unicode): + def join(self, iterable): + return super(JStr, self).join((unicode(item) for item in iterable)) + + +key_sep = JStr('/') +list_sep = JStr(', ') + + +def init_context(config): + return ((MarkedUnicode('', config.mark), config),) + + +def context_key(context): + return key_sep.join((c[0] for c in context)) + + +def havemarks(*args, **kwargs): + origin = kwargs.get('origin', '') + for i, v in enumerate(args): + if not hasattr(v, 'mark'): + raise AssertionError('Value #{0}/{1} ({2!r}) has no attribute `mark`'.format(origin, i, v)) + if isinstance(v, dict): + for key, val in v.items(): + havemarks(key, val, origin=(origin + '[' + unicode(i) + ']/' + unicode(key))) + elif isinstance(v, list): + havemarks(*v, origin=(origin + '[' + unicode(i) + ']')) + + +def context_has_marks(context): + for i, v in enumerate(context): + havemarks(v[0], origin='context key') + havemarks(v[1], origin='context val') + + +class EchoErr(object): + __slots__ = ('echoerr', 'logger',) + + def __init__(self, echoerr, logger): + self.echoerr = echoerr + self.logger = logger + + def __call__(self, *args, **kwargs): + self.echoerr(*args, **kwargs) + + +class DelayedEchoErr(EchoErr): + __slots__ = ('echoerr', 'logger', 'errs') + + def __init__(self, echoerr): + super(DelayedEchoErr, self).__init__(echoerr, echoerr.logger) + self.errs = [] + + def __call__(self, *args, **kwargs): + self.errs.append((args, kwargs)) + + def echo_all(self): + for args, kwargs in self.errs: + self.echoerr(*args, **kwargs) + + def __nonzero__(self): + return not not self.errs + + __bool__ = __nonzero__ + + +def new_context_item(key, value): + return ((value.keydict[key], value[key]),) + + +class Spec(object): + def __init__(self, **keys): + self.specs = [] + self.keys = {} + self.checks = [] + self.cmsg = '' + self.isoptional = False + self.uspecs = [] + self.ufailmsg = lambda key: 'found unknown key: {0}'.format(key) + self.did_type = False + self.update(**keys) + + def update(self, **keys): + for k, v in keys.items(): + self.keys[k] = len(self.specs) + self.specs.append(v) + if self.keys and not self.did_type: + self.type(dict) + self.did_type = True + return self + + def copy(self, copied=None): + copied = copied or {} + try: + return copied[id(self)] + except KeyError: + instance = self.__class__() + copied[id(self)] = instance + return self.__class__()._update(self.__dict__, copied) + + def _update(self, d, copied): + self.__dict__.update(d) + self.keys = copy(self.keys) + self.checks = copy(self.checks) + self.uspecs = copy(self.uspecs) + self.specs = [spec.copy(copied) for spec in self.specs] + return self + + def unknown_spec(self, keyfunc, spec): + if isinstance(keyfunc, Spec): + self.specs.append(keyfunc) + keyfunc = len(self.specs) - 1 + self.specs.append(spec) + self.uspecs.append((keyfunc, len(self.specs) - 1)) + return self + + def unknown_msg(self, msgfunc): + self.ufailmsg = msgfunc + return self + + def context_message(self, msg): + self.cmsg = msg + for spec in self.specs: + if not spec.cmsg: + spec.context_message(msg) + return self + + def check_type(self, value, context_mark, data, context, echoerr, types): + havemarks(value) + if type(value.value) not in types: + echoerr( + context=self.cmsg.format(key=context_key(context)), + context_mark=context_mark, + problem='{0!r} must be a {1} instance, not {2}'.format( + value, + list_sep.join((t.__name__ for t in types)), + type(value.value).__name__ + ), + problem_mark=value.mark + ) + return False, True + return True, False + + def check_func(self, value, context_mark, data, context, echoerr, func, msg_func): + havemarks(value) + proceed, echo, hadproblem = func(value, data, context, echoerr) + if echo and hadproblem: + echoerr(context=self.cmsg.format(key=context_key(context)), + context_mark=context_mark, + problem=msg_func(value), + problem_mark=value.mark) + return proceed, hadproblem + + def check_list(self, value, context_mark, data, context, echoerr, item_func, msg_func): + havemarks(value) + i = 0 + hadproblem = False + for item in value: + havemarks(item) + if isinstance(item_func, int): + spec = self.specs[item_func] + proceed, fhadproblem = spec.match( + item, + value.mark, + data, + context + ((MarkedUnicode('list item ' + unicode(i), item.mark), item),), + echoerr + ) + else: + proceed, echo, fhadproblem = item_func(item, data, context, echoerr) + if echo and fhadproblem: + echoerr(context=self.cmsg.format(key=context_key(context) + '/list item ' + unicode(i)), + context_mark=value.mark, + problem=msg_func(item), + problem_mark=item.mark) + if fhadproblem: + hadproblem = True + if not proceed: + return proceed, hadproblem + i += 1 + return True, hadproblem + + def check_either(self, value, context_mark, data, context, echoerr, start, end): + havemarks(value) + new_echoerr = DelayedEchoErr(echoerr) + + hadproblem = False + for spec in self.specs[start:end]: + proceed, hadproblem = spec.match(value, value.mark, data, context, new_echoerr) + if not proceed: + break + if not hadproblem: + return True, False + + new_echoerr.echo_all() + + return False, hadproblem + + def check_tuple(self, value, context_mark, data, context, echoerr, start, end): + havemarks(value) + hadproblem = False + for (i, item, spec) in zip(itertools.count(), value, self.specs[start:end]): + proceed, ihadproblem = spec.match( + item, + value.mark, + data, + context + ((MarkedUnicode('tuple item ' + unicode(i), item.mark), item),), + echoerr + ) + if ihadproblem: + hadproblem = True + if not proceed: + return False, hadproblem + return True, hadproblem + + def type(self, *args): + self.checks.append(('check_type', args)) + return self + + cmp_funcs = { + 'le': lambda x, y: x <= y, + 'lt': lambda x, y: x < y, + 'ge': lambda x, y: x >= y, + 'gt': lambda x, y: x > y, + 'eq': lambda x, y: x == y, + } + + cmp_msgs = { + 'le': 'lesser or equal to', + 'lt': 'lesser then', + 'ge': 'greater or equal to', + 'gt': 'greater then', + 'eq': 'equal to', + } + + def len(self, comparison, cint, msg_func=None): + cmp_func = self.cmp_funcs[comparison] + msg_func = ( + msg_func + or (lambda value: 'length of {0!r} is not {1} {2}'.format( + value, self.cmp_msgs[comparison], cint)) + ) + self.checks.append(( + 'check_func', + (lambda value, *args: (True, True, not cmp_func(len(value), cint))), + msg_func + )) + return self + + def cmp(self, comparison, cint, msg_func=None): + if type(cint) is str: + self.type(unicode) + elif type(cint) is float: + self.type(int, float) + else: + self.type(type(cint)) + cmp_func = self.cmp_funcs[comparison] + msg_func = msg_func or (lambda value: '{0} is not {1} {2}'.format(value, self.cmp_msgs[comparison], cint)) + self.checks.append(( + 'check_func', + (lambda value, *args: (True, True, not cmp_func(value.value, cint))), + msg_func + )) + return self + + def unsigned(self, msg_func=None): + self.type(int) + self.checks.append(( + 'check_func', + (lambda value, *args: (True, True, value < 0)), + (lambda value: '{0} must be greater then zero'.format(value)) + )) + return self + + def list(self, item_func, msg_func=None): + self.type(list) + if isinstance(item_func, Spec): + self.specs.append(item_func) + item_func = len(self.specs) - 1 + self.checks.append(('check_list', item_func, msg_func or (lambda item: 'failed check'))) + return self + + def tuple(self, *specs): + self.type(list) + + max_len = len(specs) + min_len = max_len + for spec in reversed(specs): + if spec.isoptional: + min_len -= 1 + else: + break + if max_len == min_len: + self.len('eq', len(specs)) + else: + self.len('ge', min_len) + self.len('le', max_len) + + start = len(self.specs) + for i, spec in zip(itertools.count(), specs): + self.specs.append(spec) + self.checks.append(('check_tuple', start, len(self.specs))) + return self + + def func(self, func, msg_func=None): + self.checks.append(('check_func', func, msg_func or (lambda value: 'failed check'))) + return self + + def re(self, regex, msg_func=None): + self.type(unicode) + compiled = re.compile(regex) + msg_func = msg_func or (lambda value: 'String "{0}" does not match "{1}"'.format(value, regex)) + self.checks.append(( + 'check_func', + (lambda value, *args: (True, True, not compiled.match(value.value))), + msg_func + )) + return self + + def ident(self, msg_func=None): + msg_func = ( + msg_func + or (lambda value: 'String "{0}" is not an alphanumeric/underscore colon-separated identifier'.format(value)) + ) + return self.re('^\w+(?::\w+)?$', msg_func) + + def oneof(self, collection, msg_func=None): + msg_func = msg_func or (lambda value: '"{0}" must be one of {1!r}'.format(value, list(collection))) + self.checks.append(( + 'check_func', + (lambda value, *args: (True, True, value not in collection)), + msg_func + )) + return self + + def error(self, msg): + self.checks.append(( + 'check_func', + (lambda *args: (True, True, True)), + (lambda value: msg.format(value)) + )) + return self + + def either(self, *specs): + start = len(self.specs) + self.specs.extend(specs) + self.checks.append(('check_either', start, len(self.specs))) + return self + + def optional(self): + self.isoptional = True + return self + + def match_checks(self, *args): + hadproblem = False + for check in self.checks: + proceed, chadproblem = getattr(self, check[0])(*(args + check[1:])) + if chadproblem: + hadproblem = True + if not proceed: + return False, hadproblem + return True, hadproblem + + def match(self, value, context_mark=None, data=None, context=EMPTYTUPLE, echoerr=echoerr): + havemarks(value) + proceed, hadproblem = self.match_checks(value, context_mark, data, context, echoerr) + if proceed: + if self.keys or self.uspecs: + for key, vali in self.keys.items(): + valspec = self.specs[vali] + if key in value: + proceed, mhadproblem = valspec.match( + value[key], + value.mark, + data, + context + new_context_item(key, value), + echoerr + ) + if mhadproblem: + hadproblem = True + if not proceed: + return False, hadproblem + else: + if not valspec.isoptional: + hadproblem = True + echoerr(context=self.cmsg.format(key=context_key(context)), + context_mark=None, + problem='required key is missing: {0}'.format(key), + problem_mark=value.mark) + for key in value.keys(): + havemarks(key) + if key not in self.keys: + for keyfunc, vali in self.uspecs: + valspec = self.specs[vali] + if isinstance(keyfunc, int): + spec = self.specs[keyfunc] + proceed, khadproblem = spec.match(key, context_mark, data, context, echoerr) + else: + proceed, khadproblem = keyfunc(key, data, context, echoerr) + if khadproblem: + hadproblem = True + if proceed: + proceed, vhadproblem = valspec.match( + value[key], + value.mark, + data, + context + new_context_item(key, value), + echoerr + ) + if vhadproblem: + hadproblem = True + break + else: + hadproblem = True + if self.ufailmsg: + echoerr(context=self.cmsg.format(key=context_key(context)), + context_mark=None, + problem=self.ufailmsg(key), + problem_mark=key.mark) + return True, hadproblem + + +class WithPath(object): + def __init__(self, import_paths): + self.import_paths = import_paths + + def __enter__(self): + self.oldpath = sys.path + sys.path = self.import_paths + sys.path + + def __exit__(self, *args): + sys.path = self.oldpath + + +def check_matcher_func(ext, match_name, data, context, echoerr): + havemarks(match_name) + import_paths = [os.path.expanduser(path) for path in context[0][1].get('common', {}).get('paths', [])] + + match_module, separator, match_function = match_name.rpartition('.') + if not separator: + match_module = 'powerline.matchers.{0}'.format(ext) + match_function = match_name + with WithPath(import_paths): + try: + func = getattr(__import__(str(match_module), fromlist=[str(match_function)]), str(match_function)) + except ImportError: + echoerr(context='Error while loading matcher functions', + problem='failed to load module {0}'.format(match_module), + problem_mark=match_name.mark) + return True, False, True + except AttributeError: + echoerr(context='Error while loading matcher functions', + problem='failed to load matcher function {0}'.format(match_function), + problem_mark=match_name.mark) + return True, False, True + + if not callable(func): + echoerr(context='Error while loading matcher functions', + problem='loaded "function" {0} is not callable'.format(match_function), + problem_mark=match_name.mark) + return True, False, True + + if hasattr(func, 'func_code') and hasattr(func.func_code, 'co_argcount'): + if func.func_code.co_argcount != 1: + echoerr( + context='Error while loading matcher functions', + problem=( + 'function {0} accepts {1} arguments instead of 1. ' + 'Are you sure it is the proper function?' + ).format(match_function, func.func_code.co_argcount), + problem_mark=match_name.mark + ) + + return True, False, False + + +def check_ext(ext, data, context, echoerr): + havemarks(ext) + hadsomedirs = False + hadproblem = False + if ext not in data['lists']['exts']: + hadproblem = True + echoerr(context='Error while loading {0} extension configuration'.format(ext), + context_mark=ext.mark, + problem='extension configuration does not exist') + else: + for typ in ('themes', 'colorschemes'): + if ext not in data['configs'][typ] and not data['configs']['top_' + typ]: + hadproblem = True + echoerr(context='Error while loading {0} extension configuration'.format(ext), + context_mark=ext.mark, + problem='{0} configuration does not exist'.format(typ)) + else: + hadsomedirs = True + return hadsomedirs, hadproblem + + +def check_config(d, theme, data, context, echoerr): + context_has_marks(context) + if len(context) == 4: + ext = context[-2][0] + else: + # local_themes + ext = context[-3][0] + if ext not in data['lists']['exts']: + echoerr(context='Error while loading {0} extension configuration'.format(ext), + context_mark=ext.mark, + problem='extension configuration does not exist') + return True, False, True + if ( + (ext not in data['configs'][d] or theme not in data['configs'][d][ext]) + and theme not in data['configs']['top_' + d] + ): + echoerr(context='Error while loading {0} from {1} extension configuration'.format(d[:-1], ext), + problem='failed to find configuration file {0}/{1}/{2}.json'.format(d, ext, theme), + problem_mark=theme.mark) + return True, False, True + return True, False, False + + +def check_top_theme(theme, data, context, echoerr): + context_has_marks(context) + havemarks(theme) + if theme not in data['configs']['top_themes']: + echoerr(context='Error while checking extension configuration (key {key})'.format(key=context_key(context)), + context_mark=context[-2][0].mark, + problem='failed to find top theme {0}'.format(theme), + problem_mark=theme.mark) + return True, False, True + return True, False, False + + +function_name_re = '^(\w+\.)*[a-zA-Z_]\w*$' + + +divider_spec = Spec().type(unicode).len( + 'le', 3, (lambda value: 'Divider {0!r} is too large!'.format(value))).copy +ext_theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy +top_theme_spec = Spec().type(unicode).func(check_top_theme).copy +ext_spec = Spec( + colorscheme=Spec().type(unicode).func( + (lambda *args: check_config('colorschemes', *args)) + ), + theme=ext_theme_spec(), + top_theme=top_theme_spec().optional(), +).copy +gen_components_spec = (lambda *components: Spec().list(Spec().type(unicode).oneof(set(components)))) +main_spec = (Spec( + common=Spec( + default_top_theme=top_theme_spec().optional(), + term_truecolor=Spec().type(bool).optional(), + # Python is capable of loading from zip archives. Thus checking path + # only for existence of the path, not for it being a directory + paths=Spec().list( + (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))) + ) + ), + (lambda value: 'directory does not exist: {0}'.format(os.path.dirname(value))) + ).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(), + 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(), + ).context_message('Error while loading common configuration (key {key})'), + ext=Spec( + vim=ext_spec().update( + components=gen_components_spec('statusline', 'tabline').optional(), + local_themes=Spec( + __tabline__=ext_theme_spec(), + ).unknown_spec( + Spec().re(function_name_re).func(partial(check_matcher_func, 'vim')), + ext_theme_spec() + ), + ).optional(), + ipython=ext_spec().update( + local_themes=Spec( + in2=ext_theme_spec(), + out=ext_theme_spec(), + rewrite=ext_theme_spec(), + ), + ).optional(), + shell=ext_spec().update( + components=gen_components_spec('tmux', 'prompt').optional(), + local_themes=Spec( + continuation=ext_theme_spec(), + select=ext_theme_spec(), + ), + ).optional(), + ).unknown_spec( + check_ext, + ext_spec(), + ).context_message('Error while loading extensions configuration (key {key})'), +).context_message('Error while loading main configuration')) + +term_color_spec = Spec().unsigned().cmp('le', 255).copy +true_color_spec = Spec().re( + '^[0-9a-fA-F]{6}$', + (lambda value: '"{0}" is not a six-digit hexadecimal unsigned integer written as a string'.format(value)) +).copy +colors_spec = (Spec( + colors=Spec().unknown_spec( + Spec().ident(), + Spec().either( + Spec().tuple(term_color_spec(), true_color_spec()), + term_color_spec() + ) + ).context_message('Error while checking colors (key {key})'), + gradients=Spec().unknown_spec( + Spec().ident(), + Spec().tuple( + Spec().len('gt', 1).list(term_color_spec()), + Spec().len('gt', 1).list(true_color_spec()).optional(), + ) + ).context_message('Error while checking gradients (key {key})'), +).context_message('Error while loading colors configuration')) + + +def check_color(color, data, context, echoerr): + havemarks(color) + if (color not in data['colors_config'].get('colors', {}) + and color not in data['colors_config'].get('gradients', {})): + echoerr( + context='Error while checking highlight group in colorscheme (key {key})'.format( + key=context_key(context)), + problem='found unexistent color or gradient {0}'.format(color), + problem_mark=color.mark + ) + return True, False, True + return True, False, False + + +def check_translated_group_name(group, data, context, echoerr): + return check_group(group, data, context, echoerr) + + +def check_group(group, data, context, echoerr): + havemarks(group) + if not isinstance(group, unicode): + return True, False, False + colorscheme = data['colorscheme'] + ext = data['ext'] + configs = [] + if ext: + if colorscheme == '__main__': + configs.append([config for config in data['ext_colorscheme_configs'][ext].items()]) + configs.append([config for config in data['top_colorscheme_configs'].items()]) + else: + try: + configs.append([data['ext_colorscheme_configs'][ext][colorscheme]]) + except KeyError: + pass + try: + configs.append([data['ext_colorscheme_configs'][ext]['__main__']]) + except KeyError: + pass + try: + configs.append([data['top_colorscheme_configs'][colorscheme]]) + except KeyError: + pass + else: + try: + configs.append([data['top_colorscheme_configs'][colorscheme]]) + except KeyError: + pass + new_echoerr = DelayedEchoErr(echoerr) + hadproblem = False + for config_lst in configs: + tofind = len(config_lst) + not_found = [] + for config in config_lst: + if isinstance(config, tuple): + new_colorscheme, config = config + new_data = data.copy() + new_data['colorscheme'] = new_colorscheme + else: + new_data = data + havemarks(config) + try: + group_data = config['groups'][group] + except KeyError: + not_found.append(config.mark.name) + else: + proceed, echo, chadproblem = check_group( + group_data, + new_data, + context, + echoerr, + ) + if chadproblem: + hadproblem = True + else: + tofind -= 1 + if not tofind: + return proceed, echo, hadproblem + if not proceed: + break + if not_found: + new_echoerr( + context='Error while checking group definition in colorscheme (key {key})'.format( + key=context_key(context)), + problem='name {0} is not present in {1} {2} colorschemes: {3}'.format( + group, tofind, ext, ', '.join(not_found)), + problem_mark=group.mark + ) + new_echoerr.echo_all() + return True, False, hadproblem + + +color_spec = Spec().type(unicode).func(check_color).copy +name_spec = Spec().type(unicode).len('gt', 0).optional().copy +group_name_spec = Spec().ident().copy +group_spec = Spec().either(Spec( + fg=color_spec(), + bg=color_spec(), + attr=Spec().list(Spec().type(unicode).oneof(set(('bold', 'italic', 'underline')))), +), group_name_spec().func(check_group)).copy +groups_spec = Spec().unknown_spec( + group_name_spec(), + group_spec(), +).context_message('Error while loading groups (key {key})').copy +colorscheme_spec = (Spec( + name=name_spec(), + groups=groups_spec(), +).context_message('Error while loading coloscheme')) +mode_translations_value_spec = Spec( + colors=Spec().unknown_spec( + color_spec(), + color_spec(), + ).optional(), + groups=Spec().unknown_spec( + group_name_spec().func(check_translated_group_name), + group_spec(), + ).optional(), +).copy +top_colorscheme_spec = (Spec( + name=name_spec(), + groups=groups_spec(), + mode_translations=Spec().unknown_spec( + Spec().type(unicode), + mode_translations_value_spec(), + ).optional().context_message('Error while loading mode translations (key {key})').optional(), +).context_message('Error while loading top-level coloscheme')) +vim_mode_spec = Spec().oneof(set(list(vim_modes) + ['nc', 'tab_nc', 'buf_nc'])).copy +vim_colorscheme_spec = (Spec( + name=name_spec(), + groups=groups_spec(), + mode_translations=Spec().unknown_spec( + vim_mode_spec(), + mode_translations_value_spec(), + ).optional().context_message('Error while loading mode translations (key {key})'), +).context_message('Error while loading vim colorscheme')) +shell_mode_spec = Spec().re('^(?:[\w\-]+|\.safe)$').copy +shell_colorscheme_spec = (Spec( + name=name_spec(), + groups=groups_spec(), + mode_translations=Spec().unknown_spec( + shell_mode_spec(), + mode_translations_value_spec(), + ).optional().context_message('Error while loading mode translations (key {key})'), +).context_message('Error while loading shell colorscheme')) + + +generic_keys = set(( + 'exclude_modes', 'include_modes', + 'exclude_function', 'include_function', + 'width', 'align', + 'name', + 'draw_soft_divider', 'draw_hard_divider', + 'priority', + 'after', 'before', + 'display' +)) +type_keys = { + 'function': set(('function', 'args', 'draw_inner_divider')), + 'string': set(('contents', 'type', 'highlight_group', 'divider_highlight_group')), + 'segment_list': set(('function', 'segments', 'args', 'type')), +} +required_keys = { + 'function': set(('function',)), + 'string': set(()), + 'segment_list': set(('function', 'segments',)), +} +highlight_keys = set(('highlight_group', 'name')) + + +def check_key_compatibility(segment, data, context, echoerr): + context_has_marks(context) + havemarks(segment) + segment_type = segment.get('type', MarkedUnicode('function', None)) + havemarks(segment_type) + + if segment_type not in type_keys: + echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)), + problem='found segment with unknown type {0}'.format(segment_type), + problem_mark=segment_type.mark) + return False, False, True + + hadproblem = False + + keys = set(segment) + if not ((keys - generic_keys) < type_keys[segment_type]): + unknown_keys = keys - generic_keys - type_keys[segment_type] + echoerr( + context='Error while checking segments (key {key})'.format(key=context_key(context)), + context_mark=context[-1][1].mark, + problem='found keys not used with the current segment type: {0}'.format( + list_sep.join(unknown_keys)), + problem_mark=list(unknown_keys)[0].mark + ) + hadproblem = True + + if not (keys >= required_keys[segment_type]): + missing_keys = required_keys[segment_type] - keys + echoerr( + context='Error while checking segments (key {key})'.format(key=context_key(context)), + context_mark=context[-1][1].mark, + problem='found missing required keys: {0}'.format( + list_sep.join(missing_keys)) + ) + hadproblem = True + + if not (segment_type == 'function' or (keys & highlight_keys)): + echoerr( + context='Error while checking segments (key {key})'.format(key=context_key(context)), + context_mark=context[-1][1].mark, + problem=( + 'found missing keys required to determine highlight group. ' + 'Either highlight_group or name key must be present' + ) + ) + hadproblem = True + + return True, False, hadproblem + + +def check_segment_module(module, data, context, echoerr): + havemarks(module) + with WithPath(data['import_paths']): + try: + __import__(str(module)) + except ImportError as e: + if echoerr.logger.level >= logging.DEBUG: + echoerr.logger.exception(e) + echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)), + problem='failed to import module {0}'.format(module), + problem_mark=module.mark) + return True, False, True + return True, False, False + + +def get_function_strings(function_name, context, ext): + if '.' in function_name: + module, function_name = function_name.rpartition('.')[::2] + else: + module = context[0][1].get( + 'default_module', MarkedUnicode('powerline.segments.' + ext, None)) + return module, function_name + + +def check_full_segment_data(segment, data, context, echoerr): + if 'name' not in segment and 'function' not in segment: + return True, False, False + + ext = data['ext'] + theme_segment_data = context[0][1].get('segment_data', {}) + main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None) + if not main_theme_name or data['theme'] == main_theme_name: + top_segment_data = {} + else: + top_segment_data = data['ext_theme_configs'].get(main_theme_name, {}).get('segment_data', {}) + + if segment.get('type', 'function') == 'function': + function_name = segment.get('function') + if function_name: + module, function_name = get_function_strings(function_name, context, ext) + names = [module + '.' + function_name, function_name] + else: + names = [] + elif segment.get('name'): + names = [segment['name']] + else: + return True, False, False + + segment_copy = segment.copy() + + for key in ('before', 'after', 'args', 'contents'): + if key not in segment_copy: + for segment_data in [theme_segment_data, top_segment_data]: + for name in names: + try: + val = segment_data[name][key] + k = segment_data[name].keydict[key] + segment_copy[k] = val + except KeyError: + pass + + return check_key_compatibility(segment_copy, data, context, echoerr) + + +def import_function(function_type, name, data, context, echoerr, module): + context_has_marks(context) + havemarks(name, module) + + with WithPath(data['import_paths']): + try: + func = getattr(__import__(str(module), fromlist=[str(name)]), str(name)) + except ImportError: + echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)), + context_mark=name.mark, + problem='failed to import module {0}'.format(module), + problem_mark=module.mark) + return None + except AttributeError: + echoerr(context='Error while loading {0} function (key {key})'.format(function_type, key=context_key(context)), + problem='failed to load function {0} from module {1}'.format(name, module), + problem_mark=name.mark) + return None + + if not callable(func): + echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)), + context_mark=name.mark, + problem='imported "function" {0} from module {1} is not callable'.format(name, module), + problem_mark=module.mark) + return None + + return func + + +def import_segment(*args, **kwargs): + return import_function('segment', *args, **kwargs) + + +def check_segment_function(function_name, data, context, echoerr): + havemarks(function_name) + ext = data['ext'] + module, function_name = get_function_strings(function_name, context, ext) + if context[-2][1].get('type', 'function') == 'function': + func = import_segment(function_name, data, context, echoerr, module=module) + + if not func: + return True, False, True + + hl_groups = [] + divider_hl_group = None + + if func.__doc__: + H_G_USED_STR = 'Highlight groups used: ' + LHGUS = len(H_G_USED_STR) + D_H_G_USED_STR = 'Divider highlight group used: ' + LDHGUS = len(D_H_G_USED_STR) + pointer = 0 + mark_name = '<{0} docstring>'.format(function_name) + for i, line in enumerate(func.__doc__.split('\n')): + if H_G_USED_STR in line: + idx = line.index(H_G_USED_STR) + LHGUS + hl_groups.append(( + line[idx:], + (mark_name, i + 1, idx + 1, func.__doc__), + pointer + idx + )) + elif D_H_G_USED_STR in line: + idx = line.index(D_H_G_USED_STR) + LDHGUS + 2 + mark = Mark(mark_name, i + 1, idx + 1, func.__doc__, pointer + idx) + divider_hl_group = MarkedUnicode(line[idx:-3], mark) + pointer += len(line) + len('\n') + + hadproblem = False + + if divider_hl_group: + r = hl_exists(divider_hl_group, data, context, echoerr, allow_gradients=True) + if r: + echoerr( + context='Error while checking theme (key {key})'.format(key=context_key(context)), + problem=( + 'found highlight group {0} not defined in the following colorschemes: {1}\n' + '(Group name was obtained from function documentation.)' + ).format(divider_hl_group, list_sep.join(r)), + problem_mark=function_name.mark + ) + hadproblem = True + + if hl_groups: + greg = re.compile(r'``([^`]+)``( \(gradient\))?') + parsed_hl_groups = [] + for line, mark_args, pointer in hl_groups: + for s in line.split(', '): + required_pack = [] + sub_pointer = pointer + for subs in s.split(' or '): + match = greg.match(subs) + try: + if not match: + continue + hl_group = MarkedUnicode( + match.group(1), + Mark(*mark_args, pointer=sub_pointer + match.start(1)) + ) + gradient = bool(match.group(2)) + required_pack.append((hl_group, gradient)) + finally: + sub_pointer += len(subs) + len(' or ') + parsed_hl_groups.append(required_pack) + pointer += len(s) + len(', ') + del hl_group, gradient + for required_pack in parsed_hl_groups: + rs = [ + hl_exists(hl_group, data, context, echoerr, allow_gradients=('force' if gradient else False)) + for hl_group, gradient in required_pack + ] + if all(rs): + echoerr( + context='Error while checking theme (key {key})'.format(key=context_key(context)), + problem=( + 'found highlight groups list ({0}) with all groups not defined in some colorschemes\n' + '(Group names were taken from function documentation.)' + ).format(list_sep.join((h[0] for h in required_pack))), + problem_mark=function_name.mark + ) + for r, h in zip(rs, required_pack): + echoerr( + context='Error while checking theme (key {key})'.format(key=context_key(context)), + problem='found highlight group {0} not defined in the following colorschemes: {1}'.format( + h[0], list_sep.join(r)) + ) + hadproblem = True + else: + r = hl_exists(function_name, data, context, echoerr, allow_gradients=True) + if r: + echoerr( + context='Error while checking theme (key {key})'.format(key=context_key(context)), + problem=( + 'found highlight group {0} not defined in the following colorschemes: {1}\n' + '(If not specified otherwise in documentation, ' + 'highlight group for function segments\n' + 'is the same as the function name.)' + ).format(function_name, list_sep.join(r)), + problem_mark=function_name.mark + ) + hadproblem = True + + return True, False, hadproblem + elif context[-2][1].get('type') != 'segment_list': + if function_name not in context[0][1].get('segment_data', {}): + main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None) + if data['theme'] == main_theme_name: + main_theme = {} + else: + main_theme = data['ext_theme_configs'].get(main_theme_name, {}) + if ( + function_name not in main_theme.get('segment_data', {}) + and function_name not in data['ext_theme_configs'].get('__main__', {}).get('segment_data', {}) + and not any(((function_name in theme.get('segment_data', {})) for theme in data['top_themes'].values())) + ): + echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)), + problem='found useless use of name key (such name is not present in theme/segment_data)', + problem_mark=function_name.mark) + + return True, False, False + + +def hl_exists(hl_group, data, context, echoerr, allow_gradients=False): + havemarks(hl_group) + ext = data['ext'] + if ext not in data['colorscheme_configs']: + # No colorschemes. Error was already reported, no need to report it + # twice + return [] + r = [] + for colorscheme, cconfig in data['colorscheme_configs'][ext].items(): + if hl_group not in cconfig.get('groups', {}): + r.append(colorscheme) + elif not allow_gradients or allow_gradients == 'force': + group_config = cconfig['groups'][hl_group] + havemarks(group_config) + hadgradient = False + for ckey in ('fg', 'bg'): + color = group_config.get(ckey) + if not color: + # No color. Error was already reported. + continue + havemarks(color) + # Gradients are only allowed for function segments. Note that + # whether *either* color or gradient exists should have been + # already checked + hascolor = color in data['colors_config'].get('colors', {}) + hasgradient = color in data['colors_config'].get('gradients', {}) + if hasgradient: + hadgradient = True + if allow_gradients is False and not hascolor and hasgradient: + echoerr( + context='Error while checking highlight group in theme (key {key})'.format( + key=context_key(context)), + context_mark=hl_group.mark, + problem='group {0} is using gradient {1} instead of a color'.format(hl_group, color), + problem_mark=color.mark + ) + r.append(colorscheme) + continue + if allow_gradients == 'force' and not hadgradient: + echoerr( + context='Error while checking highlight group in theme (key {key})'.format( + key=context_key(context)), + context_mark=hl_group.mark, + problem='group {0} should have at least one gradient color, but it has no'.format(hl_group), + problem_mark=group_config.mark + ) + r.append(colorscheme) + return r + + +def check_highlight_group(hl_group, data, context, echoerr): + havemarks(hl_group) + r = hl_exists(hl_group, data, context, echoerr) + if r: + echoerr( + context='Error while checking theme (key {key})'.format(key=context_key(context)), + problem='found highlight group {0} not defined in the following colorschemes: {1}'.format( + hl_group, list_sep.join(r)), + problem_mark=hl_group.mark + ) + return True, False, True + return True, False, False + + +def check_highlight_groups(hl_groups, data, context, echoerr): + havemarks(hl_groups) + rs = [hl_exists(hl_group, data, context, echoerr) for hl_group in hl_groups] + if all(rs): + echoerr( + context='Error while checking theme (key {key})'.format(key=context_key(context)), + problem='found highlight groups list ({0}) with all groups not defined in some colorschemes'.format( + list_sep.join((unicode(h) for h in hl_groups))), + problem_mark=hl_groups.mark + ) + for r, hl_group in zip(rs, hl_groups): + echoerr( + context='Error while checking theme (key {key})'.format(key=context_key(context)), + problem='found highlight group {0} not defined in the following colorschemes: {1}'.format( + hl_group, list_sep.join(r)), + problem_mark=hl_group.mark + ) + return True, False, True + return True, False, False + + +def list_themes(data, context): + theme_type = data['theme_type'] + ext = data['ext'] + main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None) + is_main_theme = (data['theme'] == main_theme_name) + if theme_type == 'top': + return list(itertools.chain(*[ + [(theme_ext, theme) for theme in theme_configs.values()] + for theme_ext, theme_configs in data['theme_configs'].items() + ])) + elif theme_type == 'main' or is_main_theme: + return [(ext, theme) for theme in data['ext_theme_configs'].values()] + else: + return [(ext, context[0][1])] + + +def check_segment_data_key(key, data, context, echoerr): + havemarks(key) + has_module_name = '.' in key + found = False + for ext, theme in list_themes(data, context): + for segments in theme.get('segments', {}).values(): + for segment in segments: + if 'name' in segment: + if key == segment['name']: + found = True + break + else: + function_name = segment.get('function') + if function_name: + module, function_name = get_function_strings(function_name, ((None, theme),), ext) + if has_module_name: + full_name = module + '.' + function_name + if key == full_name: + found = True + break + else: + if key == function_name: + found = True + break + if found: + break + if found: + break + else: + if data['theme_type'] != 'top': + echoerr(context='Error while checking segment data', + problem='found key {0} that cannot be associated with any segment'.format(key), + problem_mark=key.mark) + return True, False, True + + return True, False, False + + +threaded_args_specs = { + 'interval': Spec().cmp('gt', 0.0), + 'update_first': Spec().type(bool), + 'shutdown_event': Spec().error('Shutdown event must be set by powerline'), +} + + +def check_args_variant(func, args, data, context, echoerr): + havemarks(args) + argspec = getconfigargspec(func) + present_args = set(args) + all_args = set(argspec.args) + required_args = set(argspec.args[:-len(argspec.defaults)]) + + hadproblem = False + + if required_args - present_args: + echoerr( + context='Error while checking segment arguments (key {key})'.format(key=context_key(context)), + context_mark=args.mark, + problem='some of the required keys are missing: {0}'.format(list_sep.join(required_args - present_args)) + ) + hadproblem = True + + if not all_args >= present_args: + echoerr(context='Error while checking segment arguments (key {key})'.format(key=context_key(context)), + context_mark=args.mark, + problem='found unknown keys: {0}'.format(list_sep.join(present_args - all_args)), + problem_mark=next(iter(present_args - all_args)).mark) + hadproblem = True + + if isinstance(func, ThreadedSegment): + for key in set(threaded_args_specs) & present_args: + proceed, khadproblem = threaded_args_specs[key].match( + args[key], + args.mark, + data, + context + new_context_item(key, args), + echoerr + ) + if khadproblem: + hadproblem = True + if not proceed: + return hadproblem + + return hadproblem + + +def check_args(get_functions, args, data, context, echoerr): + context_has_marks(context) + new_echoerr = DelayedEchoErr(echoerr) + count = 0 + hadproblem = False + for func in get_functions(data, context, new_echoerr): + count += 1 + shadproblem = check_args_variant(func, args, data, context, echoerr) + if shadproblem: + hadproblem = True + + if not count: + hadproblem = True + if new_echoerr: + new_echoerr.echo_all() + else: + echoerr(context='Error while checking segment arguments (key {key})'.format(key=context_key(context)), + context_mark=context[-2][1].mark, + problem='no suitable segments found') + + return True, False, hadproblem + + +def get_one_segment_function(data, context, echoerr): + ext = data['ext'] + function_name = context[-2][1].get('function') + if function_name: + module, function_name = get_function_strings(function_name, context, ext) + func = import_segment(function_name, data, context, echoerr, module=module) + if func: + yield func + + +def get_all_possible_functions(data, context, echoerr): + name = context[-2][0] + module, name = name.rpartition('.')[::2] + if module: + func = import_segment(name, data, context, echoerr, module=module) + if func: + yield func + else: + for ext, theme_config in list_themes(data, context): + for segments in theme_config.get('segments', {}).values(): + for segment in segments: + if segment.get('type', 'function') == 'function': + function_name = segment.get('function') + current_name = segment.get('name') + if function_name: + module, function_name = get_function_strings(function_name, ((None, theme_config),), ext) + if current_name == name or function_name == name: + func = import_segment(function_name, data, context, echoerr, module=module) + if func: + yield func + + +def check_exinclude_function(name, data, context, echoerr): + ext = data['ext'] + module, name = name.rpartition('.')[::2] + if not module: + module = MarkedUnicode('powerline.selectors.' + ext, None) + func = import_function('selector', name, data, context, echoerr, module=module) + if not func: + return True, False, True + return True, False, False + + +args_spec = Spec( + pl=Spec().error('pl object must be set by powerline').optional(), + segment_info=Spec().error('Segment info dictionary must be set by powerline').optional(), +).unknown_spec(Spec(), Spec()).optional().copy +highlight_group_spec = Spec().type(unicode).copy +segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy +sub_segments_spec = Spec() +exinclude_spec = Spec().re(function_name_re).func(check_exinclude_function).copy +segment_spec = Spec( + type=Spec().oneof(type_keys).optional(), + name=Spec().re('^[a-zA-Z_]\w*$').optional(), + function=Spec().re(function_name_re).func(check_segment_function).optional(), + exclude_modes=Spec().list(vim_mode_spec()).optional(), + include_modes=Spec().list(vim_mode_spec()).optional(), + exclude_function=exinclude_spec().optional(), + include_function=exinclude_spec().optional(), + draw_hard_divider=Spec().type(bool).optional(), + draw_soft_divider=Spec().type(bool).optional(), + draw_inner_divider=Spec().type(bool).optional(), + display=Spec().type(bool).optional(), + module=segment_module_spec(), + priority=Spec().type(int, float, type(None)).optional(), + after=Spec().type(unicode).optional(), + before=Spec().type(unicode).optional(), + width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(), + align=Spec().oneof(set('lr')).optional(), + args=args_spec().func(lambda *args, **kwargs: check_args(get_one_segment_function, *args, **kwargs)), + contents=Spec().type(unicode).optional(), + highlight_group=Spec().list( + highlight_group_spec().re( + '^(?:(?!:divider$).)+$', + (lambda value: 'it is recommended that only divider highlight group names end with ":divider"') + ) + ).func(check_highlight_groups).optional(), + divider_highlight_group=highlight_group_spec().func(check_highlight_group).re( + ':divider$', + (lambda value: 'it is recommended that divider highlight group names end with ":divider"') + ).optional(), + segments=sub_segments_spec, +).func(check_full_segment_data) +sub_segments_spec.optional().list(segment_spec) +segments_spec = Spec().optional().list(segment_spec).copy +segdict_spec = Spec( + left=segments_spec().context_message('Error while loading segments from left side (key {key})'), + right=segments_spec().context_message('Error while loading segments from right side (key {key})'), +).func( + (lambda value, *args: (True, True, not (('left' in value) or ('right' in value)))), + (lambda value: 'segments dictionary must contain either left, right or both keys') +).context_message('Error while loading segments (key {key})').copy +divside_spec = Spec( + hard=divider_spec(), + soft=divider_spec(), +).copy +segment_data_value_spec = Spec( + after=Spec().type(unicode).optional(), + before=Spec().type(unicode).optional(), + display=Spec().type(bool).optional(), + args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_functions, *args, **kwargs)), + contents=Spec().type(unicode).optional(), +).copy +dividers_spec = Spec( + left=divside_spec(), + right=divside_spec(), +).copy +spaces_spec = Spec().unsigned().cmp( + 'le', 2, (lambda value: 'Are you sure you need such a big ({0}) number of spaces?'.format(value)) +).copy +common_theme_spec = Spec( + default_module=segment_module_spec().optional(), + cursor_space=Spec().type(int, float).cmp('le', 100).cmp('gt', 0).optional(), + cursor_columns=Spec().type(int).cmp('gt', 0).optional(), +).context_message('Error while loading theme').copy +top_theme_spec = common_theme_spec().update( + dividers=dividers_spec(), + spaces=spaces_spec(), + use_non_breaking_spaces=Spec().type(bool).optional(), + segment_data=Spec().unknown_spec( + Spec().func(check_segment_data_key), + segment_data_value_spec(), + ).optional().context_message('Error while loading segment data (key {key})'), +) +main_theme_spec = common_theme_spec().update( + dividers=dividers_spec().optional(), + spaces=spaces_spec().optional(), + segment_data=Spec().unknown_spec( + Spec().func(check_segment_data_key), + segment_data_value_spec(), + ).optional().context_message('Error while loading segment data (key {key})'), +) +theme_spec = common_theme_spec().update( + dividers=dividers_spec().optional(), + spaces=spaces_spec().optional(), + segment_data=Spec().unknown_spec( + Spec().func(check_segment_data_key), + segment_data_value_spec(), + ).optional().context_message('Error while loading segment data (key {key})'), + segments=segdict_spec().update(above=Spec().list(segdict_spec()).optional()), +) + + +def generate_json_config_loader(lhadproblem): + def load_json_config(config_file_path, load=load, open_file=open_file): + with open_file(config_file_path) as config_file_fp: + r, hadproblem = load(config_file_fp) + if hadproblem: + lhadproblem[0] = True + return r + return load_json_config + + +def check(paths=None, debug=False): + search_paths = paths or get_config_paths() + find_config_files = generate_config_finder(lambda: search_paths) + + logger = logging.getLogger('powerline-lint') + logger.setLevel(logging.DEBUG if debug else logging.ERROR) + logger.addHandler(logging.StreamHandler()) + + ee = EchoErr(echoerr, logger) + + lhadproblem = [False] + load_json_config = generate_json_config_loader(lhadproblem) + + config_loader = ConfigLoader(run_once=True, load=load_json_config) + + paths = { + 'themes': defaultdict(lambda: []), + 'colorschemes': defaultdict(lambda: []), + 'top_colorschemes': [], + 'top_themes': [], + } + lists = { + 'colorschemes': set(), + 'themes': set(), + 'exts': set(), + } + for path in reversed(search_paths): + for typ in ('themes', 'colorschemes'): + d = os.path.join(path, typ) + if os.path.isdir(d): + for subp in os.listdir(d): + extpath = os.path.join(d, subp) + if os.path.isdir(extpath): + lists['exts'].add(subp) + paths[typ][subp].append(extpath) + elif extpath.endswith('.json'): + name = subp[:-5] + if name != '__main__': + lists[typ].add(name) + paths['top_' + typ].append(extpath) + else: + hadproblem = True + sys.stderr.write('Path {0} is supposed to be a directory, but it is not\n'.format(d)) + + hadproblem = False + + configs = defaultdict(lambda: defaultdict(lambda: {})) + for typ in ('themes', 'colorschemes'): + for ext in paths[typ]: + for d in paths[typ][ext]: + for subp in os.listdir(d): + if subp.endswith('.json'): + name = subp[:-5] + if name != '__main__': + lists[typ].add(name) + if name.startswith('__') or name.endswith('__'): + hadproblem = True + sys.stderr.write('File name is not supposed to start or end with “__”: {0}'.format( + os.path.join(d, subp) + )) + configs[typ][ext][name] = os.path.join(d, subp) + for path in paths['top_' + typ]: + name = os.path.basename(path)[:-5] + configs['top_' + typ][name] = path + + diff = set(configs['colorschemes']) - set(configs['themes']) + if diff: + hadproblem = True + for ext in diff: + typ = 'colorschemes' if ext in configs['themes'] else 'themes' + if not configs['top_' + typ] or typ == 'themes': + sys.stderr.write('{0} extension {1} not present in {2}\n'.format( + ext, + 'configuration' if (ext in paths['themes'] and ext in paths['colorschemes']) else 'directory', + typ, + )) + + try: + main_config = load_config('config', find_config_files, config_loader) + except IOError: + main_config = {} + sys.stderr.write('\nConfiguration file not found: config.json\n') + hadproblem = True + except MarkedError as e: + main_config = {} + sys.stderr.write(str(e) + '\n') + hadproblem = True + else: + if main_spec.match( + main_config, + data={'configs': configs, 'lists': lists}, + context=init_context(main_config), + echoerr=ee + )[1]: + hadproblem = True + + import_paths = [os.path.expanduser(path) for path in main_config.get('common', {}).get('paths', [])] + + try: + colors_config = load_config('colors', find_config_files, config_loader) + except IOError: + colors_config = {} + sys.stderr.write('\nConfiguration file not found: colors.json\n') + hadproblem = True + except MarkedError as e: + colors_config = {} + sys.stderr.write(str(e) + '\n') + hadproblem = True + else: + if colors_spec.match(colors_config, context=init_context(colors_config), echoerr=ee)[1]: + hadproblem = True + + if lhadproblem[0]: + hadproblem = True + + top_colorscheme_configs = {} + data = { + 'ext': None, + 'top_colorscheme_configs': top_colorscheme_configs, + 'ext_colorscheme_configs': {}, + 'colors_config': colors_config + } + for colorscheme, cfile in configs['top_colorschemes'].items(): + with open_file(cfile) as config_file_fp: + try: + config, lhadproblem = load(config_file_fp) + except MarkedError as e: + sys.stderr.write(str(e) + '\n') + hadproblem = True + continue + if lhadproblem: + hadproblem = True + top_colorscheme_configs[colorscheme] = config + data['colorscheme'] = colorscheme + if top_colorscheme_spec.match(config, context=init_context(config), data=data, echoerr=ee)[1]: + hadproblem = True + + ext_colorscheme_configs = defaultdict(lambda: {}) + for ext in configs['colorschemes']: + for colorscheme, cfile in configs['colorschemes'][ext].items(): + with open_file(cfile) as config_file_fp: + try: + config, lhadproblem = load(config_file_fp) + except MarkedError as e: + sys.stderr.write(str(e) + '\n') + hadproblem = True + continue + if lhadproblem: + hadproblem = True + ext_colorscheme_configs[ext][colorscheme] = config + + for ext, econfigs in ext_colorscheme_configs.items(): + data = { + 'ext': ext, + 'top_colorscheme_configs': top_colorscheme_configs, + 'ext_colorscheme_configs': ext_colorscheme_configs, + 'colors_config': colors_config, + } + for colorscheme, config in econfigs.items(): + data['colorscheme'] = colorscheme + if ext == 'vim': + spec = vim_colorscheme_spec + elif ext == 'shell': + spec = shell_colorscheme_spec + else: + spec = colorscheme_spec + if spec.match(config, context=init_context(config), data=data, echoerr=ee)[1]: + hadproblem = True + + colorscheme_configs = {} + for ext in lists['exts']: + colorscheme_configs[ext] = {} + for colorscheme in lists['colorschemes']: + econfigs = ext_colorscheme_configs[ext] + ecconfigs = econfigs.get(colorscheme) + mconfigs = ( + top_colorscheme_configs.get(colorscheme), + econfigs.get('__main__'), + ecconfigs, + ) + config = None + for mconfig in mconfigs: + if not mconfig: + continue + if config: + config = mergedicts_copy(config, mconfig) + else: + config = mconfig + colorscheme_configs[colorscheme] = config + + theme_configs = defaultdict(lambda: {}) + for ext in configs['themes']: + for theme, sfile in configs['themes'][ext].items(): + with open_file(sfile) as config_file_fp: + try: + config, lhadproblem = load(config_file_fp) + except MarkedError as e: + sys.stderr.write(str(e) + '\n') + hadproblem = True + continue + if lhadproblem: + hadproblem = True + theme_configs[ext][theme] = config + + top_theme_configs = {} + for top_theme, top_theme_file in configs['top_themes'].items(): + with open_file(top_theme_file) as config_file_fp: + try: + config, lhadproblem = load(config_file_fp) + except MarkedError as e: + sys.stderr.write(str(e) + '\n') + hadproblem = True + continue + if lhadproblem: + hadproblem = True + top_theme_configs[top_theme] = config + + for ext, configs in theme_configs.items(): + data = { + 'ext': ext, + 'colorscheme_configs': colorscheme_configs, + 'import_paths': import_paths, + 'main_config': main_config, + 'top_themes': top_theme_configs, + 'ext_theme_configs': configs, + 'colors_config': colors_config + } + for theme, config in configs.items(): + data['theme'] = theme + if theme == '__main__': + data['theme_type'] = 'main' + spec = main_theme_spec + else: + data['theme_type'] = 'regular' + spec = theme_spec + if spec.match(config, context=init_context(config), data=data, echoerr=ee)[1]: + hadproblem = True + + for top_theme, config in top_theme_configs.items(): + data = { + 'ext': ext, + 'colorscheme_configs': colorscheme_configs, + 'import_paths': import_paths, + 'main_config': main_config, + 'theme_configs': theme_configs, + 'ext_theme_configs': configs, + 'colors_config': colors_config + } + data['theme_type'] = 'top' + data['theme'] = top_theme + if top_theme_spec.match(config, context=init_context(config), data=data, echoerr=ee)[1]: + hadproblem = True + + return hadproblem diff --git a/powerline/lint/inspect.py b/powerline/lint/inspect.py new file mode 100644 index 00000000..b6e0380b --- /dev/null +++ b/powerline/lint/inspect.py @@ -0,0 +1,61 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from inspect import ArgSpec, getargspec + +from powerline.segments import Segment + + +def getconfigargspec(obj): + if hasattr(obj, 'powerline_origin'): + obj = obj.powerline_origin + else: + obj = obj + + args = [] + defaults = [] + + if isinstance(obj, Segment): + additional_args = obj.additional_args() + argspecobjs = obj.argspecobjs() + get_omitted_args = obj.omitted_args + else: + additional_args = () + argspecobjs = ((None, obj),) + get_omitted_args = lambda *args: () + + for arg in additional_args: + args.append(arg[0]) + if len(arg) > 1: + defaults.append(arg[1]) + + requires_segment_info = hasattr(obj, 'powerline_requires_segment_info') + requires_filesystem_watcher = hasattr(obj, 'powerline_requires_filesystem_watcher') + + for name, method in argspecobjs: + argspec = getargspec(method) + omitted_args = get_omitted_args(name, method) + largs = len(argspec.args) + for i, arg in enumerate(reversed(argspec.args)): + if ( + largs - (i + 1) in omitted_args + or arg == 'pl' + or (arg == 'create_watcher' and requires_filesystem_watcher) + or (arg == 'segment_info' and requires_segment_info) + ): + continue + if argspec.defaults and len(argspec.defaults) > i: + if arg in args: + idx = args.index(arg) + if len(args) - idx > len(defaults): + args.pop(idx) + else: + continue + default = argspec.defaults[-(i + 1)] + defaults.append(default) + args.append(arg) + else: + if arg not in args: + args.insert(0, arg) + + return ArgSpec(args=args, varargs=None, keywords=None, defaults=tuple(defaults)) diff --git a/powerline/lint/markedjson/__init__.py b/powerline/lint/markedjson/__init__.py new file mode 100644 index 00000000..aa084eaf --- /dev/null +++ b/powerline/lint/markedjson/__init__.py @@ -0,0 +1,17 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lint.markedjson.loader import Loader + + +def load(stream, Loader=Loader): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + """ + loader = Loader(stream) + try: + r = loader.get_single_data() + return r, loader.haserrors + finally: + loader.dispose() diff --git a/powerline/lint/markedjson/composer.py b/powerline/lint/markedjson/composer.py new file mode 100644 index 00000000..3067af04 --- /dev/null +++ b/powerline/lint/markedjson/composer.py @@ -0,0 +1,119 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lint.markedjson import nodes +from powerline.lint.markedjson import events +from powerline.lint.markedjson.error import MarkedError + + +__all__ = ['Composer', 'ComposerError'] + + +class ComposerError(MarkedError): + pass + + +class Composer: + def __init__(self): + pass + + def check_node(self): + # Drop the STREAM-START event. + if self.check_event(events.StreamStartEvent): + self.get_event() + + # If there are more documents available? + return not self.check_event(events.StreamEndEvent) + + def get_node(self): + # Get the root node of the next document. + if not self.check_event(events.StreamEndEvent): + return self.compose_document() + + def get_single_node(self): + # Drop the STREAM-START event. + self.get_event() + + # Compose a document if the stream is not empty. + document = None + if not self.check_event(events.StreamEndEvent): + document = self.compose_document() + + # Ensure that the stream contains no more documents. + if not self.check_event(events.StreamEndEvent): + event = self.get_event() + raise ComposerError( + "expected a single document in the stream", + document.start_mark, + "but found another document", + event.start_mark + ) + + # Drop the STREAM-END event. + self.get_event() + + return document + + def compose_document(self): + # Drop the DOCUMENT-START event. + self.get_event() + + # Compose the root node. + node = self.compose_node(None, None) + + # Drop the DOCUMENT-END event. + self.get_event() + + return node + + def compose_node(self, parent, index): + self.descend_resolver(parent, index) + if self.check_event(events.ScalarEvent): + node = self.compose_scalar_node() + elif self.check_event(events.SequenceStartEvent): + node = self.compose_sequence_node() + elif self.check_event(events.MappingStartEvent): + node = self.compose_mapping_node() + self.ascend_resolver() + return node + + def compose_scalar_node(self): + event = self.get_event() + tag = event.tag + if tag is None or tag == '!': + tag = self.resolve(nodes.ScalarNode, event.value, event.implicit, event.start_mark) + node = nodes.ScalarNode(tag, event.value, event.start_mark, event.end_mark, style=event.style) + return node + + def compose_sequence_node(self): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == '!': + tag = self.resolve(nodes.SequenceNode, None, start_event.implicit) + node = nodes.SequenceNode(tag, [], start_event.start_mark, None, flow_style=start_event.flow_style) + index = 0 + while not self.check_event(events.SequenceEndEvent): + node.value.append(self.compose_node(node, index)) + index += 1 + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + + def compose_mapping_node(self): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == '!': + tag = self.resolve(nodes.MappingNode, None, start_event.implicit) + node = nodes.MappingNode(tag, [], start_event.start_mark, None, flow_style=start_event.flow_style) + while not self.check_event(events.MappingEndEvent): + # key_event = self.peek_event() + item_key = self.compose_node(node, None) + # if item_key in node.value: + # raise ComposerError("while composing a mapping", start_event.start_mark, + # "found duplicate key", key_event.start_mark) + item_value = self.compose_node(node, item_key) + # node.value[item_key] = item_value + node.value.append((item_key, item_value)) + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node diff --git a/powerline/lint/markedjson/constructor.py b/powerline/lint/markedjson/constructor.py new file mode 100644 index 00000000..f51f25ac --- /dev/null +++ b/powerline/lint/markedjson/constructor.py @@ -0,0 +1,285 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import collections +import types + +from functools import wraps + +from powerline.lint.markedjson.error import MarkedError + +from powerline.lint.markedjson import nodes +from powerline.lint.markedjson.markedvalue import gen_marked_value +from powerline.lib.unicode import unicode + + +def marked(func): + @wraps(func) + def f(self, node, *args, **kwargs): + return gen_marked_value(func(self, node, *args, **kwargs), node.start_mark) + return f + + +class ConstructorError(MarkedError): + pass + + +class BaseConstructor: + yaml_constructors = {} + + def __init__(self): + self.constructed_objects = {} + self.state_generators = [] + self.deep_construct = False + + def check_data(self): + # If there are more documents available? + return self.check_node() + + def get_data(self): + # Construct and return the next document. + if self.check_node(): + return self.construct_document(self.get_node()) + + def get_single_data(self): + # Ensure that the stream contains a single document and construct it. + node = self.get_single_node() + if node is not None: + return self.construct_document(node) + return None + + def construct_document(self, node): + data = self.construct_object(node) + while self.state_generators: + state_generators = self.state_generators + self.state_generators = [] + for generator in state_generators: + for dummy in generator: + pass + self.constructed_objects = {} + self.deep_construct = False + return data + + def construct_object(self, node, deep=False): + if node in self.constructed_objects: + return self.constructed_objects[node] + if deep: + old_deep = self.deep_construct + self.deep_construct = True + constructor = None + tag_suffix = None + if node.tag in self.yaml_constructors: + constructor = self.yaml_constructors[node.tag] + else: + raise ConstructorError(None, None, 'no constructor for tag %s' % node.tag) + if tag_suffix is None: + data = constructor(self, node) + else: + data = constructor(self, tag_suffix, node) + if isinstance(data, types.GeneratorType): + generator = data + data = next(generator) + if self.deep_construct: + for dummy in generator: + pass + else: + self.state_generators.append(generator) + self.constructed_objects[node] = data + if deep: + self.deep_construct = old_deep + return data + + @marked + def construct_scalar(self, node): + if not isinstance(node, nodes.ScalarNode): + raise ConstructorError( + None, None, + "expected a scalar node, but found %s" % node.id, + node.start_mark + ) + return node.value + + def construct_sequence(self, node, deep=False): + if not isinstance(node, nodes.SequenceNode): + raise ConstructorError( + None, None, + "expected a sequence node, but found %s" % node.id, + node.start_mark + ) + return [ + self.construct_object(child, deep=deep) + for child in node.value + ] + + @marked + def construct_mapping(self, node, deep=False): + if not isinstance(node, nodes.MappingNode): + raise ConstructorError( + None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark + ) + mapping = {} + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + if not isinstance(key, collections.Hashable): + self.echoerr( + 'While constructing a mapping', node.start_mark, + 'found unhashable key', key_node.start_mark + ) + continue + elif type(key.value) != unicode: + self.echoerr( + 'Error while constructing a mapping', node.start_mark, + 'found key that is not a string', key_node.start_mark + ) + continue + elif key in mapping: + self.echoerr( + 'Error while constructing a mapping', node.start_mark, + 'found duplicate key', key_node.start_mark + ) + continue + value = self.construct_object(value_node, deep=deep) + mapping[key] = value + return mapping + + @classmethod + def add_constructor(cls, tag, constructor): + if 'yaml_constructors' not in cls.__dict__: + cls.yaml_constructors = cls.yaml_constructors.copy() + cls.yaml_constructors[tag] = constructor + + +class Constructor(BaseConstructor): + def construct_scalar(self, node): + if isinstance(node, nodes.MappingNode): + for key_node, value_node in node.value: + if key_node.tag == 'tag:yaml.org,2002:value': + return self.construct_scalar(value_node) + return BaseConstructor.construct_scalar(self, node) + + def flatten_mapping(self, node): + merge = [] + index = 0 + while index < len(node.value): + key_node, value_node = node.value[index] + if key_node.tag == 'tag:yaml.org,2002:merge': + del node.value[index] + if isinstance(value_node, nodes.MappingNode): + self.flatten_mapping(value_node) + merge.extend(value_node.value) + elif isinstance(value_node, nodes.SequenceNode): + submerge = [] + for subnode in value_node.value: + if not isinstance(subnode, nodes.MappingNode): + raise ConstructorError( + "while constructing a mapping", + node.start_mark, + "expected a mapping for merging, but found %s" % subnode.id, + subnode.start_mark + ) + self.flatten_mapping(subnode) + submerge.append(subnode.value) + submerge.reverse() + for value in submerge: + merge.extend(value) + else: + raise ConstructorError( + "while constructing a mapping", + node.start_mark, + ("expected a mapping or list of mappings for merging, but found %s" % value_node.id), + value_node.start_mark + ) + elif key_node.tag == 'tag:yaml.org,2002:value': + key_node.tag = 'tag:yaml.org,2002:str' + index += 1 + else: + index += 1 + if merge: + node.value = merge + node.value + + def construct_mapping(self, node, deep=False): + if isinstance(node, nodes.MappingNode): + self.flatten_mapping(node) + return BaseConstructor.construct_mapping(self, node, deep=deep) + + @marked + def construct_yaml_null(self, node): + self.construct_scalar(node) + return None + + @marked + def construct_yaml_bool(self, node): + value = self.construct_scalar(node).value + return bool(value) + + @marked + def construct_yaml_int(self, node): + value = self.construct_scalar(node).value + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '0': + return 0 + else: + return sign * int(value) + + @marked + def construct_yaml_float(self, node): + value = self.construct_scalar(node).value + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + else: + return sign * float(value) + + def construct_yaml_str(self, node): + return self.construct_scalar(node) + + def construct_yaml_seq(self, node): + data = gen_marked_value([], node.start_mark) + yield data + data.extend(self.construct_sequence(node)) + + def construct_yaml_map(self, node): + data = gen_marked_value({}, node.start_mark) + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_undefined(self, node): + raise ConstructorError( + None, None, + "could not determine a constructor for the tag %r" % node.tag, + node.start_mark + ) + + +Constructor.add_constructor( + 'tag:yaml.org,2002:null', Constructor.construct_yaml_null) + +Constructor.add_constructor( + 'tag:yaml.org,2002:bool', Constructor.construct_yaml_bool) + +Constructor.add_constructor( + 'tag:yaml.org,2002:int', Constructor.construct_yaml_int) + +Constructor.add_constructor( + 'tag:yaml.org,2002:float', Constructor.construct_yaml_float) + +Constructor.add_constructor( + 'tag:yaml.org,2002:str', Constructor.construct_yaml_str) + +Constructor.add_constructor( + 'tag:yaml.org,2002:seq', Constructor.construct_yaml_seq) + +Constructor.add_constructor( + 'tag:yaml.org,2002:map', Constructor.construct_yaml_map) + +Constructor.add_constructor( + None, Constructor.construct_undefined) diff --git a/powerline/lint/markedjson/error.py b/powerline/lint/markedjson/error.py new file mode 100644 index 00000000..643c86b3 --- /dev/null +++ b/powerline/lint/markedjson/error.py @@ -0,0 +1,100 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import re + +from powerline.lib.unicode import unichr + + +NON_PRINTABLE = re.compile('[^\t\n\x20-\x7E' + unichr(0x85) + (unichr(0xA0) + '-' + unichr(0xD7FF)) + (unichr(0xE000) + '-' + unichr(0xFFFD)) + ']') + + +def repl(s): + return '' % ord(s.group()) + + +def strtrans(s): + return NON_PRINTABLE.sub(repl, s.replace('\t', '>---')) + + +class Mark: + def __init__(self, name, line, column, buffer, pointer): + self.name = name + self.line = line + self.column = column + self.buffer = buffer + self.pointer = pointer + + def copy(self): + return Mark(self.name, self.line, self.column, self.buffer, self.pointer) + + def get_snippet(self, indent=4, max_length=75): + if self.buffer is None: + return None + head = '' + start = self.pointer + while start > 0 and self.buffer[start - 1] not in '\0\n': + start -= 1 + if self.pointer - start > max_length / 2 - 1: + head = ' ... ' + start += 5 + break + tail = '' + end = self.pointer + while end < len(self.buffer) and self.buffer[end] not in '\0\n': + end += 1 + if end - self.pointer > max_length / 2 - 1: + tail = ' ... ' + end -= 5 + break + snippet = [self.buffer[start:self.pointer], self.buffer[self.pointer], self.buffer[self.pointer + 1:end]] + snippet = [strtrans(s) for s in snippet] + return ( + ' ' * indent + head + ''.join(snippet) + tail + '\n' + + ' ' * (indent + len(head) + len(snippet[0])) + '^' + ) + + def __str__(self): + snippet = self.get_snippet() + where = (" in \"%s\", line %d, column %d" % ( + self.name, self.line + 1, self.column + 1)) + if snippet is not None: + where += ":\n" + snippet + if type(where) is str: + return where + else: + return where.encode('utf-8') + + +def echoerr(*args, **kwargs): + sys.stderr.write('\n') + sys.stderr.write(format_error(*args, **kwargs) + '\n') + + +def format_error(context=None, context_mark=None, problem=None, problem_mark=None, note=None): + lines = [] + if context is not None: + lines.append(context) + if ( + context_mark is not None + and ( + problem is None or problem_mark is None + or context_mark.name != problem_mark.name + or context_mark.line != problem_mark.line + or context_mark.column != problem_mark.column + ) + ): + lines.append(str(context_mark)) + if problem is not None: + lines.append(problem) + if problem_mark is not None: + lines.append(str(problem_mark)) + if note is not None: + lines.append(note) + return '\n'.join(lines) + + +class MarkedError(Exception): + def __init__(self, context=None, context_mark=None, problem=None, problem_mark=None, note=None): + Exception.__init__(self, format_error(context, context_mark, problem, problem_mark, note)) diff --git a/powerline/lint/markedjson/events.py b/powerline/lint/markedjson/events.py new file mode 100644 index 00000000..ef8a70e5 --- /dev/null +++ b/powerline/lint/markedjson/events.py @@ -0,0 +1,97 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + + +# Abstract classes. +class Event(object): + def __init__(self, start_mark=None, end_mark=None): + self.start_mark = start_mark + self.end_mark = end_mark + + def __repr__(self): + attributes = [ + key for key in ['implicit', 'value'] + if hasattr(self, key) + ] + arguments = ', '.join([ + '%s=%r' % (key, getattr(self, key)) + for key in attributes + ]) + return '%s(%s)' % (self.__class__.__name__, arguments) + + +class NodeEvent(Event): + def __init__(self, start_mark=None, end_mark=None): + self.start_mark = start_mark + self.end_mark = end_mark + + +class CollectionStartEvent(NodeEvent): + def __init__(self, implicit, start_mark=None, end_mark=None, flow_style=None): + self.tag = None + self.implicit = implicit + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + + +class CollectionEndEvent(Event): + pass + + +# Implementations. +class StreamStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + + +class StreamEndEvent(Event): + pass + + +class DocumentStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, explicit=None, version=None, tags=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + self.version = version + self.tags = tags + + +class DocumentEndEvent(Event): + def __init__(self, start_mark=None, end_mark=None, explicit=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + + +class AliasEvent(NodeEvent): + pass + + +class ScalarEvent(NodeEvent): + def __init__(self, implicit, value, start_mark=None, end_mark=None, style=None): + self.tag = None + self.implicit = implicit + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + + +class SequenceStartEvent(CollectionStartEvent): + pass + + +class SequenceEndEvent(CollectionEndEvent): + pass + + +class MappingStartEvent(CollectionStartEvent): + pass + + +class MappingEndEvent(CollectionEndEvent): + pass diff --git a/powerline/lint/markedjson/loader.py b/powerline/lint/markedjson/loader.py new file mode 100644 index 00000000..3ee56866 --- /dev/null +++ b/powerline/lint/markedjson/loader.py @@ -0,0 +1,25 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lint.markedjson.reader import Reader +from powerline.lint.markedjson.scanner import Scanner +from powerline.lint.markedjson.parser import Parser +from powerline.lint.markedjson.composer import Composer +from powerline.lint.markedjson.constructor import Constructor +from powerline.lint.markedjson.resolver import Resolver +from powerline.lint.markedjson.error import echoerr + + +class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver): + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + Constructor.__init__(self) + Resolver.__init__(self) + self.haserrors = False + + def echoerr(self, *args, **kwargs): + echoerr(*args, **kwargs) + self.haserrors = True diff --git a/powerline/lint/markedjson/markedvalue.py b/powerline/lint/markedjson/markedvalue.py new file mode 100644 index 00000000..74a62b64 --- /dev/null +++ b/powerline/lint/markedjson/markedvalue.py @@ -0,0 +1,135 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lib.unicode import unicode + + +def gen_new(cls): + def __new__(arg_cls, value, mark): + r = super(arg_cls, arg_cls).__new__(arg_cls, value) + r.mark = mark + r.value = value + return r + return __new__ + + +def gen_init(cls): + def __init__(self, value, mark): + return cls.__init__(self, value) + return __init__ + + +def gen_getnewargs(cls): + def __getnewargs__(self): + return (self.value, self.mark) + return __getnewargs__ + + +class MarkedUnicode(unicode): + __new__ = gen_new(unicode) + __getnewargs__ = gen_getnewargs(unicode) + + def _proc_partition(self, part_result): + pointdiff = 1 + r = [] + for s in part_result: + mark = self.mark.copy() + # XXX Does not work properly with escaped strings, but this requires + # saving much more information in mark. + mark.column += pointdiff + mark.pointer += pointdiff + r.append(MarkedUnicode(s, mark)) + pointdiff += len(s) + return tuple(r) + + def rpartition(self, sep): + return self._proc_partition(super(MarkedUnicode, self).rpartition(sep)) + + def partition(self, sep): + return self._proc_partition(super(MarkedUnicode, self).partition(sep)) + + +class MarkedInt(int): + __new__ = gen_new(int) + __getnewargs__ = gen_getnewargs(int) + + +class MarkedFloat(float): + __new__ = gen_new(float) + __getnewargs__ = gen_getnewargs(float) + + +class MarkedDict(dict): + __init__ = gen_init(dict) + __getnewargs__ = gen_getnewargs(dict) + + def __new__(arg_cls, value, mark): + r = super(arg_cls, arg_cls).__new__(arg_cls, value) + r.mark = mark + r.value = value + r.keydict = dict(((key, key) for key in r)) + return r + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + self.keydict[key] = key + + def update(self, *args, **kwargs): + dict.update(self, *args, **kwargs) + self.keydict = dict(((key, key) for key in self)) + + def copy(self): + return MarkedDict(super(MarkedDict, self).copy(), self.mark) + + +class MarkedList(list): + __new__ = gen_new(list) + __init__ = gen_init(list) + __getnewargs__ = gen_getnewargs(list) + + +class MarkedValue: + def __init__(self, value, mark): + self.mark = mark + self.value = value + + __getinitargs__ = gen_getnewargs(None) + + +specialclasses = { + unicode: MarkedUnicode, + int: MarkedInt, + float: MarkedFloat, + dict: MarkedDict, + list: MarkedList, +} + +classcache = {} + + +def gen_marked_value(value, mark, use_special_classes=True): + if use_special_classes and value.__class__ in specialclasses: + Marked = specialclasses[value.__class__] + elif value.__class__ in classcache: + Marked = classcache[value.__class__] + else: + class Marked(MarkedValue): + for func in value.__class__.__dict__: + if func == 'copy': + def copy(self): + return self.__class__(self.value.copy(), self.mark) + elif func not in set(('__init__', '__new__', '__getattribute__')): + if func in set(('__eq__',)): + # HACK to make marked dictionaries always work + exec (( + 'def {0}(self, *args):\n' + ' return self.value.{0}(*[arg.value if isinstance(arg, MarkedValue) else arg for arg in args])' + ).format(func)) + else: + exec (( + 'def {0}(self, *args, **kwargs):\n' + ' return self.value.{0}(*args, **kwargs)\n' + ).format(func)) + classcache[value.__class__] = Marked + + return Marked(value, mark) diff --git a/powerline/lint/markedjson/nodes.py b/powerline/lint/markedjson/nodes.py new file mode 100644 index 00000000..66ad8433 --- /dev/null +++ b/powerline/lint/markedjson/nodes.py @@ -0,0 +1,55 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + + +class Node(object): + def __init__(self, tag, value, start_mark, end_mark): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + + def __repr__(self): + value = self.value + # if isinstance(value, list): + # if len(value) == 0: + # value = '' + # elif len(value) == 1: + # value = '<1 item>' + # else: + # value = '<%d items>' % len(value) + # else: + # if len(value) > 75: + # value = repr(value[:70]+u' ... ') + # else: + # value = repr(value) + value = repr(value) + return '%s(tag=%r, value=%s)' % (self.__class__.__name__, self.tag, value) + + +class ScalarNode(Node): + id = 'scalar' + + def __init__(self, tag, value, start_mark=None, end_mark=None, style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + + +class CollectionNode(Node): + def __init__(self, tag, value, start_mark=None, end_mark=None, flow_style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + + +class SequenceNode(CollectionNode): + id = 'sequence' + + +class MappingNode(CollectionNode): + id = 'mapping' diff --git a/powerline/lint/markedjson/parser.py b/powerline/lint/markedjson/parser.py new file mode 100644 index 00000000..960b74a3 --- /dev/null +++ b/powerline/lint/markedjson/parser.py @@ -0,0 +1,255 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lint.markedjson.error import MarkedError +from powerline.lint.markedjson import tokens +from powerline.lint.markedjson import events + + +class ParserError(MarkedError): + pass + + +class Parser: + def __init__(self): + self.current_event = None + self.yaml_version = None + self.states = [] + self.marks = [] + self.state = self.parse_stream_start + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def check_event(self, *choices): + # Check the type of the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + if self.current_event is not None: + if not choices: + return True + for choice in choices: + if isinstance(self.current_event, choice): + return True + return False + + def peek_event(self): + # Get the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + return self.current_event + + def get_event(self): + # Get the next event and proceed further. + if self.current_event is None: + if self.state: + self.current_event = self.state() + value = self.current_event + self.current_event = None + return value + + # stream ::= STREAM-START implicit_document? explicit_document* STREAM-END + # implicit_document ::= block_node DOCUMENT-END* + # explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + + def parse_stream_start(self): + # Parse the stream start. + token = self.get_token() + event = events.StreamStartEvent(token.start_mark, token.end_mark, encoding=token.encoding) + + # Prepare the next state. + self.state = self.parse_implicit_document_start + + return event + + def parse_implicit_document_start(self): + # Parse an implicit document. + if not self.check_token(tokens.StreamEndToken): + token = self.peek_token() + start_mark = end_mark = token.start_mark + event = events.DocumentStartEvent(start_mark, end_mark, explicit=False) + + # Prepare the next state. + self.states.append(self.parse_document_end) + self.state = self.parse_node + + return event + + else: + return self.parse_document_start() + + def parse_document_start(self): + # Parse an explicit document. + if not self.check_token(tokens.StreamEndToken): + token = self.peek_token() + self.echoerr( + None, None, + ("expected '', but found %r" % token.id), token.start_mark + ) + return events.StreamEndEvent(token.start_mark, token.end_mark) + else: + # Parse the end of the stream. + token = self.get_token() + event = events.StreamEndEvent(token.start_mark, token.end_mark) + assert not self.states + assert not self.marks + self.state = None + return event + + def parse_document_end(self): + # Parse the document end. + token = self.peek_token() + start_mark = end_mark = token.start_mark + explicit = False + event = events.DocumentEndEvent(start_mark, end_mark, explicit=explicit) + + # Prepare the next state. + self.state = self.parse_document_start + + return event + + def parse_document_content(self): + return self.parse_node() + + def parse_node(self, indentless_sequence=False): + start_mark = end_mark = None + if start_mark is None: + start_mark = end_mark = self.peek_token().start_mark + event = None + implicit = True + if self.check_token(tokens.ScalarToken): + token = self.get_token() + end_mark = token.end_mark + if token.plain: + implicit = (True, False) + else: + implicit = (False, True) + event = events.ScalarEvent(implicit, token.value, start_mark, end_mark, style=token.style) + self.state = self.states.pop() + elif self.check_token(tokens.FlowSequenceStartToken): + end_mark = self.peek_token().end_mark + event = events.SequenceStartEvent(implicit, start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_sequence_first_entry + elif self.check_token(tokens.FlowMappingStartToken): + end_mark = self.peek_token().end_mark + event = events.MappingStartEvent(implicit, start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_mapping_first_key + else: + token = self.peek_token() + raise ParserError( + "while parsing a flow node", start_mark, + "expected the node content, but found %r" % token.id, + token.start_mark + ) + return event + + def parse_flow_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_sequence_entry(first=True) + + def parse_flow_sequence_entry(self, first=False): + if not self.check_token(tokens.FlowSequenceEndToken): + if not first: + if self.check_token(tokens.FlowEntryToken): + self.get_token() + if self.check_token(tokens.FlowSequenceEndToken): + token = self.peek_token() + self.echoerr( + "While parsing a flow sequence", self.marks[-1], + ("expected sequence value, but got %r" % token.id), token.start_mark + ) + else: + token = self.peek_token() + raise ParserError( + "while parsing a flow sequence", self.marks[-1], + ("expected ',' or ']', but got %r" % token.id), token.start_mark + ) + + if not self.check_token(tokens.FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry) + return self.parse_node() + token = self.get_token() + event = events.SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_sequence_entry_mapping_end(self): + self.state = self.parse_flow_sequence_entry + token = self.peek_token() + return events.MappingEndEvent(token.start_mark, token.start_mark) + + def parse_flow_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_mapping_key(first=True) + + def parse_flow_mapping_key(self, first=False): + if not self.check_token(tokens.FlowMappingEndToken): + if not first: + if self.check_token(tokens.FlowEntryToken): + self.get_token() + if self.check_token(tokens.FlowMappingEndToken): + token = self.peek_token() + self.echoerr( + "While parsing a flow mapping", self.marks[-1], + ("expected mapping key, but got %r" % token.id), token.start_mark + ) + else: + token = self.peek_token() + raise ParserError( + "while parsing a flow mapping", self.marks[-1], + ("expected ',' or '}', but got %r" % token.id), token.start_mark + ) + if self.check_token(tokens.KeyToken): + token = self.get_token() + if not self.check_token(tokens.ValueToken, tokens.FlowEntryToken, tokens.FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_value) + return self.parse_node() + else: + token = self.peek_token() + raise ParserError( + "while parsing a flow mapping", self.marks[-1], + ("expected value, but got %r" % token.id), token.start_mark + ) + elif not self.check_token(tokens.FlowMappingEndToken): + token = self.peek_token() + expect_key = self.check_token(tokens.ValueToken, tokens.FlowEntryToken) + if not expect_key: + self.get_token() + expect_key = self.check_token(tokens.ValueToken) + + if expect_key: + raise ParserError( + "while parsing a flow mapping", self.marks[-1], + ("expected string key, but got %r" % token.id), token.start_mark + ) + else: + token = self.peek_token() + raise ParserError( + "while parsing a flow mapping", self.marks[-1], + ("expected ':', but got %r" % token.id), token.start_mark + ) + token = self.get_token() + event = events.MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_mapping_value(self): + if self.check_token(tokens.ValueToken): + token = self.get_token() + if not self.check_token(tokens.FlowEntryToken, tokens.FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_key) + return self.parse_node() + + token = self.peek_token() + raise ParserError( + "while parsing a flow mapping", self.marks[-1], + ("expected mapping value, but got %r" % token.id), token.start_mark + ) diff --git a/powerline/lint/markedjson/reader.py b/powerline/lint/markedjson/reader.py new file mode 100644 index 00000000..e212a2bd --- /dev/null +++ b/powerline/lint/markedjson/reader.py @@ -0,0 +1,136 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import codecs + +from powerline.lint.markedjson.error import MarkedError, Mark, NON_PRINTABLE +from powerline.lib.unicode import unicode + + +# This module contains abstractions for the input stream. You don't have to +# looks further, there are no pretty code. + + +class ReaderError(MarkedError): + pass + + +class Reader(object): + # Reader: + # - determines the data encoding and converts it to a unicode string, + # - checks if characters are in allowed range, + # - adds '\0' to the end. + + # Reader accepts + # - a file-like object with its `read` method returning `str`, + + # Yeah, it's ugly and slow. + def __init__(self, stream): + self.name = None + self.stream = None + self.stream_pointer = 0 + self.eof = True + self.buffer = '' + self.pointer = 0 + self.full_buffer = unicode('') + self.full_pointer = 0 + self.raw_buffer = None + self.raw_decode = codecs.utf_8_decode + self.encoding = 'utf-8' + self.index = 0 + self.line = 0 + self.column = 0 + + self.stream = stream + self.name = getattr(stream, 'name', "") + self.eof = False + self.raw_buffer = None + + while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2): + self.update_raw() + self.update(1) + + def peek(self, index=0): + try: + return self.buffer[self.pointer + index] + except IndexError: + self.update(index + 1) + return self.buffer[self.pointer + index] + + def prefix(self, length=1): + if self.pointer + length >= len(self.buffer): + self.update(length) + return self.buffer[self.pointer:self.pointer + length] + + def update_pointer(self, length): + while length: + ch = self.buffer[self.pointer] + self.pointer += 1 + self.full_pointer += 1 + self.index += 1 + if ch == '\n': + self.line += 1 + self.column = 0 + else: + self.column += 1 + length -= 1 + + def forward(self, length=1): + if self.pointer + length + 1 >= len(self.buffer): + self.update(length + 1) + self.update_pointer(length) + + def get_mark(self): + return Mark(self.name, self.line, self.column, self.full_buffer, self.full_pointer) + + def check_printable(self, data): + match = NON_PRINTABLE.search(data) + if match: + self.update_pointer(match.start()) + raise ReaderError( + 'while reading from stream', None, + 'found special characters which are not allowed', + Mark(self.name, self.line, self.column, self.full_buffer, self.full_pointer) + ) + + def update(self, length): + if self.raw_buffer is None: + return + self.buffer = self.buffer[self.pointer:] + self.pointer = 0 + while len(self.buffer) < length: + if not self.eof: + self.update_raw() + try: + data, converted = self.raw_decode(self.raw_buffer, 'strict', self.eof) + except UnicodeDecodeError as exc: + character = self.raw_buffer[exc.start] + position = self.stream_pointer - len(self.raw_buffer) + exc.start + data, converted = self.raw_decode(self.raw_buffer[:exc.start], 'strict', self.eof) + self.buffer += data + self.full_buffer += data + '<' + str(ord(character)) + '>' + self.raw_buffer = self.raw_buffer[converted:] + self.update_pointer(exc.start - 1) + raise ReaderError( + 'while reading from stream', None, + 'found character #x%04x that cannot be decoded by UTF-8 codec' % ord(character), + Mark(self.name, self.line, self.column, self.full_buffer, position) + ) + self.buffer += data + self.full_buffer += data + self.raw_buffer = self.raw_buffer[converted:] + self.check_printable(data) + if self.eof: + self.buffer += '\0' + self.raw_buffer = None + break + + def update_raw(self, size=4096): + data = self.stream.read(size) + if self.raw_buffer is None: + self.raw_buffer = data + else: + self.raw_buffer += data + self.stream_pointer += len(data) + if not data: + self.eof = True diff --git a/powerline/lint/markedjson/resolver.py b/powerline/lint/markedjson/resolver.py new file mode 100644 index 00000000..fa8ceaa4 --- /dev/null +++ b/powerline/lint/markedjson/resolver.py @@ -0,0 +1,131 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re + +from powerline.lint.markedjson.error import MarkedError +from powerline.lint.markedjson import nodes + + +class ResolverError(MarkedError): + pass + + +class BaseResolver: + DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str' + DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq' + DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map' + + yaml_implicit_resolvers = {} + yaml_path_resolvers = {} + + def __init__(self): + self.resolver_exact_paths = [] + self.resolver_prefix_paths = [] + + @classmethod + def add_implicit_resolver(cls, tag, regexp, first): + if 'yaml_implicit_resolvers' not in cls.__dict__: + cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy() + if first is None: + first = [None] + for ch in first: + cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp)) + + def descend_resolver(self, current_node, current_index): + if not self.yaml_path_resolvers: + return + exact_paths = {} + prefix_paths = [] + if current_node: + depth = len(self.resolver_prefix_paths) + for path, kind in self.resolver_prefix_paths[-1]: + if self.check_resolver_prefix(depth, path, kind, current_node, current_index): + if len(path) > depth: + prefix_paths.append((path, kind)) + else: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + for path, kind in self.yaml_path_resolvers: + if not path: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + prefix_paths.append((path, kind)) + self.resolver_exact_paths.append(exact_paths) + self.resolver_prefix_paths.append(prefix_paths) + + def ascend_resolver(self): + if not self.yaml_path_resolvers: + return + self.resolver_exact_paths.pop() + self.resolver_prefix_paths.pop() + + def check_resolver_prefix(self, depth, path, kind, current_node, current_index): + node_check, index_check = path[depth - 1] + if isinstance(node_check, str): + if current_node.tag != node_check: + return + elif node_check is not None: + if not isinstance(current_node, node_check): + return + if index_check is True and current_index is not None: + return + if ((index_check is False or index_check is None) + and current_index is None): + return + if isinstance(index_check, str): + if not (isinstance(current_index, nodes.ScalarNode) and index_check == current_index.value): + return + elif isinstance(index_check, int) and not isinstance(index_check, bool): + if index_check != current_index: + return + return True + + def resolve(self, kind, value, implicit, mark=None): + if kind is nodes.ScalarNode and implicit[0]: + if value == '': + resolvers = self.yaml_implicit_resolvers.get('', []) + else: + resolvers = self.yaml_implicit_resolvers.get(value[0], []) + resolvers += self.yaml_implicit_resolvers.get(None, []) + for tag, regexp in resolvers: + if regexp.match(value): + return tag + else: + self.echoerr( + 'While resolving plain scalar', None, + 'expected floating-point value, integer, null or boolean, but got %r' % value, + mark + ) + return self.DEFAULT_SCALAR_TAG + if kind is nodes.ScalarNode: + return self.DEFAULT_SCALAR_TAG + elif kind is nodes.SequenceNode: + return self.DEFAULT_SEQUENCE_TAG + elif kind is nodes.MappingNode: + return self.DEFAULT_MAPPING_TAG + + +class Resolver(BaseResolver): + pass + + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:bool', + re.compile(r'''^(?:true|false)$''', re.X), + list('yYnNtTfFoO')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:float', + re.compile(r'^-?(?:0|[1-9]\d*)(?=[.eE])(?:\.\d+)?(?:[eE][-+]?\d+)?$', re.X), + list('-0123456789')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:int', + re.compile(r'^(?:0|-?[1-9]\d*)$', re.X), + list('-0123456789')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:null', + re.compile(r'^null$', re.X), + ['n']) diff --git a/powerline/lint/markedjson/scanner.py b/powerline/lint/markedjson/scanner.py new file mode 100644 index 00000000..b4776f8a --- /dev/null +++ b/powerline/lint/markedjson/scanner.py @@ -0,0 +1,476 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lint.markedjson.error import MarkedError +from powerline.lint.markedjson import tokens +from powerline.lib.unicode import unicode + + +# Scanner produces tokens of the following types: +# STREAM-START +# STREAM-END +# DOCUMENT-START +# DOCUMENT-END +# FLOW-SEQUENCE-START +# FLOW-MAPPING-START +# FLOW-SEQUENCE-END +# FLOW-MAPPING-END +# FLOW-ENTRY +# KEY +# VALUE +# SCALAR(value, plain, style) +# +# Read comments in the Scanner code for more details. + + +class ScannerError(MarkedError): + pass + + +class SimpleKey: + # See below simple keys treatment. + def __init__(self, token_number, index, line, column, mark): + self.token_number = token_number + self.index = index + self.line = line + self.column = column + self.mark = mark + + +class Scanner: + def __init__(self): + """Initialize the scanner.""" + # It is assumed that Scanner and Reader will have a common descendant. + # Reader do the dirty work of checking for BOM and converting the + # input data to Unicode. It also adds NUL to the end. + # + # Reader supports the following methods + # self.peek(i=0) # peek the next i-th character + # self.prefix(l=1) # peek the next l characters + # self.forward(l=1) # read the next l characters and move the pointer. + + # Had we reached the end of the stream? + self.done = False + + # The number of unclosed '{' and '['. `flow_level == 0` means block + # context. + self.flow_level = 0 + + # List of processed tokens that are not yet emitted. + self.tokens = [] + + # Add the STREAM-START token. + self.fetch_stream_start() + + # Number of tokens that were emitted through the `get_token` method. + self.tokens_taken = 0 + + # Variables related to simple keys treatment. + + # A simple key is a key that is not denoted by the '?' indicator. + # We emit the KEY token before all keys, so when we find a potential + # simple key, we try to locate the corresponding ':' indicator. + # Simple keys should be limited to a single line. + + # Can a simple key start at the current position? A simple key may + # start: + # - after '{', '[', ',' (in the flow context), + self.allow_simple_key = False + + # Keep track of possible simple keys. This is a dictionary. The key + # is `flow_level`; there can be no more that one possible simple key + # for each level. The value is a SimpleKey record: + # (token_number, index, line, column, mark) + # A simple key may start with SCALAR(flow), '[', or '{' tokens. + self.possible_simple_keys = {} + + # Public methods. + + def check_token(self, *choices): + # Check if the next token is one of the given types. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + if not choices: + return True + for choice in choices: + if isinstance(self.tokens[0], choice): + return True + return False + + def peek_token(self): + # Return the next token, but do not delete if from the queue. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + return self.tokens[0] + + def get_token(self): + # Return the next token. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + self.tokens_taken += 1 + return self.tokens.pop(0) + + # Private methods. + + def need_more_tokens(self): + if self.done: + return False + if not self.tokens: + return True + # The current token may be a potential simple key, so we + # need to look further. + self.stale_possible_simple_keys() + if self.next_possible_simple_key() == self.tokens_taken: + return True + + def fetch_more_tokens(self): + + # Eat whitespaces and comments until we reach the next token. + self.scan_to_next_token() + + # Remove obsolete possible simple keys. + self.stale_possible_simple_keys() + + # Peek the next character. + ch = self.peek() + + # Is it the end of stream? + if ch == '\0': + return self.fetch_stream_end() + + # Note: the order of the following checks is NOT significant. + + # Is it the flow sequence start indicator? + if ch == '[': + return self.fetch_flow_sequence_start() + + # Is it the flow mapping start indicator? + if ch == '{': + return self.fetch_flow_mapping_start() + + # Is it the flow sequence end indicator? + if ch == ']': + return self.fetch_flow_sequence_end() + + # Is it the flow mapping end indicator? + if ch == '}': + return self.fetch_flow_mapping_end() + + # Is it the flow entry indicator? + if ch == ',': + return self.fetch_flow_entry() + + # Is it the value indicator? + if ch == ':' and self.flow_level: + return self.fetch_value() + + # Is it a double quoted scalar? + if ch == '\"': + return self.fetch_double() + + # It must be a plain scalar then. + if self.check_plain(): + return self.fetch_plain() + + # No? It's an error. Let's produce a nice error message. + raise ScannerError( + "while scanning for the next token", None, + "found character %r that cannot start any token" % ch, + self.get_mark() + ) + + # Simple keys treatment. + + def next_possible_simple_key(self): + # Return the number of the nearest possible simple key. Actually we + # don't need to loop through the whole dictionary. We may replace it + # with the following code: + # if not self.possible_simple_keys: + # return None + # return self.possible_simple_keys[ + # min(self.possible_simple_keys.keys())].token_number + min_token_number = None + for level in self.possible_simple_keys: + key = self.possible_simple_keys[level] + if min_token_number is None or key.token_number < min_token_number: + min_token_number = key.token_number + return min_token_number + + def stale_possible_simple_keys(self): + # Remove entries that are no longer possible simple keys. According to + # the YAML specification, simple keys + # - should be limited to a single line, + # Disabling this procedure will allow simple keys of any length and + # height (may cause problems if indentation is broken though). + for level in list(self.possible_simple_keys): + key = self.possible_simple_keys[level] + if key.line != self.line: + del self.possible_simple_keys[level] + + def save_possible_simple_key(self): + # The next token may start a simple key. We check if it's possible + # and save its position. This function is called for + # SCALAR(flow), '[', and '{'. + + # The next token might be a simple key. Let's save it's number and + # position. + if self.allow_simple_key: + self.remove_possible_simple_key() + token_number = self.tokens_taken + len(self.tokens) + key = SimpleKey(token_number, self.index, self.line, self.column, self.get_mark()) + self.possible_simple_keys[self.flow_level] = key + + def remove_possible_simple_key(self): + # Remove the saved possible key position at the current flow level. + if self.flow_level in self.possible_simple_keys: + del self.possible_simple_keys[self.flow_level] + + # Fetchers. + + def fetch_stream_start(self): + # We always add STREAM-START as the first token and STREAM-END as the + # last token. + + # Read the token. + mark = self.get_mark() + + # Add STREAM-START. + self.tokens.append(tokens.StreamStartToken(mark, mark, encoding=self.encoding)) + + def fetch_stream_end(self): + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + self.possible_simple_keys = {} + + # Read the token. + mark = self.get_mark() + + # Add STREAM-END. + self.tokens.append(tokens.StreamEndToken(mark, mark)) + + # The steam is finished. + self.done = True + + def fetch_flow_sequence_start(self): + self.fetch_flow_collection_start(tokens.FlowSequenceStartToken) + + def fetch_flow_mapping_start(self): + self.fetch_flow_collection_start(tokens.FlowMappingStartToken) + + def fetch_flow_collection_start(self, TokenClass): + # '[' and '{' may start a simple key. + self.save_possible_simple_key() + + # Increase the flow level. + self.flow_level += 1 + + # Simple keys are allowed after '[' and '{'. + self.allow_simple_key = True + + # Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_end(self): + self.fetch_flow_collection_end(tokens.FlowSequenceEndToken) + + def fetch_flow_mapping_end(self): + self.fetch_flow_collection_end(tokens.FlowMappingEndToken) + + def fetch_flow_collection_end(self, TokenClass): + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Decrease the flow level. + self.flow_level -= 1 + + # No simple keys after ']' or '}'. + self.allow_simple_key = False + + # Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_value(self): + # Do we determine a simple key? + if self.flow_level in self.possible_simple_keys: + + # Add KEY. + key = self.possible_simple_keys[self.flow_level] + del self.possible_simple_keys[self.flow_level] + self.tokens.insert(key.token_number - self.tokens_taken, tokens.KeyToken(key.mark, key.mark)) + + # There cannot be two simple keys one after another. + self.allow_simple_key = False + + # Add VALUE. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(tokens.ValueToken(start_mark, end_mark)) + + def fetch_flow_entry(self): + # Simple keys are allowed after ','. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add FLOW-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(tokens.FlowEntryToken(start_mark, end_mark)) + + def fetch_double(self): + # A flow scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after flow scalars. + self.allow_simple_key = False + + # Scan and add SCALAR. + self.tokens.append(self.scan_flow_scalar()) + + def fetch_plain(self): + + self.save_possible_simple_key() + + # No simple keys after plain scalars. + self.allow_simple_key = False + + # Scan and add SCALAR. May change `allow_simple_key`. + self.tokens.append(self.scan_plain()) + + # Checkers. + + def check_plain(self): + return self.peek() in '0123456789-ntf' + + # Scanners. + + def scan_to_next_token(self): + while self.peek() in ' \t\n': + self.forward() + + def scan_flow_scalar(self): + # See the specification for details. + # Note that we loose indentation rules for quoted scalars. Quoted + # scalars don't need to adhere indentation because " and ' clearly + # mark the beginning and the end of them. Therefore we are less + # restrictive then the specification requires. We only need to check + # that document separators are not included in scalars. + chunks = [] + start_mark = self.get_mark() + quote = self.peek() + self.forward() + chunks.extend(self.scan_flow_scalar_non_spaces(start_mark)) + while self.peek() != quote: + chunks.extend(self.scan_flow_scalar_spaces(start_mark)) + chunks.extend(self.scan_flow_scalar_non_spaces(start_mark)) + self.forward() + end_mark = self.get_mark() + return tokens.ScalarToken(unicode().join(chunks), False, start_mark, end_mark, '"') + + ESCAPE_REPLACEMENTS = { + 'b': '\x08', + 't': '\x09', + 'n': '\x0A', + 'f': '\x0C', + 'r': '\x0D', + '\"': '\"', + '\\': '\\', + } + + ESCAPE_CODES = { + 'u': 4, + } + + def scan_flow_scalar_non_spaces(self, start_mark): + # See the specification for details. + chunks = [] + while True: + length = 0 + while self.peek(length) not in '\"\\\0 \t\n': + length += 1 + if length: + chunks.append(self.prefix(length)) + self.forward(length) + ch = self.peek() + if ch == '\\': + self.forward() + ch = self.peek() + if ch in self.ESCAPE_REPLACEMENTS: + chunks.append(self.ESCAPE_REPLACEMENTS[ch]) + self.forward() + elif ch in self.ESCAPE_CODES: + length = self.ESCAPE_CODES[ch] + self.forward() + for k in range(length): + if self.peek(k) not in '0123456789ABCDEFabcdef': + raise ScannerError( + "while scanning a double-quoted scalar", start_mark, + "expected escape sequence of %d hexdecimal numbers, but found %r" % ( + length, self.peek(k)), + self.get_mark() + ) + code = int(self.prefix(length), 16) + chunks.append(chr(code)) + self.forward(length) + else: + raise ScannerError( + "while scanning a double-quoted scalar", start_mark, + ("found unknown escape character %r" % ch), self.get_mark() + ) + else: + return chunks + + def scan_flow_scalar_spaces(self, start_mark): + # See the specification for details. + chunks = [] + length = 0 + while self.peek(length) in ' \t': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch == '\0': + raise ScannerError( + "while scanning a quoted scalar", start_mark, + "found unexpected end of stream", self.get_mark() + ) + elif ch == '\n': + raise ScannerError( + "while scanning a quoted scalar", start_mark, + "found unexpected line end", self.get_mark() + ) + else: + chunks.append(whitespaces) + return chunks + + def scan_plain(self): + chunks = [] + start_mark = self.get_mark() + spaces = [] + while True: + length = 0 + while True: + if self.peek(length) not in 'eE.0123456789nul-tr+fas': + break + length += 1 + if length == 0: + break + self.allow_simple_key = False + chunks.extend(spaces) + chunks.append(self.prefix(length)) + self.forward(length) + end_mark = self.get_mark() + return tokens.ScalarToken(''.join(chunks), True, start_mark, end_mark) diff --git a/powerline/lint/markedjson/tokens.py b/powerline/lint/markedjson/tokens.py new file mode 100644 index 00000000..6fa8bf18 --- /dev/null +++ b/powerline/lint/markedjson/tokens.py @@ -0,0 +1,72 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + + +class Token(object): + def __init__(self, start_mark, end_mark): + self.start_mark = start_mark + self.end_mark = end_mark + + def __repr__(self): + attributes = [ + key for key in self.__dict__ + if not key.endswith('_mark') + ] + attributes.sort() + arguments = ', '.join([ + '%s=%r' % (key, getattr(self, key)) + for key in attributes + ]) + return '%s(%s)' % (self.__class__.__name__, arguments) + + +class StreamStartToken(Token): + id = '' + + def __init__(self, start_mark=None, end_mark=None, encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + + +class StreamEndToken(Token): + id = '' + + +class FlowSequenceStartToken(Token): + id = '[' + + +class FlowMappingStartToken(Token): + id = '{' + + +class FlowSequenceEndToken(Token): + id = ']' + + +class FlowMappingEndToken(Token): + id = '}' + + +class KeyToken(Token): + id = '?' + + +class ValueToken(Token): + id = ':' + + +class FlowEntryToken(Token): + id = ',' + + +class ScalarToken(Token): + id = '' + + def __init__(self, value, plain, start_mark, end_mark, style=None): + self.value = value + self.plain = plain + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style diff --git a/powerline/listers/__init__.py b/powerline/listers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline/listers/vim.py b/powerline/listers/vim.py new file mode 100644 index 00000000..c1995957 --- /dev/null +++ b/powerline/listers/vim.py @@ -0,0 +1,110 @@ +# vim:fileencoding=utf-8:noet +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) + +try: + import vim +except ImportError: + vim = {} + + +def tabpage_updated_segment_info(segment_info, tabpage): + segment_info = segment_info.copy() + window = tabpage.window + buffer = window.buffer + segment_info.update( + tabpage=tabpage, + tabnr=tabpage.number, + window=window, + winnr=window.number, + window_id=int(window.vars.get('powerline_window_id', -1)), + buffer=buffer, + bufnr=buffer.number, + ) + return segment_info + + +@requires_segment_info +def tablister(pl, segment_info, **kwargs): + '''List all tab pages in segment_info format + + Specifically generates a list of segment info dictionaries with ``window``, + ``winnr``, ``window_id``, ``buffer`` and ``bufnr`` keys set to tab-local + ones and additional ``tabpage`` and ``tabnr`` keys. + + Adds either ``tab:`` or ``tab_nc:`` prefix to all segment highlight groups. + + Works best with vim-7.4 or later: earlier versions miss tabpage object and + thus window objects are not available as well. + ''' + cur_tabpage = current_tabpage() + cur_tabnr = cur_tabpage.number + + def add_multiplier(tabpage, dct): + dct['priority_multiplier'] = 1 + (0.001 * abs(tabpage.number - cur_tabnr)) + return dct + + return ( + (lambda tabpage, prefix: ( + tabpage_updated_segment_info(segment_info, tabpage), + add_multiplier(tabpage, {'highlight_group_prefix': prefix}) + ))(tabpage, 'tab' if tabpage == cur_tabpage else 'tab_nc') + for tabpage in list_tabpages() + ) + + +def buffer_updated_segment_info(segment_info, buffer): + segment_info = segment_info.copy() + segment_info.update( + window=None, + winnr=None, + window_id=None, + buffer=buffer, + bufnr=buffer.number, + ) + return segment_info + + +@requires_segment_info +def bufferlister(pl, segment_info, show_unlisted=False, **kwargs): + '''List all buffers in segment_info format + + Specifically generates a list of segment info dictionaries with ``buffer`` + 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. + + :param bool show_unlisted: + True if unlisted buffers should be shown as well. Current buffer is + always shown. + ''' + cur_buffer = vim.current.buffer + cur_bufnr = cur_buffer.number + + def add_multiplier(buffer, dct): + dct['priority_multiplier'] = 1 + (0.001 * abs(buffer.number - cur_bufnr)) + return dct + + return ( + ( + buf_segment_info, + add_multiplier(buf_segment_info['buffer'], {'highlight_group_prefix': prefix}) + ) + 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')) + ) + ) diff --git a/powerline/matchers/__init__.py b/powerline/matchers/__init__.py new file mode 100644 index 00000000..b2b9f102 --- /dev/null +++ b/powerline/matchers/__init__.py @@ -0,0 +1,6 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) +from pkgutil import extend_path + + +__path__ = extend_path(__path__, __name__) diff --git a/powerline/matchers/vim/__init__.py b/powerline/matchers/vim/__init__.py new file mode 100644 index 00000000..d7e9b48d --- /dev/null +++ b/powerline/matchers/vim/__init__.py @@ -0,0 +1,19 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from powerline.bindings.vim import vim_getbufoption + + +def help(matcher_info): + return str(vim_getbufoption(matcher_info, 'buftype')) == 'help' + + +def cmdwin(matcher_info): + name = matcher_info['buffer'].name + return name and os.path.basename(name) == '[Command Line]' + + +def quickfix(matcher_info): + return str(vim_getbufoption(matcher_info, 'buftype')) == 'quickfix' diff --git a/powerline/matchers/vim/plugin/__init__.py b/powerline/matchers/vim/plugin/__init__.py new file mode 100644 index 00000000..b2b9f102 --- /dev/null +++ b/powerline/matchers/vim/plugin/__init__.py @@ -0,0 +1,6 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) +from pkgutil import extend_path + + +__path__ = extend_path(__path__, __name__) diff --git a/powerline/matchers/vim/plugin/ctrlp.py b/powerline/matchers/vim/plugin/ctrlp.py new file mode 100644 index 00000000..f2b05980 --- /dev/null +++ b/powerline/matchers/vim/plugin/ctrlp.py @@ -0,0 +1,30 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +try: + import vim +except ImportError: + vim = object() +else: + vim.command(''' + function! Powerline_plugin_ctrlp_main(...) + let b:powerline_ctrlp_type = 'main' + let b:powerline_ctrlp_args = a:000 + endfunction''') + + vim.command(''' + function! Powerline_plugin_ctrlp_prog(...) + let b:powerline_ctrlp_type = 'prog' + let b:powerline_ctrlp_args = a:000 + endfunction''') + + vim.command(''' + let g:ctrlp_status_func = {'main': 'Powerline_plugin_ctrlp_main', 'prog': 'Powerline_plugin_ctrlp_prog'} + ''') + + +def ctrlp(matcher_info): + name = matcher_info['buffer'].name + return name and os.path.basename(name) == 'ControlP' diff --git a/powerline/matchers/vim/plugin/gundo.py b/powerline/matchers/vim/plugin/gundo.py new file mode 100644 index 00000000..356ffd65 --- /dev/null +++ b/powerline/matchers/vim/plugin/gundo.py @@ -0,0 +1,14 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + + +def gundo(matcher_info): + name = matcher_info['buffer'].name + return name and os.path.basename(name) == '__Gundo__' + + +def gundo_preview(matcher_info): + name = matcher_info['buffer'].name + return name and os.path.basename(name) == '__Gundo_Preview__' diff --git a/powerline/matchers/vim/plugin/nerdtree.py b/powerline/matchers/vim/plugin/nerdtree.py new file mode 100644 index 00000000..aeb2c24c --- /dev/null +++ b/powerline/matchers/vim/plugin/nerdtree.py @@ -0,0 +1,10 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re + + +def nerdtree(matcher_info): + name = matcher_info['buffer'].name + return name and re.match(r'NERD_tree_\d+', os.path.basename(name)) diff --git a/powerline/renderer.py b/powerline/renderer.py new file mode 100644 index 00000000..dccc8f15 --- /dev/null +++ b/powerline/renderer.py @@ -0,0 +1,437 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from unicodedata import east_asian_width, combining +from itertools import chain + +from powerline.theme import Theme +from powerline.lib.unicode import unichr + + +NBSP = ' ' + + +def construct_returned_value(rendered_highlighted, segments, width, output_raw, output_width): + if not (output_raw or output_width): + return rendered_highlighted + else: + return ( + (rendered_highlighted,) + + ((''.join((segment['_rendered_raw'] for segment in segments)),) if output_raw else ()) + + ((width,) if output_width else ()) + ) + + +class Renderer(object): + '''Object that is responsible for generating the highlighted string. + + :param dict theme_config: + Main theme configuration. + :param local_themes: + Local themes. Is to be used by subclasses from ``.get_theme()`` method, + base class only records this parameter to a ``.local_themes`` attribute. + :param dict theme_kwargs: + Keyword arguments for ``Theme`` class constructor. + :param PowerlineLogger pl: + Object used for logging. + :param int ambiwidth: + Width of the characters with east asian width unicode attribute equal to + ``A`` (Ambigious). + :param dict options: + Various options. Are normally not used by base renderer, but all options + are recorded as attributes. + ''' + + segment_info = { + 'environ': os.environ, + 'getcwd': getattr(os, 'getcwdu', os.getcwd), + 'home': os.environ.get('HOME'), + } + '''Basic segment info. Is merged with local segment information by + ``.get_segment_info()`` method. Keys: + + ``environ`` + Object containing environment variables. Must define at least the + following methods: ``.__getitem__(var)`` that raises ``KeyError`` in + case requested environment variable is not present, ``.get(var, + default=None)`` that works like ``dict.get`` and be able to be passed to + ``Popen``. + + ``getcwd`` + Function that returns current working directory. Will be called without + any arguments, should return ``unicode`` or (in python-2) regular + string. + + ``home`` + String containing path to home directory. Should be ``unicode`` or (in + python-2) regular string or ``None``. + ''' + + character_translations = {} + '''Character translations for use in escape() function. + + See documentation of ``unicode.translate`` for details. + ''' + + np_character_translations = dict(((i, '^' + unichr(i + 0x40)) for i in range(0x20))) + '''Non-printable character translations + + These are used to transform characters in range 0x00—0x1F into ``^@``, + ``^A`` and so on. Unilke with ``.escape()`` method (and + ``character_translations``) result is passed to ``.strwidth()`` method. + + Note: transforms tab into ``^I``. + ''' + + def __init__(self, + theme_config, + local_themes, + theme_kwargs, + pl, + ambiwidth=1, + **options): + self.__dict__.update(options) + self.theme_config = theme_config + theme_kwargs['pl'] = pl + self.pl = pl + if theme_config.get('use_non_breaking_spaces', True): + self.character_translations = self.character_translations.copy() + self.character_translations[ord(' ')] = NBSP + self.theme = Theme(theme_config=theme_config, **theme_kwargs) + self.local_themes = local_themes + self.theme_kwargs = theme_kwargs + self.width_data = { + 'N': 1, # Neutral + 'Na': 1, # Narrow + 'A': ambiwidth, # Ambigious + 'H': 1, # Half-width + 'W': 2, # Wide + 'F': 2, # Fullwidth + } + + def strwidth(self, string): + '''Function that returns string width. + + Is used to calculate the place given string occupies when handling + ``width`` argument to ``.render()`` method. Must take east asian width + into account. + + :param unicode string: + String whose width will be calculated. + + :return: unsigned integer. + ''' + return sum((0 if combining(symbol) else self.width_data[east_asian_width(symbol)] for symbol in string)) + + def get_theme(self, matcher_info): + '''Get Theme object. + + Is to be overridden by subclasses to support local themes, this variant + only returns ``.theme`` attribute. + + :param matcher_info: + Parameter ``matcher_info`` that ``.render()`` method received. + Unused. + ''' + return self.theme + + def shutdown(self): + '''Prepare for interpreter shutdown. The only job it is supposed to do + is calling ``.shutdown()`` method for all theme objects. Should be + overridden by subclasses in case they support local themes. + ''' + self.theme.shutdown() + + def get_segment_info(self, segment_info, mode): + '''Get segment information. + + Must return a dictionary containing at least ``home``, ``environ`` and + ``getcwd`` keys (see documentation for ``segment_info`` attribute). This + implementation merges ``segment_info`` dictionary passed to + ``.render()`` method with ``.segment_info`` attribute, preferring keys + from the former. It also replaces ``getcwd`` key with function returning + ``segment_info['environ']['PWD']`` in case ``PWD`` variable is + available. + + :param dict segment_info: + Segment information that was passed to ``.render()`` method. + + :return: dict with segment information. + ''' + r = self.segment_info.copy() + r['mode'] = mode + if segment_info: + r.update(segment_info) + if 'PWD' in r['environ']: + r['getcwd'] = lambda: r['environ']['PWD'] + return r + + def render_above_lines(self, **kwargs): + '''Render all segments in the {theme}/segments/above list + + Rendering happens in the reversed order. Parameters are the same as in + .render() method. + + :yield: rendered line. + ''' + + theme = self.get_theme(kwargs.get('matcher_info', None)) + for line in range(theme.get_line_number() - 1, 0, -1): + yield self.render(side=None, line=line, **kwargs) + + def render(self, mode=None, width=None, side=None, line=0, output_raw=False, output_width=False, segment_info=None, matcher_info=None): + '''Render all segments. + + When a width is provided, low-priority segments are dropped one at + a time until the line is shorter than the width, or only segments + with a negative priority are left. If one or more segments with + ``"width": "auto"`` are provided they will fill the remaining space + until the desired width is reached. + + :param str mode: + Mode string. Affects contents (colors and the set of segments) of + rendered string. + :param int width: + Maximum width text can occupy. May be exceeded if there are too much + non-removable segments. + :param str side: + One of ``left``, ``right``. Determines which side will be rendered. + If not present all sides are rendered. + :param int line: + Line number for which segments should be obtained. Is counted from + zero (botmost line). + :param bool output_raw: + Changes the output: if this parameter is ``True`` then in place of + one string this method outputs a pair ``(colored_string, + colorless_string)``. + :param bool output_width: + Changes the output: if this parameter is ``True`` then in place of + one string this method outputs a pair ``(colored_string, + string_width)``. Returns a three-tuple if ``output_raw`` is also + ``True``: ``(colored_string, colorless_string, string_width)``. + :param dict segment_info: + Segment information. See also ``.get_segment_info()`` method. + :param matcher_info: + Matcher information. Is processed in ``.get_theme()`` method. + ''' + theme = self.get_theme(matcher_info) + return self.do_render( + mode=mode, + width=width, + side=side, + line=line, + output_raw=output_raw, + output_width=output_width, + segment_info=segment_info, + theme=theme, + ) + + def compute_divider_widths(self, theme): + return { + 'left': { + 'hard': self.strwidth(theme.get_divider('left', 'hard')), + 'soft': self.strwidth(theme.get_divider('left', 'soft')), + }, + 'right': { + 'hard': self.strwidth(theme.get_divider('right', 'hard')), + 'soft': self.strwidth(theme.get_divider('right', 'soft')), + }, + } + + def do_render(self, mode, width, side, line, output_raw, output_width, segment_info, theme): + '''Like Renderer.render(), but accept theme in place of matcher_info + ''' + segments = list(theme.get_segments(side, line, self.get_segment_info(segment_info, mode), mode)) + + current_width = 0 + + if not width: + # No width specified, so we don't need to crop or pad anything + if output_width: + current_width = self._render_length(theme, segments, self.compute_divider_widths(theme)) + return construct_returned_value(''.join([ + segment['_rendered_hl'] + for segment in self._render_segments(theme, segments) + ]) + self.hlstyle(), segments, current_width, output_raw, output_width) + + divider_widths = self.compute_divider_widths(theme) + + # Create an ordered list of segments that can be dropped + segments_priority = sorted((segment for segment in segments if segment['priority'] is not None), key=lambda segment: segment['priority'], reverse=True) + no_priority_segments = filter(lambda segment: segment['priority'] is None, segments) + current_width = self._render_length(theme, segments, divider_widths) + if current_width > width: + for segment in chain(segments_priority, no_priority_segments): + if segment['truncate'] is not None: + segment['contents'] = segment['truncate'](self.pl, current_width - width, segment) + + segments_priority = iter(segments_priority) + if current_width > width and len(segments) > 100: + # When there are too many segments use faster, but less correct + # algorythm for width computation + diff = current_width - width + for segment in segments_priority: + segments.remove(segment) + diff -= segment['_len'] + if diff <= 0: + break + current_width = self._render_length(theme, segments, divider_widths) + if current_width > width: + # When there are not too much use more precise, but much slower + # width computation. It also finishes computations in case + # previous variant did not free enough space. + for segment in segments_priority: + segments.remove(segment) + current_width = self._render_length(theme, segments, divider_widths) + if current_width <= width: + break + del segments_priority + + # Distribute the remaining space on spacer segments + segments_spacers = [segment for segment in segments if segment['expand'] is not None] + if segments_spacers: + distribute_len, distribute_len_remainder = divmod(width - current_width, len(segments_spacers)) + for segment in segments_spacers: + segment['contents'] = ( + segment['expand']( + self.pl, + distribute_len + (1 if distribute_len_remainder > 0 else 0), + segment)) + distribute_len_remainder -= 1 + # `_len` key is not needed anymore, but current_width should have an + # actual value for various bindings. + current_width = width + elif output_width: + current_width = self._render_length(theme, segments, divider_widths) + + rendered_highlighted = ''.join([segment['_rendered_hl'] for segment in self._render_segments(theme, segments)]) + self.hlstyle() + + return construct_returned_value(rendered_highlighted, segments, current_width, output_raw, output_width) + + def _render_length(self, theme, segments, divider_widths): + '''Update segments lengths and return them + ''' + segments_len = len(segments) + ret = 0 + divider_spaces = theme.get_spaces() + for index, segment in enumerate(segments): + side = segment['side'] + if segment['_contents_len'] is None: + segment_len = segment['_contents_len'] = self.strwidth(segment['contents']) + else: + segment_len = segment['_contents_len'] + + prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT + next_segment = segments[index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT + compare_segment = next_segment if side == 'left' else prev_segment + divider_type = 'soft' if compare_segment['highlight']['bg'] == segment['highlight']['bg'] else 'hard' + + outer_padding = int(bool( + (index == 0 and side == 'left') or + (index == segments_len - 1 and side == 'right') + )) + + draw_divider = segment['draw_' + divider_type + '_divider'] + segment_len += outer_padding + if draw_divider: + segment_len += divider_widths[side][divider_type] + divider_spaces + + segment['_len'] = segment_len + ret += segment_len + return ret + + def _render_segments(self, theme, segments, render_highlighted=True): + '''Internal segment rendering method. + + This method loops through the segment array and compares the + foreground/background colors and divider properties and returns the + rendered statusline as a string. + + The method always renders the raw segment contents (i.e. without + highlighting strings added), and only renders the highlighted + statusline if render_highlighted is True. + ''' + segments_len = len(segments) + divider_spaces = theme.get_spaces() + + for index, segment in enumerate(segments): + side = segment['side'] + prev_segment = segments[index - 1] if index > 0 else theme.EMPTY_SEGMENT + next_segment = segments[index + 1] if index < segments_len - 1 else theme.EMPTY_SEGMENT + compare_segment = next_segment if side == 'left' else prev_segment + outer_padding = int(bool( + (index == 0 and side == 'left') or + (index == segments_len - 1 and side == 'right') + )) * ' ' + divider_type = 'soft' if compare_segment['highlight']['bg'] == segment['highlight']['bg'] else 'hard' + + divider_highlighted = '' + contents_raw = segment['contents'] + contents_highlighted = '' + draw_divider = segment['draw_' + divider_type + '_divider'] + + contents_raw = contents_raw.translate(self.np_character_translations) + + # XXX Make sure self.hl() calls are called in the same order + # segments are displayed. This is needed for Vim renderer to work. + if draw_divider: + divider_raw = self.escape(theme.get_divider(side, divider_type)) + if side == 'left': + contents_raw = outer_padding + contents_raw + (divider_spaces * ' ') + else: + contents_raw = (divider_spaces * ' ') + contents_raw + outer_padding + + if divider_type == 'soft': + divider_highlight_group_key = 'highlight' if segment['divider_highlight_group'] is None else 'divider_highlight' + divider_fg = segment[divider_highlight_group_key]['fg'] + divider_bg = segment[divider_highlight_group_key]['bg'] + else: + divider_fg = segment['highlight']['bg'] + divider_bg = compare_segment['highlight']['bg'] + + if side == 'left': + if render_highlighted: + contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) + divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False) + segment['_rendered_raw'] = contents_raw + divider_raw + segment['_rendered_hl'] = contents_highlighted + divider_highlighted + else: + if render_highlighted: + divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False) + contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) + segment['_rendered_raw'] = divider_raw + contents_raw + segment['_rendered_hl'] = divider_highlighted + contents_highlighted + else: + if side == 'left': + contents_raw = outer_padding + contents_raw + else: + contents_raw = contents_raw + outer_padding + + contents_highlighted = self.hl(self.escape(contents_raw), **segment['highlight']) + segment['_rendered_raw'] = contents_raw + segment['_rendered_hl'] = contents_highlighted + yield segment + + def escape(self, string): + '''Method that escapes segment contents. + ''' + return string.translate(self.character_translations) + + def hlstyle(fg=None, bg=None, attr=None): + '''Output highlight style string. + + Assuming highlighted string looks like ``{style}{contents}`` this method + should output ``{style}``. If it is called without arguments this method + is supposed to reset style to its default. + ''' + raise NotImplementedError + + def hl(self, contents, fg=None, bg=None, attr=None): + '''Output highlighted chunk. + + This implementation just outputs ``.hlstyle()`` joined with + ``contents``. + ''' + return self.hlstyle(fg, bg, attr) + (contents or '') diff --git a/powerline/renderers/__init__.py b/powerline/renderers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline/renderers/i3bar.py b/powerline/renderers/i3bar.py new file mode 100644 index 00000000..223458c2 --- /dev/null +++ b/powerline/renderers/i3bar.py @@ -0,0 +1,37 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import json + +from powerline.renderer import Renderer + + +class I3barRenderer(Renderer): + '''I3bar Segment Renderer. + + Currently works only for i3bgbar (i3 bar with custom patches). + ''' + + @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, attr=None): + segment = { + "full_text": contents, + "separator": False, + "separator_block_width": 0, # no seperators + } + + if fg is not None: + if fg is not False and fg[1] is not False: + segment['color'] = "#{0:06x}".format(fg[1]) + if bg is not None: + if bg is not False and bg[1] is not False: + segment['background_color'] = "#{0:06x}".format(bg[1]) + # i3bar "pseudo json" requires one line at a time + return json.dumps(segment) + ",\n" + + +renderer = I3barRenderer diff --git a/powerline/renderers/ipython/__init__.py b/powerline/renderers/ipython/__init__.py new file mode 100644 index 00000000..985e6c34 --- /dev/null +++ b/powerline/renderers/ipython/__init__.py @@ -0,0 +1,75 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell import ShellRenderer +from powerline.theme import Theme + + +class IPythonRenderer(ShellRenderer): + '''Powerline ipython segment renderer.''' + def get_segment_info(self, segment_info, mode): + r = self.segment_info.copy() + r['ipython'] = segment_info + return r + + def get_theme(self, matcher_info): + if matcher_info == 'in': + return self.theme + else: + 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'] + + def shutdown(self): + self.theme.shutdown() + for match in self.local_themes.values(): + if 'theme' in match: + match['theme'].shutdown() + + def render(self, *args, **kwargs): + # XXX super(ShellRenderer), *not* super(IPythonRenderer) + return super(ShellRenderer, self).render(*args, **kwargs) + + +class IPythonPromptRenderer(IPythonRenderer): + '''Powerline ipython prompt (in and in2) renderer''' + escape_hl_start = '\x01' + escape_hl_end = '\x02' + + +class IPythonNonPromptRenderer(IPythonRenderer): + '''Powerline ipython non-prompt (out and rewrite) renderer''' + pass + + +class RendererProxy(object): + '''Powerline IPython renderer proxy which chooses appropriate renderer + + Instantiates two renderer objects: one will be used for prompts and the + other for non-prompts. + ''' + def __init__(self, **kwargs): + old_widths = {} + self.non_prompt_renderer = IPythonNonPromptRenderer(old_widths=old_widths, **kwargs) + self.prompt_renderer = IPythonPromptRenderer(old_widths=old_widths, **kwargs) + + def render_above_lines(self, *args, **kwargs): + return self.non_prompt_renderer.render_above_lines(*args, **kwargs) + + def render(self, is_prompt, *args, **kwargs): + return (self.prompt_renderer if is_prompt else self.non_prompt_renderer).render( + *args, **kwargs) + + def shutdown(self, *args, **kwargs): + self.prompt_renderer.shutdown(*args, **kwargs) + self.non_prompt_renderer.shutdown(*args, **kwargs) + + +renderer = RendererProxy diff --git a/powerline/renderers/pango_markup.py b/powerline/renderers/pango_markup.py new file mode 100644 index 00000000..02511ab5 --- /dev/null +++ b/powerline/renderers/pango_markup.py @@ -0,0 +1,39 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from xml.sax.saxutils import escape as _escape + +from powerline.renderer import Renderer +from powerline.colorscheme import ATTR_BOLD, ATTR_ITALIC, ATTR_UNDERLINE + + +class PangoMarkupRenderer(Renderer): + '''Powerline Pango markup segment renderer.''' + + @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, attr=None): + '''Highlight a segment.''' + awesome_attr = [] + if fg is not None: + if fg is not False and fg[1] is not False: + awesome_attr += ['foreground="#{0:06x}"'.format(fg[1])] + if bg is not None: + if bg is not False and bg[1] is not False: + awesome_attr += ['background="#{0:06x}"'.format(bg[1])] + if attr is not None and attr is not False: + if attr & ATTR_BOLD: + awesome_attr += ['font_weight="bold"'] + if attr & ATTR_ITALIC: + awesome_attr += ['font_style="italic"'] + if attr & ATTR_UNDERLINE: + awesome_attr += ['underline="single"'] + return '' + contents + '' + + escape = staticmethod(_escape) + + +renderer = PangoMarkupRenderer diff --git a/powerline/renderers/shell/__init__.py b/powerline/renderers/shell/__init__.py new file mode 100644 index 00000000..b767fc26 --- /dev/null +++ b/powerline/renderers/shell/__init__.py @@ -0,0 +1,138 @@ +# 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_BOLD, ATTR_ITALIC, ATTR_UNDERLINE + + +def int_to_rgb(num): + r = (num >> 16) & 0xff + g = (num >> 8) & 0xff + b = num & 0xff + return r, g, b + + +class ShellRenderer(Renderer): + '''Powerline shell segment renderer.''' + escape_hl_start = '' + escape_hl_end = '' + term_truecolor = False + tmux_escape = False + screen_escape = False + + character_translations = Renderer.character_translations.copy() + + def __init__(self, old_widths=None, **kwargs): + super(ShellRenderer, self).__init__(**kwargs) + self.old_widths = old_widths if old_widths is not None else {} + + def render(self, segment_info, **kwargs): + local_theme = segment_info.get('local_theme') + return super(ShellRenderer, self).render( + matcher_info=local_theme, + segment_info=segment_info, + **kwargs + ) + + def do_render(self, output_width, segment_info, side, theme, width=None, **kwargs): + if isinstance(segment_info, dict): + client_id = segment_info.get('client_id') + else: + client_id = None + local_key = (client_id, side, None if theme is self.theme else id(theme)) + key = (client_id, side, None) + did_width = False + if local_key[-1] != key[-1] and side == 'left': + try: + width = self.old_widths[key] + except KeyError: + pass + else: + did_width = True + if not did_width: + if width is not None: + if theme.cursor_space_multiplier is not None: + width = int(width * theme.cursor_space_multiplier) + elif theme.cursor_columns: + width -= theme.cursor_columns + + if side == 'right': + try: + width -= self.old_widths[(client_id, 'left', local_key[-1])] + except KeyError: + pass + res = super(ShellRenderer, self).do_render( + output_width=True, + width=width, + theme=theme, + segment_info=segment_info, + side=side, + **kwargs + ) + self.old_widths[local_key] = res[-1] + ret = res if output_width else res[:-1] + if len(ret) == 1: + return ret[0] + else: + return ret + + def hlstyle(self, fg=None, bg=None, attr=None): + '''Highlight a segment. + + If an argument is None, the argument is ignored. If an argument is + False, the argument is reset to the terminal defaults. If an argument + is a valid color or attribute, it's added to the ANSI escape code. + ''' + ansi = [0] + if fg is not None: + if fg is False or fg[0] is False: + ansi += [39] + else: + if self.term_truecolor: + ansi += [38, 2] + list(int_to_rgb(fg[1])) + else: + ansi += [38, 5, fg[0]] + if bg is not None: + if bg is False or bg[0] is False: + ansi += [49] + else: + if self.term_truecolor: + ansi += [48, 2] + list(int_to_rgb(bg[1])) + else: + ansi += [48, 5, bg[0]] + if attr is not None: + if attr is False: + ansi += [22] + else: + if attr & ATTR_BOLD: + ansi += [1] + elif attr & ATTR_ITALIC: + # Note: is likely not to work or even be inverse in place of + # italic. Omit using this in colorschemes. + ansi += [3] + elif attr & ATTR_UNDERLINE: + ansi += [4] + r = '\033[{0}m'.format(';'.join(str(attr) for attr in ansi)) + if self.tmux_escape: + r = '\033Ptmux;' + r.replace('\033', '\033\033') + '\033\\' + elif self.screen_escape: + r = '\033P' + r.replace('\033', '\033\033') + '\033\\' + return self.escape_hl_start + r + self.escape_hl_end + + def get_theme(self, matcher_info): + if not matcher_info: + 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 = ShellRenderer diff --git a/powerline/renderers/shell/bash.py b/powerline/renderers/shell/bash.py new file mode 100644 index 00000000..783bd501 --- /dev/null +++ b/powerline/renderers/shell/bash.py @@ -0,0 +1,18 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell import ShellRenderer + + +class BashPromptRenderer(ShellRenderer): + '''Powerline bash prompt segment renderer.''' + escape_hl_start = '\[' + escape_hl_end = '\]' + + character_translations = ShellRenderer.character_translations.copy() + character_translations[ord('$')] = '\\$' + character_translations[ord('`')] = '\\`' + character_translations[ord('\\')] = '\\\\' + + +renderer = BashPromptRenderer diff --git a/powerline/renderers/shell/ksh.py b/powerline/renderers/shell/ksh.py new file mode 100644 index 00000000..0828e573 --- /dev/null +++ b/powerline/renderers/shell/ksh.py @@ -0,0 +1,19 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell import ShellRenderer + + +ESCAPE_CHAR = '\001' + + +class KshPromptRenderer(ShellRenderer): + '''Powerline bash prompt segment renderer.''' + escape_hl_start = '\001' + escape_hl_end = '\001' + + def render(self, *args, **kwargs): + return '\001\r' + super(KshPromptRenderer, self).render(*args, **kwargs) + + +renderer = KshPromptRenderer diff --git a/powerline/renderers/shell/tcsh.py b/powerline/renderers/shell/tcsh.py new file mode 100644 index 00000000..5d138b93 --- /dev/null +++ b/powerline/renderers/shell/tcsh.py @@ -0,0 +1,15 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell.zsh import ZshPromptRenderer + + +class TcshPromptRenderer(ZshPromptRenderer): + '''Powerline tcsh prompt segment renderer.''' + character_translations = ZshPromptRenderer.character_translations.copy() + character_translations[ord('%')] = '%%' + character_translations[ord('\\')] = '\\\\' + character_translations[ord('^')] = '\\^' + + +renderer = TcshPromptRenderer diff --git a/powerline/renderers/shell/zsh.py b/powerline/renderers/shell/zsh.py new file mode 100644 index 00000000..a2315124 --- /dev/null +++ b/powerline/renderers/shell/zsh.py @@ -0,0 +1,16 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell import ShellRenderer + + +class ZshPromptRenderer(ShellRenderer): + '''Powerline zsh prompt segment renderer.''' + escape_hl_start = '%{' + escape_hl_end = '%}' + + character_translations = ShellRenderer.character_translations.copy() + character_translations[ord('%')] = '%%' + + +renderer = ZshPromptRenderer diff --git a/powerline/renderers/tmux.py b/powerline/renderers/tmux.py new file mode 100644 index 00000000..5c02f29b --- /dev/null +++ b/powerline/renderers/tmux.py @@ -0,0 +1,60 @@ +# 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_BOLD, ATTR_ITALIC, ATTR_UNDERLINE + + +class TmuxRenderer(Renderer): + '''Powerline tmux segment renderer.''' + + character_translations = Renderer.character_translations.copy() + character_translations[ord('#')] = '##[]' + + def hlstyle(self, fg=None, bg=None, attr=None): + '''Highlight a segment.''' + # We don't need to explicitly reset attributes, so skip those calls + if not attr and not bg and not fg: + return '' + tmux_attr = [] + if fg is not None: + if fg is False or fg[0] is False: + tmux_attr += ['fg=default'] + else: + tmux_attr += ['fg=colour' + str(fg[0])] + if bg is not None: + if bg is False or bg[0] is False: + tmux_attr += ['bg=default'] + else: + tmux_attr += ['bg=colour' + str(bg[0])] + if attr is not None: + if attr is False: + tmux_attr += ['nobold', 'noitalics', 'nounderscore'] + else: + if attr & ATTR_BOLD: + tmux_attr += ['bold'] + else: + tmux_attr += ['nobold'] + if attr & ATTR_ITALIC: + tmux_attr += ['italics'] + else: + tmux_attr += ['noitalics'] + if attr & ATTR_UNDERLINE: + tmux_attr += ['underscore'] + else: + tmux_attr += ['nounderscore'] + return '#[' + ','.join(tmux_attr) + ']' + + def get_segment_info(self, segment_info, mode): + r = self.segment_info.copy() + if segment_info: + r.update(segment_info) + if 'pane_id' in r: + varname = 'TMUX_PWD_' + r['pane_id'].lstrip('%') + if varname in r['environ']: + r['getcwd'] = lambda: r['environ'][varname] + r['mode'] = mode + return r + + +renderer = TmuxRenderer diff --git a/powerline/renderers/vim.py b/powerline/renderers/vim.py new file mode 100644 index 00000000..9cb806f7 --- /dev/null +++ b/powerline/renderers/vim.py @@ -0,0 +1,185 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +import vim + +from powerline.bindings.vim import vim_get_func, vim_getoption, environ, current_tabpage +from powerline.renderer import Renderer +from powerline.colorscheme import ATTR_BOLD, ATTR_ITALIC, ATTR_UNDERLINE +from powerline.theme import Theme +from powerline.lib.unicode import unichr + + +vim_mode = vim_get_func('mode', rettype=str) +if int(vim.eval('v:version')) >= 702: + _vim_mode = vim_mode + vim_mode = lambda: _vim_mode(1) + +mode_translations = { + unichr(ord('V') - 0x40): '^V', + unichr(ord('S') - 0x40): '^S', +} + + +class VimRenderer(Renderer): + '''Powerline vim segment renderer.''' + + character_translations = Renderer.character_translations.copy() + character_translations[ord('%')] = '%%' + + segment_info = Renderer.segment_info.copy() + segment_info.update(environ=environ) + + def __init__(self, *args, **kwargs): + if not hasattr(vim, 'strwidth'): + # Hope nobody want to change this at runtime + if vim.eval('&ambiwidth') == 'double': + kwargs = dict(**kwargs) + kwargs['ambigious'] = 2 + super(VimRenderer, self).__init__(*args, **kwargs) + self.hl_groups = {} + self.prev_highlight = None + + def shutdown(self): + self.theme.shutdown() + for match in self.local_themes.values(): + if 'theme' in match: + match['theme'].shutdown() + + def add_local_theme(self, matcher, theme): + if matcher in self.local_themes: + raise KeyError('There is already a local theme with given matcher') + self.local_themes[matcher] = theme + + def get_matched_theme(self, match): + 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'] + + def get_theme(self, matcher_info): + if matcher_info is None: + return self.get_matched_theme(self.local_themes[None]) + for matcher in self.local_themes.keys(): + if matcher and matcher(matcher_info): + return self.get_matched_theme(self.local_themes[matcher]) + else: + return self.theme + + if hasattr(vim, 'strwidth'): + if sys.version_info < (3,): + @staticmethod + def strwidth(string): + # Does not work with tabs, but neither is strwidth from default + # renderer + return vim.strwidth(string.encode('utf-8')) + else: + @staticmethod + def strwidth(string): + return vim.strwidth(string) + + def get_segment_info(self, segment_info, mode): + return segment_info or self.segment_info + + def render(self, window=None, window_id=None, winnr=None, is_tabline=False): + '''Render all segments.''' + segment_info = self.segment_info.copy() + + if window is vim.current.window: + mode = vim_mode() + mode = mode_translations.get(mode, mode) + else: + mode = 'nc' + + segment_info.update( + window=window, + mode=mode, + window_id=window_id, + winnr=winnr, + buffer=window.buffer, + tabpage=current_tabpage(), + ) + segment_info['tabnr'] = segment_info['tabpage'].number + segment_info['bufnr'] = segment_info['buffer'].number + if is_tabline: + winwidth = int(vim_getoption('columns')) + else: + winwidth = segment_info['window'].width + + statusline = super(VimRenderer, self).render( + mode=mode, + width=winwidth, + segment_info=segment_info, + matcher_info=(None if is_tabline else segment_info), + ) + return statusline + + def reset_highlight(self): + self.hl_groups.clear() + + def hlstyle(self, fg=None, bg=None, attr=None): + '''Highlight a segment. + + If an argument is None, the argument is ignored. If an argument is + False, the argument is reset to the terminal defaults. If an argument + is a valid color or attribute, it's added to the vim highlight group. + ''' + # In order not to hit E541 two consequent identical highlighting + # specifiers may be squashed into one. + attr = attr or 0 # Normalize `attr` + if (fg, bg, attr) == self.prev_highlight: + return '' + self.prev_highlight = (fg, bg, attr) + + # We don't need to explicitly reset attributes in vim, so skip those + # calls + if not attr and not bg and not fg: + return '' + + if not (fg, bg, attr) in self.hl_groups: + hl_group = { + 'ctermfg': 'NONE', + 'guifg': None, + 'ctermbg': 'NONE', + 'guibg': None, + 'attr': ['NONE'], + 'name': '', + } + if fg is not None and fg is not False: + hl_group['ctermfg'] = fg[0] + hl_group['guifg'] = fg[1] + if bg is not None and bg is not False: + hl_group['ctermbg'] = bg[0] + hl_group['guibg'] = bg[1] + if attr: + hl_group['attr'] = [] + if attr & ATTR_BOLD: + hl_group['attr'].append('bold') + if attr & ATTR_ITALIC: + hl_group['attr'].append('italic') + if attr & ATTR_UNDERLINE: + hl_group['attr'].append('underline') + hl_group['name'] = ( + 'Pl_' + + str(hl_group['ctermfg']) + '_' + + str(hl_group['guifg']) + '_' + + str(hl_group['ctermbg']) + '_' + + str(hl_group['guibg']) + '_' + + ''.join(hl_group['attr']) + ) + self.hl_groups[(fg, bg, attr)] = hl_group + vim.command('hi {group} ctermfg={ctermfg} guifg={guifg} guibg={guibg} ctermbg={ctermbg} cterm={attr} gui={attr}'.format( + group=hl_group['name'], + ctermfg=hl_group['ctermfg'], + guifg='#{0:06x}'.format(hl_group['guifg']) if hl_group['guifg'] is not None else 'NONE', + ctermbg=hl_group['ctermbg'], + guibg='#{0:06x}'.format(hl_group['guibg']) if hl_group['guibg'] is not None else 'NONE', + attr=','.join(hl_group['attr']), + )) + return '%#' + self.hl_groups[(fg, bg, attr)]['name'] + '#' + + +renderer = VimRenderer diff --git a/powerline/segment.py b/powerline/segment.py new file mode 100644 index 00000000..edcda434 --- /dev/null +++ b/powerline/segment.py @@ -0,0 +1,418 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lib.watcher import create_file_watcher + + +def list_segment_key_values(segment, theme_configs, segment_data, key, function_name=None, name=None, module=None, default=None): + try: + yield segment[key] + except KeyError: + pass + found_module_key = False + for theme_config in theme_configs: + try: + segment_data = theme_config['segment_data'] + except KeyError: + pass + else: + if function_name and not name: + if module: + try: + yield segment_data[module + '.' + function_name][key] + found_module_key = True + except KeyError: + pass + if not found_module_key: + try: + yield segment_data[function_name][key] + except KeyError: + pass + if name: + try: + yield segment_data[name][key] + except KeyError: + pass + if segment_data is not None: + try: + yield segment_data[key] + except KeyError: + pass + yield default + + +def get_segment_key(merge, *args, **kwargs): + if merge: + ret = None + for value in list_segment_key_values(*args, **kwargs): + if ret is None: + ret = value + elif isinstance(ret, dict) and isinstance(value, dict): + old_ret = ret + ret = value.copy() + ret.update(old_ret) + else: + return ret + return ret + else: + return next(list_segment_key_values(*args, **kwargs)) + + +def get_function(data, segment): + function_name = segment['function'] + if '.' in function_name: + module, function_name = function_name.rpartition('.')[::2] + else: + module = data['default_module'] + function = data['get_module_attr'](module, function_name, prefix='segment_generator') + if not function: + raise ImportError('Failed to obtain segment function') + return None, function, module, function_name, segment.get('name') + + +def get_string(data, segment): + name = segment.get('name') + return data['get_key'](False, segment, None, None, name, 'contents'), None, None, None, name + + +segment_getters = { + 'function': get_function, + 'string': get_string, + 'segment_list': get_function, +} + + +def get_attr_func(contents_func, key, args, is_space_func=False): + try: + func = getattr(contents_func, key) + except AttributeError: + return None + else: + if is_space_func: + def expand_func(pl, amount, segment): + try: + return func(pl=pl, amount=amount, segment=segment, **args) + except Exception as e: + pl.exception('Exception while computing {0} function: {1}', key, str(e)) + return segment['contents'] + (' ' * amount) + return expand_func + else: + return lambda pl, shutdown_event: func(pl=pl, shutdown_event=shutdown_event, **args) + + +def process_segment_lister(pl, segment_info, parsed_segments, side, mode, colorscheme, + lister, subsegments, patcher_args): + subsegments = [ + subsegment + for subsegment in subsegments + if subsegment['display_condition'](pl, segment_info, mode) + ] + for subsegment_info, subsegment_update in lister(pl=pl, segment_info=segment_info, **patcher_args): + draw_inner_divider = subsegment_update.pop('draw_inner_divider', False) + old_pslen = len(parsed_segments) + for subsegment in subsegments: + if subsegment_update: + subsegment = subsegment.copy() + subsegment.update(subsegment_update) + if 'priority_multiplier' in subsegment_update and subsegment['priority']: + subsegment['priority'] *= subsegment_update['priority_multiplier'] + + process_segment( + pl, + side, + subsegment_info, + parsed_segments, + subsegment, + mode, + colorscheme, + ) + new_pslen = len(parsed_segments) + if new_pslen > old_pslen + 1 and draw_inner_divider is not None: + for i in range(old_pslen, new_pslen - 1) if side == 'left' else range(old_pslen + 1, new_pslen): + parsed_segments[i]['draw_soft_divider'] = draw_inner_divider + return None + + +def set_segment_highlighting(pl, colorscheme, segment, mode): + try: + highlight_group_prefix = segment['highlight_group_prefix'] + except KeyError: + hl_groups = lambda hlgs: hlgs + else: + hl_groups = lambda hlgs: [highlight_group_prefix + ':' + hlg for hlg in hlgs] + hlgs + try: + segment['highlight'] = colorscheme.get_highlighting( + hl_groups(segment['highlight_group']), + mode, + segment.get('gradient_level') + ) + if segment['divider_highlight_group']: + segment['divider_highlight'] = colorscheme.get_highlighting( + hl_groups([segment['divider_highlight_group']]), + mode + ) + else: + segment['divider_highlight'] = None + except Exception as e: + pl.exception('Failed to set highlight group: {0}', str(e)) + return False + else: + return True + + +def process_segment(pl, side, segment_info, parsed_segments, segment, mode, colorscheme): + segment = segment.copy() + pl.prefix = segment['name'] + if segment['type'] in ('function', 'segment_list'): + try: + if segment['type'] == 'function': + contents = segment['contents_func'](pl, segment_info) + else: + contents = segment['contents_func'](pl, segment_info, parsed_segments, side, mode, colorscheme) + except Exception as e: + pl.exception('Exception while computing segment: {0}', str(e)) + return + + if contents is None: + return + + if isinstance(contents, list): + # Needs copying here, but it was performed at the very start of the + # function + segment_base = segment + if contents: + draw_divider_position = -1 if side == 'left' else 0 + for key, i, newval in ( + ('before', 0, ''), + ('after', -1, ''), + ('draw_soft_divider', draw_divider_position, True), + ('draw_hard_divider', draw_divider_position, True), + ): + try: + contents[i][key] = segment_base.pop(key) + segment_base[key] = newval + except KeyError: + pass + + draw_inner_divider = None + if side == 'right': + append = parsed_segments.append + else: + pslen = len(parsed_segments) + append = lambda item: parsed_segments.insert(pslen, item) + + for subsegment in (contents if side == 'right' else reversed(contents)): + segment_copy = segment_base.copy() + segment_copy.update(subsegment) + if draw_inner_divider is not None: + segment_copy['draw_soft_divider'] = draw_inner_divider + draw_inner_divider = segment_copy.pop('draw_inner_divider', None) + if set_segment_highlighting(pl, colorscheme, segment_copy, mode): + append(segment_copy) + else: + segment['contents'] = contents + if set_segment_highlighting(pl, colorscheme, segment, mode): + parsed_segments.append(segment) + elif segment['width'] == 'auto' or (segment['type'] == 'string' and segment['contents'] is not None): + if set_segment_highlighting(pl, colorscheme, segment, mode): + parsed_segments.append(segment) + + +always_true = lambda pl, segment_info, mode: True + + +def gen_segment_getter(pl, ext, common_config, theme_configs, default_module, get_module_attr, top_theme): + data = { + 'default_module': default_module or 'powerline.segments.' + ext, + 'get_module_attr': get_module_attr, + 'segment_data': None, + } + + def get_key(merge, segment, module, function_name, name, key, default=None): + return get_segment_key(merge, segment, theme_configs, data['segment_data'], key, function_name, name, module, default) + data['get_key'] = get_key + + def get_selector(function_name): + if '.' in function_name: + module, function_name = function_name.rpartition('.')[::2] + else: + module = 'powerline.selectors.' + ext + function = get_module_attr(module, function_name, prefix='segment_generator/selector_function') + if not function: + pl.error('Failed to get segment selector, ignoring it') + return function + + def get_segment_selector(segment, selector_type): + try: + function_name = segment[selector_type + '_function'] + except KeyError: + function = None + else: + function = get_selector(function_name) + try: + modes = segment[selector_type + '_modes'] + except KeyError: + modes = None + + if modes: + if function: + return lambda pl, segment_info, mode: ( + mode in modes + or function(pl=pl, segment_info=segment_info, mode=mode) + ) + else: + return lambda pl, segment_info, mode: mode in modes + else: + if function: + return lambda pl, segment_info, mode: ( + function(pl=pl, segment_info=segment_info, mode=mode) + ) + else: + return None + + def gen_display_condition(segment): + include_function = get_segment_selector(segment, 'include') + exclude_function = get_segment_selector(segment, 'exclude') + if include_function: + if exclude_function: + return lambda *args: ( + include_function(*args) + and not exclude_function(*args)) + else: + return include_function + else: + if exclude_function: + return lambda *args: not exclude_function(*args) + else: + return always_true + + def get(segment, side): + segment_type = segment.get('type', 'function') + try: + get_segment_info = segment_getters[segment_type] + except KeyError: + pl.error('Unknown segment type: {0}', segment_type) + return None + + try: + contents, _contents_func, module, function_name, name = get_segment_info(data, segment) + except Exception as e: + pl.exception('Failed to generate segment from {0!r}: {1}', segment, str(e), prefix='segment_generator') + return None + + if not get_key(False, segment, module, function_name, name, 'display', True): + return None + + segment_datas = getattr(_contents_func, 'powerline_segment_datas', None) + if segment_datas: + try: + data['segment_data'] = segment_datas[top_theme] + except KeyError: + pass + + if segment_type == 'function': + highlight_group = [function_name] + else: + highlight_group = segment.get('highlight_group') or name + + if segment_type in ('function', 'segment_list'): + args = dict(( + (str(k), v) + for k, v in + get_key(True, segment, module, function_name, name, 'args', {}).items() + )) + + display_condition = gen_display_condition(segment) + + if segment_type == 'segment_list': + # Handle startup and shutdown of _contents_func? + subsegments = [ + subsegment + for subsegment in ( + get(subsegment, side) + for subsegment in segment['segments'] + ) if subsegment + ] + return { + 'name': name or function_name, + 'type': segment_type, + 'highlight_group': None, + 'divider_highlight_group': None, + 'before': None, + 'after': None, + 'contents_func': lambda pl, segment_info, parsed_segments, side, mode, colorscheme: ( + process_segment_lister( + pl, segment_info, parsed_segments, side, mode, colorscheme, + patcher_args=args, + subsegments=subsegments, + lister=_contents_func, + ) + ), + 'contents': None, + 'priority': None, + 'draw_soft_divider': None, + 'draw_hard_divider': None, + 'draw_inner_divider': None, + 'side': side, + 'display_condition': display_condition, + 'width': None, + 'align': None, + 'expand': None, + 'truncate': None, + 'startup': None, + 'shutdown': None, + '_rendered_raw': '', + '_rendered_hl': '', + '_len': None, + '_contents_len': None, + } + + if segment_type == 'function': + startup_func = get_attr_func(_contents_func, 'startup', args) + shutdown_func = getattr(_contents_func, 'shutdown', None) + expand_func = get_attr_func(_contents_func, 'expand', args, True) + truncate_func = get_attr_func(_contents_func, 'truncate', args, True) + + if hasattr(_contents_func, 'powerline_requires_filesystem_watcher'): + create_watcher = lambda: create_file_watcher(pl, common_config['watcher']) + args[str('create_watcher')] = create_watcher + + if hasattr(_contents_func, 'powerline_requires_segment_info'): + contents_func = lambda pl, segment_info: _contents_func(pl=pl, segment_info=segment_info, **args) + else: + contents_func = lambda pl, segment_info: _contents_func(pl=pl, **args) + else: + startup_func = None + shutdown_func = None + contents_func = None + expand_func = None + truncate_func = None + + return { + 'name': name or function_name, + 'type': segment_type, + 'highlight_group': highlight_group, + 'divider_highlight_group': None, + 'before': get_key(False, segment, module, function_name, name, 'before', ''), + 'after': get_key(False, segment, module, function_name, name, 'after', ''), + 'contents_func': contents_func, + 'contents': contents, + 'priority': segment.get('priority', None), + 'draw_hard_divider': segment.get('draw_hard_divider', True), + 'draw_soft_divider': segment.get('draw_soft_divider', True), + 'draw_inner_divider': segment.get('draw_inner_divider', False), + 'side': side, + 'display_condition': display_condition, + 'width': segment.get('width'), + 'align': segment.get('align', 'l'), + 'expand': expand_func, + 'truncate': truncate_func, + 'startup': startup_func, + 'shutdown': shutdown_func, + '_rendered_raw': '', + '_rendered_hl': '', + '_len': None, + '_contents_len': None, + } + + return get diff --git a/powerline/segments/__init__.py b/powerline/segments/__init__.py new file mode 100644 index 00000000..fa09e58a --- /dev/null +++ b/powerline/segments/__init__.py @@ -0,0 +1,63 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from pkgutil import extend_path +from types import MethodType + + +__path__ = extend_path(__path__, __name__) + + +class Segment(object): + '''Base class for any segment that is not a function + + Required for powerline.lint.inspect to work properly: it defines methods for + omitting existing or adding new arguments. + + .. note:: + Until python-3.4 ``inspect.getargspec`` does not support querying + callable classes for arguments of their ``__call__`` method, requiring + to use this method directly (i.e. before 3.4 you should write + ``getargspec(obj.__call__)`` in place of ``getargspec(obj)``). + ''' + if sys.version_info < (3, 4): + def argspecobjs(self): + yield '__call__', self.__call__ + else: + def argspecobjs(self): + yield '__call__', self + + argspecobjs.__doc__ = ( + '''Return a list of valid arguments for inspect.getargspec + + Used to determine function arguments. + ''' + ) + + def omitted_args(self, name, method): + '''List arguments which should be omitted + + Returns a tuple with indexes of omitted arguments. + + .. note::``segment_info``, ``create_watcher`` and ``pl`` will be omitted + regardless of the below return (for ``segment_info`` and + ``create_watcher``: only if object was marked to require segment + info or filesystem watcher). + ''' + if isinstance(self.__call__, MethodType): + return (0,) + else: + return () + + @staticmethod + def additional_args(): + '''Returns a list of (additional argument name[, default value]) tuples. + ''' + return () + + +def with_docstring(instance, doc): + instance.__doc__ = doc + return instance diff --git a/powerline/segments/common.py b/powerline/segments/common.py new file mode 100644 index 00000000..bb83ea89 --- /dev/null +++ b/powerline/segments/common.py @@ -0,0 +1,1469 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import re +import socket + +from datetime import datetime +from multiprocessing import cpu_count as _cpu_count +from collections import namedtuple + +from powerline.lib import add_divider_highlight_group +from powerline.lib.shell import asrun, run_cmd +from powerline.lib.url import urllib_read, urllib_urlencode +from powerline.lib.vcs import guess, tree_status +from powerline.lib.threaded import ThreadedSegment, KwThreadedSegment +from powerline.lib.monotonic import monotonic +from powerline.lib.humanize_bytes import humanize_bytes +from powerline.lib.unicode import u +from powerline.theme import requires_segment_info, requires_filesystem_watcher +from powerline.segments import Segment, with_docstring + + +cpu_count = None + + +@requires_segment_info +def environment(pl, segment_info, variable=None): + '''Return the value of any defined environment variable + + :param string variable: + The environment variable to return if found + ''' + return segment_info['environ'].get(variable, None) + + +@requires_segment_info +def hostname(pl, segment_info, only_if_ssh=False, exclude_domain=False): + '''Return the current hostname. + + :param bool only_if_ssh: + only return the hostname if currently in an SSH session + :param bool exclude_domain: + return the hostname without domain if there is one + ''' + if only_if_ssh and not segment_info['environ'].get('SSH_CLIENT'): + return None + if exclude_domain: + return socket.gethostname().split('.')[0] + return socket.gethostname() + + +@requires_filesystem_watcher +@requires_segment_info +def branch(pl, segment_info, create_watcher, status_colors=False): + '''Return the current VCS branch. + + :param bool status_colors: + determines whether repository status will be used to determine highlighting. Default: False. + + Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``. + ''' + name = segment_info['getcwd']() + repo = guess(path=name, create_watcher=create_watcher) + if repo is not None: + branch = repo.branch() + scol = ['branch'] + if status_colors: + try: + status = tree_status(repo, pl) + except Exception as e: + pl.exception('Failed to compute tree status: {0}', str(e)) + status = '?' + scol.insert(0, 'branch_dirty' if status and status.strip() else 'branch_clean') + return [{ + 'contents': branch, + 'highlight_group': scol, + }] + + +@requires_segment_info +class CwdSegment(Segment): + def argspecobjs(self): + for obj in super(CwdSegment, self).argspecobjs(): + yield obj + yield 'get_shortened_path', self.get_shortened_path + + def omitted_args(self, name, method): + if method is self.get_shortened_path: + return (0, 1, 2) + else: + return super(CwdSegment, self).omitted_args(name, method) + + def get_shortened_path(self, pl, segment_info, shorten_home=True, **kwargs): + try: + path = u(segment_info['getcwd']()) + except OSError as e: + if e.errno == 2: + # user most probably deleted the directory + # this happens when removing files from Mercurial repos for example + pl.warn('Current directory not found') + return "[not found]" + else: + raise + if shorten_home: + home = segment_info['home'] + if home: + home = u(home) + if path.startswith(home): + path = '~' + path[len(home):] + return path + + def __call__(self, pl, segment_info, + dir_shorten_len=None, + dir_limit_depth=None, + use_path_separator=False, + ellipsis='...', + **kwargs): + cwd = self.get_shortened_path(pl, segment_info, **kwargs) + cwd_split = cwd.split(os.sep) + cwd_split_len = len(cwd_split) + cwd = [i[0:dir_shorten_len] if dir_shorten_len and i else i for i in cwd_split[:-1]] + [cwd_split[-1]] + if dir_limit_depth and cwd_split_len > dir_limit_depth + 1: + del(cwd[0:-dir_limit_depth]) + if ellipsis is not None: + cwd.insert(0, ellipsis) + ret = [] + if not cwd[0]: + cwd[0] = '/' + draw_inner_divider = not use_path_separator + for part in cwd: + if not part: + continue + if use_path_separator: + part += os.sep + ret.append({ + 'contents': part, + 'divider_highlight_group': 'cwd:divider', + 'draw_inner_divider': draw_inner_divider, + }) + ret[-1]['highlight_group'] = ['cwd:current_folder', 'cwd'] + if use_path_separator: + ret[-1]['contents'] = ret[-1]['contents'][:-1] + if len(ret) > 1 and ret[0]['contents'][0] == os.sep: + ret[0]['contents'] = ret[0]['contents'][1:] + return ret + + +cwd = with_docstring(CwdSegment(), +'''Return the current working directory. + +Returns a segment list to create a breadcrumb-like effect. + +:param int dir_shorten_len: + shorten parent directory names to this length (e.g. + :file:`/long/path/to/powerline` → :file:`/l/p/t/powerline`) +:param int dir_limit_depth: + limit directory depth to this number (e.g. + :file:`/long/path/to/powerline` → :file:`⋯/to/powerline`) +:param bool use_path_separator: + Use path separator in place of soft divider. +:param bool shorten_home: + Shorten home directory to ``~``. +:param str ellipsis: + Specifies what to use in place of omitted directories. Use None to not + show this subsegment at all. + +Divider highlight group used: ``cwd:divider``. + +Highlight groups used: ``cwd:current_folder`` or ``cwd``. It is recommended to define all highlight groups. +''') + + +def date(pl, format='%Y-%m-%d', istime=False): + '''Return the current date. + + :param str format: + strftime-style date format string + :param bool istime: + If true then segment uses ``time`` highlight group. + + Divider highlight group used: ``time:divider``. + + Highlight groups used: ``time`` or ``date``. + ''' + return [{ + 'contents': datetime.now().strftime(format), + 'highlight_group': (['time'] if istime else []) + ['date'], + 'divider_highlight_group': 'time:divider' if istime else None, + }] + + +UNICODE_TEXT_TRANSLATION = { + ord('\''): '’', + ord('-'): '‐', +} + + +def fuzzy_time(pl, unicode_text=False): + '''Display the current time as fuzzy time, e.g. "quarter past six". + + :param bool unicode_text: + If true then hyphenminuses (regular ASCII ``-``) and single quotes are + replaced with unicode dashes and apostrophes. + ''' + hour_str = ['twelve', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven'] + minute_str = { + 5: 'five past', + 10: 'ten past', + 15: 'quarter past', + 20: 'twenty past', + 25: 'twenty-five past', + 30: 'half past', + 35: 'twenty-five to', + 40: 'twenty to', + 45: 'quarter to', + 50: 'ten to', + 55: 'five to', + } + special_case_str = { + (23, 58): 'round about midnight', + (23, 59): 'round about midnight', + (0, 0): 'midnight', + (0, 1): 'round about midnight', + (0, 2): 'round about midnight', + (12, 0): 'noon', + } + + now = datetime.now() + + try: + return special_case_str[(now.hour, now.minute)] + except KeyError: + pass + + hour = now.hour + if now.minute > 32: + if hour == 23: + hour = 0 + else: + hour += 1 + if hour > 11: + hour = hour - 12 + hour = hour_str[hour] + + minute = int(round(now.minute / 5.0) * 5) + if minute == 60 or minute == 0: + result = ' '.join([hour, 'o\'clock']) + else: + minute = minute_str[minute] + result = ' '.join([minute, hour]) + + if unicode_text: + result = result.translate(UNICODE_TEXT_TRANSLATION) + + return result + + +def _external_ip(query_url='http://ipv4.icanhazip.com/'): + return urllib_read(query_url).strip() + + +class ExternalIpSegment(ThreadedSegment): + interval = 300 + + def set_state(self, query_url='http://ipv4.icanhazip.com/', **kwargs): + self.query_url = query_url + super(ExternalIpSegment, self).set_state(**kwargs) + + def update(self, old_ip): + return _external_ip(query_url=self.query_url) + + def render(self, ip, **kwargs): + if not ip: + return None + return [{'contents': ip, 'divider_highlight_group': 'background:divider'}] + + +external_ip = with_docstring(ExternalIpSegment(), +'''Return external IP address. + +:param str query_url: + URI to query for IP address, should return only the IP address as a text string + + Suggested URIs: + + * http://ipv4.icanhazip.com/ + * http://ipv6.icanhazip.com/ + * http://icanhazip.com/ (returns IPv6 address if available, else IPv4) + +Divider highlight group used: ``background:divider``. +''') + + +try: + import netifaces +except ImportError: + def internal_ip(pl, interface='detect', ipv=4): + return None +else: + _interface_starts = { + 'eth': 10, # Regular ethernet adapters : eth1 + 'enp': 10, # Regular ethernet adapters, Gentoo : enp2s0 + 'ath': 9, # Atheros WiFi adapters : ath0 + 'wlan': 9, # Other WiFi adapters : wlan1 + 'wlp': 9, # Other WiFi adapters, Gentoo : wlp5s0 + 'teredo': 1, # miredo interface : teredo + 'lo': -10, # Loopback interface : lo + } + + _interface_start_re = re.compile(r'^([a-z]+?)(\d|$)') + + def _interface_key(interface): + match = _interface_start_re.match(interface) + if match: + try: + base = _interface_starts[match.group(1)] * 100 + except KeyError: + base = 500 + if match.group(2): + return base - int(match.group(2)) + else: + return base + else: + return 0 + + def internal_ip(pl, interface='detect', ipv=4): + if interface == 'detect': + try: + interface = next(iter(sorted(netifaces.interfaces(), key=_interface_key, reverse=True))) + except StopIteration: + pl.info('No network interfaces found') + return None + addrs = netifaces.ifaddresses(interface) + try: + return addrs[netifaces.AF_INET6 if ipv == 6 else netifaces.AF_INET][0]['addr'] + except (KeyError, IndexError): + return None + + +internal_ip = with_docstring(internal_ip, +'''Return internal IP address + +Requires ``netifaces`` package to work properly. + +:param str interface: + Interface on which IP will be checked. Use ``detect`` to automatically + detect interface. In this case interfaces with lower numbers will be + preferred over interfaces with similar names. Order of preference based on + names: + + #. ``eth`` and ``enp`` followed by number or the end of string. + #. ``ath``, ``wlan`` and ``wlp`` followed by number or the end of string. + #. ``teredo`` followed by number or the end of string. + #. Any other interface that is not ``lo*``. + #. ``lo`` followed by number or the end of string. +:param int ipv: + 4 or 6 for ipv4 and ipv6 respectively, depending on which IP address you + need exactly. +''') + + +# Weather condition code descriptions available at +# http://developer.yahoo.com/weather/#codes +weather_conditions_codes = ( + ('tornado', 'stormy'), # 0 + ('tropical_storm', 'stormy'), # 1 + ('hurricane', 'stormy'), # 2 + ('severe_thunderstorms', 'stormy'), # 3 + ('thunderstorms', 'stormy'), # 4 + ('mixed_rain_and_snow', 'rainy' ), # 5 + ('mixed_rain_and_sleet', 'rainy' ), # 6 + ('mixed_snow_and_sleet', 'snowy' ), # 7 + ('freezing_drizzle', 'rainy' ), # 8 + ('drizzle', 'rainy' ), # 9 + ('freezing_rain', 'rainy' ), # 10 + ('showers', 'rainy' ), # 11 + ('showers', 'rainy' ), # 12 + ('snow_flurries', 'snowy' ), # 13 + ('light_snow_showers', 'snowy' ), # 14 + ('blowing_snow', 'snowy' ), # 15 + ('snow', 'snowy' ), # 16 + ('hail', 'snowy' ), # 17 + ('sleet', 'snowy' ), # 18 + ('dust', 'foggy' ), # 19 + ('fog', 'foggy' ), # 20 + ('haze', 'foggy' ), # 21 + ('smoky', 'foggy' ), # 22 + ('blustery', 'foggy' ), # 23 + ('windy', ), # 24 + ('cold', 'day' ), # 25 + ('clouds', 'cloudy'), # 26 + ('mostly_cloudy_night', 'cloudy'), # 27 + ('mostly_cloudy_day', 'cloudy'), # 28 + ('partly_cloudy_night', 'cloudy'), # 29 + ('partly_cloudy_day', 'cloudy'), # 30 + ('clear_night', 'night' ), # 31 + ('sun', 'sunny' ), # 32 + ('fair_night', 'night' ), # 33 + ('fair_day', 'day' ), # 34 + ('mixed_rain_and_hail', 'rainy' ), # 35 + ('hot', 'sunny' ), # 36 + ('isolated_thunderstorms', 'stormy'), # 37 + ('scattered_thunderstorms', 'stormy'), # 38 + ('scattered_thunderstorms', 'stormy'), # 39 + ('scattered_showers', 'rainy' ), # 40 + ('heavy_snow', 'snowy' ), # 41 + ('scattered_snow_showers', 'snowy' ), # 42 + ('heavy_snow', 'snowy' ), # 43 + ('partly_cloudy', 'cloudy'), # 44 + ('thundershowers', 'rainy' ), # 45 + ('snow_showers', 'snowy' ), # 46 + ('isolated_thundershowers', 'rainy' ), # 47 +) +# ('day', (25, 34)), +# ('rainy', (5, 6, 8, 9, 10, 11, 12, 35, 40, 45, 47)), +# ('cloudy', (26, 27, 28, 29, 30, 44)), +# ('snowy', (7, 13, 14, 15, 16, 17, 18, 41, 42, 43, 46)), +# ('stormy', (0, 1, 2, 3, 4, 37, 38, 39)), +# ('foggy', (19, 20, 21, 22, 23)), +# ('sunny', (32, 36)), +# ('night', (31, 33))): +weather_conditions_icons = { + 'day': 'DAY', + 'blustery': 'WIND', + 'rainy': 'RAIN', + 'cloudy': 'CLOUDS', + 'snowy': 'SNOW', + 'stormy': 'STORM', + 'foggy': 'FOG', + 'sunny': 'SUN', + 'night': 'NIGHT', + 'windy': 'WINDY', + 'not_available': 'NA', + 'unknown': 'UKN', +} + +temp_conversions = { + 'C': lambda temp: temp, + 'F': lambda temp: (temp * 9 / 5) + 32, + 'K': lambda temp: temp + 273.15, +} + +# Note: there are also unicode characters for units: ℃, ℉ and K +temp_units = { + 'C': '°C', + 'F': '°F', + 'K': 'K', +} + + +class WeatherSegment(ThreadedSegment): + interval = 600 + + def set_state(self, location_query=None, **kwargs): + self.location = location_query + self.url = None + super(WeatherSegment, self).set_state(**kwargs) + + def update(self, old_weather): + import json + + if not self.url: + # Do not lock attribute assignments in this branch: they are used + # only in .update() + if not self.location: + location_data = json.loads(urllib_read('http://freegeoip.net/json/')) + self.location = ','.join(( + location_data['city'], + location_data['region_code'], + location_data['country_code'] + )) + query_data = { + 'q': + 'use "https://raw.githubusercontent.com/yql/yql-tables/master/weather/weather.bylocation.xml" as we;' + 'select * from we where location="{0}" and unit="c"'.format(self.location).encode('utf-8'), + 'format': 'json', + } + self.url = 'http://query.yahooapis.com/v1/public/yql?' + urllib_urlencode(query_data) + + raw_response = urllib_read(self.url) + if not raw_response: + self.error('Failed to get response') + return + response = json.loads(raw_response) + condition = response['query']['results']['weather']['rss']['channel']['item']['condition'] + condition_code = int(condition['code']) + temp = float(condition['temp']) + + try: + icon_names = weather_conditions_codes[condition_code] + except IndexError: + if condition_code == 3200: + icon_names = ('not_available',) + self.warn('Weather is not available for location {0}', self.location) + else: + icon_names = ('unknown',) + self.error('Unknown condition code: {0}', condition_code) + + return (temp, icon_names) + + def render(self, weather, icons=None, unit='C', temp_format=None, temp_coldest=-30, temp_hottest=40, **kwargs): + if not weather: + return None + + temp, icon_names = weather + + for icon_name in icon_names: + if icons: + if icon_name in icons: + icon = icons[icon_name] + break + else: + icon = weather_conditions_icons[icon_names[-1]] + + temp_format = temp_format or ('{temp:.0f}' + temp_units[unit]) + converted_temp = temp_conversions[unit](temp) + if temp <= temp_coldest: + gradient_level = 0 + elif temp >= temp_hottest: + gradient_level = 100 + else: + gradient_level = (temp - temp_coldest) * 100.0 / (temp_hottest - temp_coldest) + groups = ['weather_condition_' + icon_name for icon_name in icon_names] + ['weather_conditions', 'weather'] + return [ + { + 'contents': icon + ' ', + 'highlight_group': groups, + 'divider_highlight_group': 'background:divider', + }, + { + 'contents': temp_format.format(temp=converted_temp), + 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], + 'divider_highlight_group': 'background:divider', + 'gradient_level': gradient_level, + }, + ] + + +weather = with_docstring(WeatherSegment(), +'''Return weather from Yahoo! Weather. + +Uses GeoIP lookup from http://freegeoip.net/ to automatically determine +your current location. This should be changed if you're in a VPN or if your +IP address is registered at another location. + +Returns a list of colorized icon and temperature segments depending on +weather conditions. + +:param str unit: + temperature unit, can be one of ``F``, ``C`` or ``K`` +:param str location_query: + location query for your current location, e.g. ``oslo, norway`` +:param dict icons: + dict for overriding default icons, e.g. ``{'heavy_snow' : u'❆'}`` +:param str temp_format: + format string, receives ``temp`` as an argument. Should also hold unit. +:param float temp_coldest: + coldest temperature. Any temperature below it will have gradient level equal + to zero. +:param float temp_hottest: + hottest temperature. Any temperature above it will have gradient level equal + to 100. Temperatures between ``temp_coldest`` and ``temp_hottest`` receive + gradient level that indicates relative position in this interval + (``100 * (cur-coldest) / (hottest-coldest)``). + +Divider highlight group used: ``background:divider``. + +Highlight groups used: ``weather_conditions`` or ``weather``, ``weather_temp_gradient`` (gradient) or ``weather``. +Also uses ``weather_conditions_{condition}`` for all weather conditions supported by Yahoo. +''') + + +def system_load(pl, format='{avg:.1f}', threshold_good=1, threshold_bad=2, track_cpu_count=False): + '''Return system load average. + + Highlights using ``system_load_good``, ``system_load_bad`` and + ``system_load_ugly`` highlighting groups, depending on the thresholds + passed to the function. + + :param str format: + format string, receives ``avg`` as an argument + :param float threshold_good: + threshold for gradient level 0: any normalized load average below this + value will have this gradient level. + :param float threshold_bad: + threshold for gradient level 100: any normalized load average above this + value will have this gradient level. Load averages between + ``threshold_good`` and ``threshold_bad`` receive gradient level that + indicates relative position in this interval: + (``100 * (cur-good) / (bad-good)``). + Note: both parameters are checked against normalized load averages. + :param bool track_cpu_count: + if True powerline will continuously poll the system to detect changes + in the number of CPUs. + + Divider highlight group used: ``background:divider``. + + Highlight groups used: ``system_load_gradient`` (gradient) or ``system_load``. + ''' + global cpu_count + try: + cpu_num = cpu_count = _cpu_count() if cpu_count is None or track_cpu_count else cpu_count + except NotImplementedError: + pl.warn('Unable to get CPU count: method is not implemented') + return None + ret = [] + for avg in os.getloadavg(): + normalized = avg / cpu_num + if normalized < threshold_good: + gradient_level = 0 + elif normalized < threshold_bad: + gradient_level = (normalized - threshold_good) * 100.0 / (threshold_bad - threshold_good) + else: + gradient_level = 100 + ret.append({ + 'contents': format.format(avg=avg), + 'highlight_group': ['system_load_gradient', 'system_load'], + 'divider_highlight_group': 'background:divider', + 'gradient_level': gradient_level, + }) + ret[0]['contents'] += ' ' + ret[1]['contents'] += ' ' + return ret + + +try: + import psutil + + def _get_bytes(interface): + try: + io_counters = psutil.net_io_counters(pernic=True) + except AttributeError: + io_counters = psutil.network_io_counters(pernic=True) + if_io = io_counters.get(interface) + if not if_io: + return None + return if_io.bytes_recv, if_io.bytes_sent + + def _get_interfaces(): + io_counters = psutil.network_io_counters(pernic=True) + for interface, data in io_counters.items(): + if data: + yield interface, data.bytes_recv, data.bytes_sent + + # psutil-2.0.0: psutil.Process.username is unbound method + if callable(psutil.Process.username): + def _get_user(segment_info): + return psutil.Process(os.getpid()).username() + # pre psutil-2.0.0: psutil.Process.username has type property + else: + def _get_user(segment_info): + return psutil.Process(os.getpid()).username + + class CPULoadPercentSegment(ThreadedSegment): + interval = 1 + + def update(self, old_cpu): + return psutil.cpu_percent(interval=None) + + def run(self): + while not self.shutdown_event.is_set(): + try: + self.update_value = psutil.cpu_percent(interval=self.interval) + except Exception as e: + self.exception('Exception while calculating cpu_percent: {0}', str(e)) + + def render(self, cpu_percent, format='{0:.0f}%', **kwargs): + if not cpu_percent: + return None + return [{ + 'contents': format.format(cpu_percent), + 'gradient_level': cpu_percent, + 'highlight_group': ['cpu_load_percent_gradient', 'cpu_load_percent'], + }] +except ImportError: + def _get_bytes(interface): + with open('/sys/class/net/{interface}/statistics/rx_bytes'.format(interface=interface), 'rb') as file_obj: + rx = int(file_obj.read()) + with open('/sys/class/net/{interface}/statistics/tx_bytes'.format(interface=interface), 'rb') as file_obj: + tx = int(file_obj.read()) + return (rx, tx) + + def _get_interfaces(): + for interface in os.listdir('/sys/class/net'): + x = _get_bytes(interface) + if x is not None: + yield interface, x[0], x[1] + + def _get_user(segment_info): + return segment_info['environ'].get('USER', None) + + class CPULoadPercentSegment(ThreadedSegment): + interval = 1 + + @staticmethod + def startup(**kwargs): + pass + + @staticmethod + def start(): + pass + + @staticmethod + def shutdown(): + pass + + @staticmethod + def render(cpu_percent, pl, format='{0:.0f}%', **kwargs): + pl.warn('psutil package is not installed, thus CPU load is not available') + return None + + +cpu_load_percent = with_docstring(CPULoadPercentSegment(), +'''Return the average CPU load as a percentage. + +Requires the ``psutil`` module. + +:param str format: + Output format. Accepts measured CPU load as the first argument. + +Highlight groups used: ``cpu_load_percent_gradient`` (gradient) or ``cpu_load_percent``. +''') + + +username = False +# os.geteuid is not available on windows +_geteuid = getattr(os, 'geteuid', lambda: 1) + + +def user(pl, segment_info=None, hide_user=None): + '''Return the current user. + + :param str hide_user: + Omit showing segment for users with names equal to this string. + + Highlights the user with the ``superuser`` if the effective user ID is 0. + + Highlight groups used: ``superuser`` or ``user``. It is recommended to define all highlight groups. + ''' + global username + if username is False: + username = _get_user(segment_info) + if username is None: + pl.warn('Failed to get username') + return None + if username == hide_user: + return None + euid = _geteuid() + return [{ + 'contents': username, + 'highlight_group': ['user'] if euid != 0 else ['superuser', 'user'], + }] +if 'psutil' not in globals(): + user = requires_segment_info(user) + + +if os.path.exists('/proc/uptime'): + def _get_uptime(): + with open('/proc/uptime', 'r') as f: + return int(float(f.readline().split()[0])) +elif 'psutil' in globals(): + from time import time + + def _get_uptime(): + # psutil.BOOT_TIME is not subject to clock adjustments, but time() is. + # Thus it is a fallback to /proc/uptime reading and not the reverse. + return int(time() - psutil.BOOT_TIME) +else: + def _get_uptime(): + raise NotImplementedError + + +@add_divider_highlight_group('background:divider') +def uptime(pl, days_format='{days:d}d', hours_format=' {hours:d}h', minutes_format=' {minutes:d}m', seconds_format=' {seconds:d}s', shorten_len=3): + '''Return system uptime. + + :param str days_format: + day format string, will be passed ``days`` as the argument + :param str hours_format: + hour format string, will be passed ``hours`` as the argument + :param str minutes_format: + minute format string, will be passed ``minutes`` as the argument + :param str seconds_format: + second format string, will be passed ``seconds`` as the argument + :param int shorten_len: + shorten the amount of units (days, hours, etc.) displayed + + Divider highlight group used: ``background:divider``. + ''' + try: + seconds = _get_uptime() + except NotImplementedError: + pl.warn('Unable to get uptime. You should install psutil package') + return None + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + time_formatted = list(filter(None, [ + days_format.format(days=days) if days and days_format else None, + hours_format.format(hours=hours) if hours and hours_format else None, + minutes_format.format(minutes=minutes) if minutes and minutes_format else None, + seconds_format.format(seconds=seconds) if seconds and seconds_format else None, + ]))[0:shorten_len] + return ''.join(time_formatted).strip() + + +class NetworkLoadSegment(KwThreadedSegment): + interfaces = {} + replace_num_pat = re.compile(r'[a-zA-Z]+') + + @staticmethod + def key(interface='detect', **kwargs): + return interface + + def compute_state(self, interface): + if interface == 'detect': + proc_exists = getattr(self, 'proc_exists', None) + if proc_exists is None: + proc_exists = self.proc_exists = os.path.exists('/proc/net/route') + if proc_exists: + # Look for default interface in routing table + with open('/proc/net/route', 'rb') as f: + for line in f.readlines(): + parts = line.split() + if len(parts) > 1: + iface, destination = parts[:2] + if not destination.replace(b'0', b''): + interface = iface.decode('utf-8') + break + if interface == 'detect': + # Choose interface with most total activity, excluding some + # well known interface names + interface, total = 'eth0', -1 + for name, rx, tx in _get_interfaces(): + base = self.replace_num_pat.match(name) + if None in (base, rx, tx) or base.group() in ('lo', 'vmnet', 'sit'): + continue + activity = rx + tx + if activity > total: + total = activity + interface = name + + try: + idata = self.interfaces[interface] + try: + idata['prev'] = idata['last'] + except KeyError: + pass + except KeyError: + idata = {} + if self.run_once: + idata['prev'] = (monotonic(), _get_bytes(interface)) + self.shutdown_event.wait(self.interval) + self.interfaces[interface] = idata + + idata['last'] = (monotonic(), _get_bytes(interface)) + return idata.copy() + + def render_one(self, idata, recv_format='DL {value:>8}', sent_format='UL {value:>8}', suffix='B/s', si_prefix=False, **kwargs): + if not idata or 'prev' not in idata: + return None + + t1, b1 = idata['prev'] + t2, b2 = idata['last'] + measure_interval = t2 - t1 + + if None in (b1, b2): + return None + + r = [] + for i, key in zip((0, 1), ('recv', 'sent')): + format = locals()[key + '_format'] + try: + value = (b2[i] - b1[i]) / measure_interval + except ZeroDivisionError: + self.warn('Measure interval zero.') + value = 0 + max_key = key + '_max' + is_gradient = max_key in kwargs + hl_groups = ['network_load_' + key, 'network_load'] + if is_gradient: + hl_groups[:0] = (group + '_gradient' for group in hl_groups) + r.append({ + 'contents': format.format(value=humanize_bytes(value, suffix, si_prefix)), + 'divider_highlight_group': 'background:divider', + 'highlight_group': hl_groups, + }) + if is_gradient: + max = kwargs[max_key] + if value >= max: + r[-1]['gradient_level'] = 100 + else: + r[-1]['gradient_level'] = value * 100.0 / max + + return r + + +network_load = with_docstring(NetworkLoadSegment(), +'''Return the network load. + +Uses the ``psutil`` module if available for multi-platform compatibility, +falls back to reading +:file:`/sys/class/net/{interface}/statistics/{rx,tx}_bytes`. + +:param str interface: + network interface to measure (use the special value "detect" to have powerline try to auto-detect the network interface) +:param str suffix: + string appended to each load string +:param bool si_prefix: + use SI prefix, e.g. MB instead of MiB +:param str recv_format: + format string, receives ``value`` as argument +:param str sent_format: + format string, receives ``value`` as argument +:param float recv_max: + maximum number of received bytes per second. Is only used to compute + gradient level +:param float sent_max: + maximum number of sent bytes per second. Is only used to compute gradient + level + +Divider highlight group used: ``background:divider``. + +Highlight groups used: ``network_load_sent_gradient`` (gradient) or ``network_load_recv_gradient`` (gradient) or ``network_load_gradient`` (gradient), ``network_load_sent`` or ``network_load_recv`` or ``network_load``. +''') + + +@requires_segment_info +def virtualenv(pl, segment_info): + '''Return the name of the current Python virtualenv.''' + return os.path.basename(segment_info['environ'].get('VIRTUAL_ENV', '')) or None + + +_IMAPKey = namedtuple('Key', 'username password server port folder') + + +class EmailIMAPSegment(KwThreadedSegment): + interval = 60 + + @staticmethod + def key(username, password, server='imap.gmail.com', port=993, folder='INBOX', **kwargs): + return _IMAPKey(username, password, server, port, folder) + + def compute_state(self, key): + if not key.username or not key.password: + self.warn('Username and password are not configured') + return None + try: + import imaplib + except imaplib.IMAP4.error as e: + unread_count = str(e) + else: + mail = imaplib.IMAP4_SSL(key.server, key.port) + mail.login(key.username, key.password) + rc, message = mail.status(key.folder, '(UNSEEN)') + unread_str = message[0].decode('utf-8') + unread_count = int(re.search('UNSEEN (\d+)', unread_str).group(1)) + return unread_count + + @staticmethod + def render_one(unread_count, max_msgs=None, **kwargs): + if not unread_count: + return None + elif type(unread_count) != int or not max_msgs: + return [{ + 'contents': str(unread_count), + 'highlight_group': ['email_alert'], + }] + else: + return [{ + 'contents': str(unread_count), + 'highlight_group': ['email_alert_gradient', 'email_alert'], + 'gradient_level': min(unread_count * 100.0 / max_msgs, 100), + }] + + +email_imap_alert = with_docstring(EmailIMAPSegment(), +'''Return unread e-mail count for IMAP servers. + +:param str username: + login username +:param str password: + login password +:param str server: + e-mail server +:param int port: + e-mail server port +:param str folder: + folder to check for e-mails +:param int max_msgs: + Maximum number of messages. If there are more messages then max_msgs then it + will use gradient level equal to 100, otherwise gradient level is equal to + ``100 * msgs_num / max_msgs``. If not present gradient is not computed. + +Highlight groups used: ``email_alert_gradient`` (gradient), ``email_alert``. +''') + + +STATE_SYMBOLS = { + 'fallback': '', + 'play': '>', + 'pause': '~', + 'stop': 'X', +} + + +class NowPlayingSegment(Segment): + def __call__(self, player='mpd', format='{state_symbol} {artist} - {title} ({total})', state_symbols=STATE_SYMBOLS, **kwargs): + player_func = getattr(self, 'player_{0}'.format(player)) + stats = { + 'state': 'fallback', + 'album': None, + 'artist': None, + 'title': None, + 'elapsed': None, + 'total': None, + } + func_stats = player_func(**kwargs) + if not func_stats: + return None + stats.update(func_stats) + stats['state_symbol'] = state_symbols.get(stats['state']) + return format.format(**stats) + + @staticmethod + def _convert_state(state): + state = state.lower() + if 'play' in state: + return 'play' + if 'pause' in state: + return 'pause' + if 'stop' in state: + return 'stop' + + @staticmethod + def _convert_seconds(seconds): + return '{0:.0f}:{1:02.0f}'.format(*divmod(float(seconds), 60)) + + def player_cmus(self, pl): + '''Return cmus player information. + + cmus-remote -Q returns data with multi-level information i.e. + status playing + file + tag artist + tag title + tag .. + tag n + set continue + set repeat + set .. + set n + + For the information we are looking for we don't really care if we're on + the tag level or the set level. The dictionary comprehension in this + method takes anything in ignore_levels and brings the key inside that + to the first level of the dictionary. + ''' + now_playing_str = run_cmd(pl, ['cmus-remote', '-Q']) + if not now_playing_str: + return + ignore_levels = ('tag', 'set',) + now_playing = dict(((token[0] if token[0] not in ignore_levels else token[1], + (' '.join(token[1:]) if token[0] not in ignore_levels else + ' '.join(token[2:]))) for token in [line.split(' ') for line in now_playing_str.split('\n')[:-1]])) + state = self._convert_state(now_playing.get('status')) + return { + 'state': state, + 'album': now_playing.get('album'), + 'artist': now_playing.get('artist'), + 'title': now_playing.get('title'), + 'elapsed': self._convert_seconds(now_playing.get('position', 0)), + 'total': self._convert_seconds(now_playing.get('duration', 0)), + } + + def player_mpd(self, pl, host='localhost', port=6600): + try: + import mpd + except ImportError: + now_playing = run_cmd(pl, ['mpc', 'current', '-f', '%album%\n%artist%\n%title%\n%time%', '-h', str(host), '-p', str(port)]) + if not now_playing: + return + now_playing = now_playing.split('\n') + return { + 'album': now_playing[0], + 'artist': now_playing[1], + 'title': now_playing[2], + 'total': now_playing[3], + } + else: + client = mpd.MPDClient() + client.connect(host, port) + now_playing = client.currentsong() + if not now_playing: + return + status = client.status() + client.close() + client.disconnect() + return { + 'state': status.get('state'), + 'album': now_playing.get('album'), + 'artist': now_playing.get('artist'), + 'title': now_playing.get('title'), + 'elapsed': self._convert_seconds(now_playing.get('elapsed', 0)), + 'total': self._convert_seconds(now_playing.get('time', 0)), + } + + def player_dbus(self, player_name, bus_name, player_path, iface_prop, iface_player): + try: + import dbus + except ImportError: + pl.exception('Could not add {0} segment: requires python-dbus', player_name) + return + bus = dbus.SessionBus() + try: + player = bus.get_object(bus_name, player_path) + iface = dbus.Interface(player, iface_prop) + info = iface.Get(iface_player, 'Metadata') + status = iface.Get(iface_player, 'PlaybackStatus') + except dbus.exceptions.DBusException: + return + if not info: + return + album = u(info.get('xesam:album')) + title = u(info.get('xesam:title')) + artist = info.get('xesam:artist') + state = self._convert_state(status) + if artist: + artist = u(artist[0]) + return { + 'state': state, + 'album': album, + 'artist': artist, + 'title': title, + 'total': self._convert_seconds(info.get('mpris:length') / 1e6), + } + + def player_spotify_dbus(self, pl): + return self.player_dbus( + player_name='Spotify', + bus_name='com.spotify.qt', + player_path='/', + iface_prop='org.freedesktop.DBus.Properties', + iface_player='org.freedesktop.MediaPlayer2', + ) + + def player_clementine(self, pl): + return self.player_dbus( + player_name='Clementine', + bus_name='org.mpris.MediaPlayer2.clementine', + player_path='/org/mpris/MediaPlayer2', + iface_prop='org.freedesktop.DBus.Properties', + iface_player='org.mpris.MediaPlayer2.Player', + ) + + def player_spotify_apple_script(self, pl): + status_delimiter = '-~`/=' + ascript = ''' + tell application "System Events" + set process_list to (name of every process) + end tell + + if process_list contains "Spotify" then + tell application "Spotify" + if player state is playing or player state is paused then + set track_name to name of current track + set artist_name to artist of current track + set album_name to album of current track + set track_length to duration of current track + set now_playing to "" & player state & "{0}" & album_name & "{0}" & artist_name & "{0}" & track_name & "{0}" & track_length + return now_playing + else + return player state + end if + + end tell + else + return "stopped" + end if + '''.format(status_delimiter) + + spotify = asrun(pl, ascript) + if not asrun: + return None + + spotify_status = spotify.split(status_delimiter) + state = self._convert_state(spotify_status[0]) + if state == 'stop': + return None + return { + 'state': state, + 'album': spotify_status[1], + 'artist': spotify_status[2], + 'title': spotify_status[3], + 'total': self._convert_seconds(int(spotify_status[4])) + } + + try: + __import__('dbus') + except ImportError: + if sys.platform.startswith('darwin'): + player_spotify = player_spotify_apple_script + else: + player_spotify = player_spotify_dbus + else: + player_spotify = player_spotify_dbus + + def player_rhythmbox(self, pl): + now_playing = run_cmd(pl, ['rhythmbox-client', '--no-start', '--no-present', '--print-playing-format', '%at\n%aa\n%tt\n%te\n%td']) + if not now_playing: + return + now_playing = now_playing.split('\n') + return { + 'album': now_playing[0], + 'artist': now_playing[1], + 'title': now_playing[2], + 'elapsed': now_playing[3], + 'total': now_playing[4], + } + + def player_rdio(self, pl): + status_delimiter = '-~`/=' + ascript = ''' + tell application "System Events" + set rdio_active to the count(every process whose name is "Rdio") + if rdio_active is 0 then + return + end if + end tell + tell application "Rdio" + set rdio_name to the name of the current track + set rdio_artist to the artist of the current track + set rdio_album to the album of the current track + set rdio_duration to the duration of the current track + set rdio_state to the player state + set rdio_elapsed to the player position + return rdio_name & "{0}" & rdio_artist & "{0}" & rdio_album & "{0}" & rdio_elapsed & "{0}" & rdio_duration & "{0}" & rdio_state + end tell + '''.format(status_delimiter) + now_playing = asrun(pl, ascript) + if not now_playing: + return + now_playing = now_playing.split('\n') + if len(now_playing) != 6: + return + state = self._convert_state(now_playing[5]) + total = self._convert_seconds(now_playing[4]) + elapsed = self._convert_seconds(float(now_playing[3]) * float(now_playing[4]) / 100) + return { + 'title': now_playing[0], + 'artist': now_playing[1], + 'album': now_playing[2], + 'elapsed': elapsed, + 'total': total, + 'state': state, + 'state_symbol': self.STATE_SYMBOLS.get(state) + } +now_playing = NowPlayingSegment() + + +def _get_battery(pl): + try: + import dbus + except ImportError: + pl.debug('Not using DBUS+UPower as dbus is not available') + else: + try: + bus = dbus.SystemBus() + except Exception as e: + pl.exception('Failed to connect to system bus: {0}', str(e)) + else: + interface = 'org.freedesktop.UPower' + try: + up = bus.get_object(interface, '/org/freedesktop/UPower') + except dbus.exceptions.DBusException as e: + if getattr(e, '_dbus_error_name', '').endswidth('ServiceUnknown'): + pl.debug('Not using DBUS+UPower as UPower is not available via dbus') + else: + pl.exception('Failed to get UPower service with dbus: {0}', str(e)) + else: + devinterface = 'org.freedesktop.DBus.Properties' + devtype_name = interface + '.Device' + for devpath in up.EnumerateDevices(dbus_interface=interface): + dev = bus.get_object(interface, devpath) + devget = lambda what: dev.Get( + devtype_name, + what, + dbus_interface=devinterface + ) + if int(devget('Type')) != 2: + pl.debug('Not using DBUS+UPower with {0}: invalid type', devpath) + continue + if not bool(devget('IsPresent')): + pl.debug('Not using DBUS+UPower with {0}: not present', devpath) + continue + if not bool(devget('PowerSupply')): + pl.debug('Not using DBUS+UPower with {0}: not a power supply', devpath) + continue + pl.debug('Using DBUS+UPower with {0}', devpath) + return lambda pl: float( + dbus.Interface(dev, dbus_interface=devinterface).Get( + devtype_name, + 'Percentage' + ) + ) + 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' + for linux_bat in os.listdir('/sys/class/power_supply'): + cap_path = linux_bat_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_capacity(pl): + with open(cap_path, 'r') as f: + return int(float(f.readline().split()[0])) + + return _get_capacity + pl.debug('Not using /sys/class/power_supply as no batteries were found') + else: + pl.debug('Not using /sys/class/power_supply: no directory') + + try: + from shutil import which # Python-3.3 and later + except ImportError: + pl.info('Using dumb “which” which only checks for file in /usr/bin') + which = lambda f: (lambda fp: os.path.exists(fp) and fp)(os.path.join('/usr/bin', f)) + + if which('pmset'): + pl.debug('Using pmset') + + def _get_capacity(pl): + import re + battery_summary = run_cmd(pl, ['pmset', '-g', 'batt']) + battery_percent = re.search(r'(\d+)%', battery_summary).group(1) + return int(battery_percent) + + return _get_capacity + else: + pl.debug('Not using pmset: executable not found') + + if sys.platform.startswith('win'): + # From http://stackoverflow.com/a/21083571/273566, reworked + try: + from win32com.client import GetObject + except ImportError: + pl.debug('Not using win32com.client as it is not available') + else: + try: + wmi = GetObject('winmgmts:') + except Exception as e: + pl.exception('Failed to run GetObject from win32com.client: {0}', str(e)) + else: + for battery in wmi.InstancesOf('Win32_Battery'): + pl.debug('Using win32com.client with Win32_Battery') + + def _get_capacity(pl): + # http://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx + return battery.EstimatedChargeRemaining + + return _get_capacity + pl.debug('Not using win32com.client as no batteries were found') + + from ctypes import Structure, c_byte, c_ulong, windll, byref + + class PowerClass(Structure): + _fields_ = [ + ('ACLineStatus', c_byte), + ('BatteryFlag', c_byte), + ('BatteryLifePercent', c_byte), + ('Reserved1', c_byte), + ('BatteryLifeTime', c_ulong), + ('BatteryFullLifeTime', c_ulong) + ] + + def _get_capacity(pl): + powerclass = PowerClass() + result = windll.kernel32.GetSystemPowerStatus(byref(powerclass)) + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa372693(v=vs.85).aspx + if result: + return None + return powerclass.BatteryLifePercent + + if _get_capacity() is None: + pl.debug('Not using GetSystemPowerStatus because it failed') + else: + pl.debug('Using GetSystemPowerStatus') + + return _get_capacity + + raise NotImplementedError + + +def _get_capacity(pl): + global _get_capacity + + def _failing_get_capacity(pl): + raise NotImplementedError + + try: + _get_capacity = _get_battery(pl) + except NotImplementedError: + _get_capacity = _failing_get_capacity + except Exception as e: + pl.exception('Exception while obtaining battery capacity getter: {0}', str(e)) + _get_capacity = _failing_get_capacity + return _get_capacity(pl) + + +def battery(pl, format='{capacity:3.0%}', steps=5, gamify=False, full_heart='O', empty_heart='O'): + '''Return battery charge status. + + :param str format: + Percent format in case gamify is False. + :param int steps: + Number of discrete steps to show between 0% and 100% capacity if gamify + is True. + :param bool gamify: + Measure in hearts (♥) instead of percentages. For full hearts + ``battery_full`` highlighting group is preferred, for empty hearts there + is ``battery_empty``. + :param str full_heart: + Heart displayed for “full” part of battery. + :param str empty_heart: + Heart displayed for “used” part of battery. It is also displayed using + another gradient level and highlighting group, so it is OK for it to be + the same as full_heart as long as necessary highlighting groups are + defined. + + ``battery_gradient`` and ``battery`` groups are used in any case, first is + preferred. + + Highlight groups used: ``battery_full`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_empty`` or ``battery_gradient`` (gradient) or ``battery``. + ''' + try: + capacity = _get_capacity(pl) + except NotImplementedError: + pl.info('Unable to get battery capacity.') + return None + ret = [] + if gamify: + denom = int(steps) + numer = int(denom * capacity / 100) + ret.append({ + 'contents': full_heart * numer, + 'draw_inner_divider': False, + 'highlight_group': ['battery_full', 'battery_gradient', 'battery'], + # Using zero as “nothing to worry about”: it is least alert color. + 'gradient_level': 0, + }) + ret.append({ + 'contents': empty_heart * (denom - numer), + 'draw_inner_divider': False, + 'highlight_group': ['battery_empty', 'battery_gradient', 'battery'], + # Using a hundred as it is most alert color. + 'gradient_level': 100, + }) + else: + ret.append({ + 'contents': format.format(capacity=(capacity / 100.0)), + 'highlight_group': ['battery_gradient', 'battery'], + # Gradients are “least alert – most alert” by default, capacity has + # the opposite semantics. + 'gradient_level': 100 - capacity, + }) + return ret diff --git a/powerline/segments/i3wm.py b/powerline/segments/i3wm.py new file mode 100644 index 00000000..1a290c78 --- /dev/null +++ b/powerline/segments/i3wm.py @@ -0,0 +1,27 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import i3 + + +def calcgrp(w): + group = [] + if w['focused']: + group.append('w_focused') + if w['urgent']: + group.append('w_urgent') + if w['visible']: + group.append('w_visible') + group.append('workspace') + return group + + +def workspaces(pl): + '''Return workspace list + + Highlight groups used: ``workspace``, ``w_visible``, ``w_focused``, ``w_urgent`` + ''' + return [{ + 'contents': w['name'], + 'highlight_group': calcgrp(w) + } for w in i3.get_workspaces()] diff --git a/powerline/segments/ipython.py b/powerline/segments/ipython.py new file mode 100644 index 00000000..622e0a50 --- /dev/null +++ b/powerline/segments/ipython.py @@ -0,0 +1,9 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info + + +@requires_segment_info +def prompt_count(pl, segment_info): + return str(segment_info['ipython'].prompt_count) diff --git a/powerline/segments/shell.py b/powerline/segments/shell.py new file mode 100644 index 00000000..f1ac2845 --- /dev/null +++ b/powerline/segments/shell.py @@ -0,0 +1,170 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info +from powerline.segments import with_docstring +from powerline.segments.common import CwdSegment + + +@requires_segment_info +def jobnum(pl, segment_info, show_zero=False): + '''Return the number of jobs. + + :param bool show_zero: + If False (default) shows nothing if there are no jobs. Otherwise shows + zero for no jobs. + ''' + jobnum = segment_info['args'].jobnum + if jobnum is None or (not show_zero and jobnum == 0): + return None + else: + return str(jobnum) + + +@requires_segment_info +def last_status(pl, segment_info): + '''Return last exit code. + + Highlight groups used: ``exit_fail`` + ''' + if not segment_info['args'].last_exit_code: + return None + return [{'contents': str(segment_info['args'].last_exit_code), 'highlight_group': ['exit_fail']}] + + +@requires_segment_info +def last_pipe_status(pl, segment_info): + '''Return last pipe status. + + Highlight groups used: ``exit_fail``, ``exit_success`` + ''' + last_pipe_status = segment_info['args'].last_pipe_status + if any(last_pipe_status): + return [ + { + 'contents': str(status), + 'highlight_group': ['exit_fail' if status else 'exit_success'], + 'draw_inner_divider': True + } + for status in last_pipe_status + ] + else: + return None + + +@requires_segment_info +def mode(pl, segment_info, override={'vicmd': 'COMMND', 'viins': 'INSERT'}, default=None): + '''Return the current mode. + + :param dict override: + dict for overriding mode strings. + :param str default: + If current mode is equal to this string then this segment will not get + displayed. If not specified the value is taken from + ``$POWERLINE_DEFAULT_MODE`` variable. This variable is set by zsh + bindings for any mode that does not start from ``vi``. + ''' + mode = segment_info['mode'] + if not mode: + pl.debug('No or empty _POWERLINE_MODE variable') + return None + default = default or segment_info['environ'].get('_POWERLINE_DEFAULT_MODE') + if mode == default: + return None + try: + return override[mode] + except KeyError: + # Note: with zsh line editor you can emulate as much modes as you wish. + # Thus having unknown mode is not an error: maybe just some developer + # added support for his own zle widgets. As there is no built-in mode() + # function like in VimL and _POWERLINE_MODE is likely be defined by our + # code or by somebody knowing what he is doing there is absolutely no + # need in keeping translations dictionary. + return mode.upper() + + +@requires_segment_info +def continuation(pl, segment_info, omit_cmdsubst=True, right_align=False, renames={}): + '''Display parser state. + + :param bool omit_cmdsubst: + Do not display cmdsubst parser state if it is the last one. + :param bool right_align: + Align to the right. + :param dict renames: + Rename states: ``{old_name : new_name}``. If ``new_name`` is ``None`` + then given state is not displayed. + + Highlight groups used: ``continuation``, ``continuation:current``. + ''' + if not segment_info.get('parser_state'): + return [{ + 'contents': '', + 'width': 'auto', + 'highlight_group': ['continuation:current', 'continuation'], + }] + ret = [] + + for state in segment_info['parser_state'].split(): + state = renames.get(state, state) + if state: + ret.append({ + 'contents': state, + 'highlight_group': ['continuation'], + 'draw_inner_divider': True, + }) + + if omit_cmdsubst and ret[-1]['contents'] == 'cmdsubst': + ret.pop(-1) + + if not ret: + ret.append({ + 'contents': '' + }) + + if right_align: + ret[0].update(width='auto', align='r') + ret[-1]['highlight_group'] = ['continuation:current', 'continuation'] + else: + ret[-1].update(width='auto', align='l', highlight_group=['continuation:current', 'continuation']) + + return ret + + +@requires_segment_info +class ShellCwdSegment(CwdSegment): + def get_shortened_path(self, pl, segment_info, use_shortened_path=True, **kwargs): + if use_shortened_path: + try: + return segment_info['shortened_path'] + except KeyError: + pass + return super(ShellCwdSegment, self).get_shortened_path(pl, segment_info, **kwargs) + + +cwd = with_docstring(ShellCwdSegment(), +'''Return the current working directory. + +Returns a segment list to create a breadcrumb-like effect. + +:param int dir_shorten_len: + shorten parent directory names to this length (e.g. + :file:`/long/path/to/powerline` → :file:`/l/p/t/powerline`) +:param int dir_limit_depth: + limit directory depth to this number (e.g. + :file:`/long/path/to/powerline` → :file:`⋯/to/powerline`) +:param bool use_path_separator: + Use path separator in place of soft divider. +:param bool use_shortened_path: + Use path from shortened_path ``--renderer_arg`` argument. If this argument + is present ``shorten_home`` argument is ignored. +:param bool shorten_home: + Shorten home directory to ``~``. +:param str ellipsis: + Specifies what to use in place of omitted directories. Use None to not + show this subsegment at all. + +Divider highlight group used: ``cwd:divider``. + +Highlight groups used: ``cwd:current_folder`` or ``cwd``. It is recommended to define all highlight groups. +''') diff --git a/powerline/segments/tmux.py b/powerline/segments/tmux.py new file mode 100644 index 00000000..b26824dc --- /dev/null +++ b/powerline/segments/tmux.py @@ -0,0 +1,22 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.bindings.tmux import get_tmux_output + + +def attached_clients(pl, minimum=1): + '''Return the number of tmux clients attached to the currently active session + + :param int minimum: + The minimum number of attached clients that must be present for this + segment to be visible. + ''' + session_output = get_tmux_output('list-panes', '-F', '#{session_name}') + if not session_output: + return None + session_name = session_output.rstrip().split('\n')[0] + + attached_clients_output = get_tmux_output('list-clients', '-t', session_name) + attached_count = len(attached_clients_output.rstrip().split('\n')) + + return None if attached_count < minimum else str(attached_count) diff --git a/powerline/segments/vim/__init__.py b/powerline/segments/vim/__init__.py new file mode 100644 index 00000000..9f59b7c4 --- /dev/null +++ b/powerline/segments/vim/__init__.py @@ -0,0 +1,623 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re + +from collections import defaultdict + +try: + import vim +except ImportError: + vim = {} + +from powerline.bindings.vim import (vim_get_func, getbufvar, vim_getbufoption, + buffer_name, vim_getwinvar, + register_buffer_cache, current_tabpage, + list_tabpage_buffers_segment_info) +from powerline.theme import requires_segment_info, requires_filesystem_watcher +from powerline.lib import add_divider_highlight_group +from powerline.lib.vcs import guess, tree_status +from powerline.lib.humanize_bytes import humanize_bytes +from powerline.lib import wraps_saveargs as wraps + +try: + from __builtin__ import xrange as range +except ImportError: + pass + + +vim_funcs = { + 'virtcol': vim_get_func('virtcol', rettype=int), + 'getpos': vim_get_func('getpos'), + 'fnamemodify': vim_get_func('fnamemodify'), + 'expand': vim_get_func('expand'), + 'bufnr': vim_get_func('bufnr', rettype=int), + 'line2byte': vim_get_func('line2byte', rettype=int), + 'line': vim_get_func('line', rettype=int), +} + +vim_modes = { + 'n': 'NORMAL', + 'no': 'N-OPER', + 'v': 'VISUAL', + 'V': 'V-LINE', + '^V': 'V-BLCK', + 's': 'SELECT', + 'S': 'S-LINE', + '^S': 'S-BLCK', + 'i': 'INSERT', + 'R': 'REPLACE', + 'Rv': 'V-RPLCE', + 'c': 'COMMND', + 'cv': 'VIM EX', + 'ce': 'EX', + 'r': 'PROMPT', + 'rm': 'MORE', + 'r?': 'CONFIRM', + '!': 'SHELL', +} + + +eventfuncs = defaultdict(lambda: []) +bufeventfuncs = defaultdict(lambda: []) +defined_events = set() + + +# TODO Remove cache when needed +def window_cached(func): + cache = {} + + @requires_segment_info + @wraps(func) + def ret(segment_info, **kwargs): + window_id = segment_info['window_id'] + if segment_info['mode'] == 'nc': + return cache.get(window_id) + else: + if getattr(func, 'powerline_requires_segment_info', False): + r = func(segment_info=segment_info, **kwargs) + else: + r = func(**kwargs) + cache[window_id] = r + return r + + return ret + + +@requires_segment_info +def mode(pl, segment_info, override=None): + '''Return the current vim mode. + + :param dict override: + dict for overriding default mode strings, e.g. ``{ 'n': 'NORM' }`` + ''' + mode = segment_info['mode'] + if mode == 'nc': + return None + if not override: + return vim_modes[mode] + try: + return override[mode] + except KeyError: + return vim_modes[mode] + + +@window_cached +@requires_segment_info +def visual_range(pl, segment_info, CTRL_V_text='{rows} x {vcols}', v_text_oneline='C:{vcols}', v_text_multiline='L:{rows}', V_text='L:{rows}'): + '''Return the current visual selection range. + + :param str CTRL_V_text: + Text to display when in block visual or select mode. + :param str v_text_oneline: + Text to display when in charaterwise visual or select mode, assuming + selection occupies only one line. + :param str v_text_multiline: + Text to display when in charaterwise visual or select mode, assuming + selection occupies more then one line. + :param str V_text: + Text to display when in linewise visual or select mode. + + All texts are format strings which are passed the following parameters: + + ========= ============================================================= + Parameter Description + ========= ============================================================= + sline Line number of the first line of the selection + eline Line number of the last line of the selection + scol Column number of the first character of the selection + ecol Column number of the last character of the selection + svcol Virtual column number of the first character of the selection + secol Virtual column number of the last character of the selection + rows Number of lines in the selection + cols Number of columns in the selection + vcols Number of virtual columns in the selection + ========= ============================================================= + ''' + sline, scol, soff = [int(v) for v in vim_funcs['getpos']("v")[1:]] + eline, ecol, eoff = [int(v) for v in vim_funcs['getpos'](".")[1:]] + svcol = vim_funcs['virtcol']([sline, scol, soff]) + evcol = vim_funcs['virtcol']([eline, ecol, eoff]) + rows = abs(eline - sline) + 1 + cols = abs(ecol - scol) + 1 + vcols = abs(evcol - svcol) + 1 + return { + '^': CTRL_V_text, + 's': v_text_oneline if rows == 1 else v_text_multiline, + 'S': V_text, + 'v': v_text_oneline if rows == 1 else v_text_multiline, + 'V': V_text, + }.get(segment_info['mode'][0], '').format( + sline=sline, eline=eline, + scol=scol, ecol=ecol, + svcol=svcol, evcol=evcol, + rows=rows, cols=cols, vcols=vcols, + ) + + +@requires_segment_info +def modified_indicator(pl, segment_info, text='+'): + '''Return a file modified indicator. + + :param string text: + text to display if the current buffer is modified + ''' + return text if int(vim_getbufoption(segment_info, 'modified')) else None + + +@requires_segment_info +def tab_modified_indicator(pl, segment_info, text='+'): + '''Return a file modified indicator for tabpages. + + :param string text: + text to display if any buffer in the current tab is modified + + Highlight groups used: ``tab_modified_indicator`` or ``modified_indicator``. + ''' + for buf_segment_info in list_tabpage_buffers_segment_info(segment_info): + if int(vim_getbufoption(buf_segment_info, 'modified')): + return [{ + 'contents': text, + 'highlight_group': ['tab_modified_indicator', 'modified_indicator'], + }] + return None + + +@requires_segment_info +def paste_indicator(pl, segment_info, text='PASTE'): + '''Return a paste mode indicator. + + :param string text: + text to display if paste mode is enabled + ''' + return text if int(vim.eval('&paste')) else None + + +@requires_segment_info +def readonly_indicator(pl, segment_info, text='RO'): + '''Return a read-only indicator. + + :param string text: + text to display if the current buffer is read-only + ''' + return text if int(vim_getbufoption(segment_info, 'readonly')) else None + + +SCHEME_RE = re.compile(b'^\\w[\\w\\d+\\-.]*(?=:)') + + +@requires_segment_info +def file_scheme(pl, segment_info): + '''Return the protocol part of the file. + + Protocol is the part of the full filename just before the colon which + starts with a latin letter and contains only latin letters, digits, plus, + period or hyphen (refer to `RFC3986 + `_ for the description of + URI scheme). If there is no such a thing ``None`` is returned, effectively + removing segment. + + .. note:: + Segment will not check whether there is ``//`` just after the + colon or if there is at least one slash after the scheme. Reason: it is + not always present. E.g. when opening file inside a zip archive file + name will look like :file:`zipfile:/path/to/archive.zip::file.txt`. + ``file_scheme`` segment will catch ``zipfile`` part here. + ''' + name = buffer_name(segment_info['buffer']) + if not name: + return None + match = SCHEME_RE.match(name) + if match: + return match.group(0).decode('ascii') + + +@requires_segment_info +def file_directory(pl, segment_info, remove_scheme=True, shorten_user=True, shorten_cwd=True, shorten_home=False): + '''Return file directory (head component of the file path). + + :param bool remove_scheme: + Remove scheme part from the segment name, if present. See documentation + of file_scheme segment for the description of what scheme is. Also + removes the colon. + + :param bool shorten_user: + Shorten ``$HOME`` directory to :file:`~/`. Does not work for files with + scheme. + + :param bool shorten_cwd: + Shorten current directory to :file:`./`. Does not work for files with + scheme present. + + :param bool shorten_home: + Shorten all directories in :file:`/home/` to :file:`~user/` instead of + :file:`/home/user/`. Does not work for files with scheme present. + ''' + name = buffer_name(segment_info['buffer']) + if not name: + return None + match = SCHEME_RE.match(name) + if match: + if remove_scheme: + name = name[len(match.group(0)) + 1:] # Remove scheme and colon + file_directory = vim_funcs['fnamemodify'](name, ':h') + else: + file_directory = vim_funcs['fnamemodify']( + name, + (':~' if shorten_user else '') + (':.' if shorten_cwd else '') + ':h' + ) + if not file_directory: + return None + if shorten_home and file_directory.startswith('/home/'): + file_directory = b'~' + file_directory[6:] + file_directory = file_directory.decode('utf-8', 'powerline_vim_strtrans_error') + return file_directory + os.sep + + +@requires_segment_info +def file_name(pl, segment_info, display_no_file=False, no_file_text='[No file]'): + '''Return file name (tail component of the file path). + + :param bool display_no_file: + display a string if the buffer is missing a file name + :param str no_file_text: + the string to display if the buffer is missing a file name + + Highlight groups used: ``file_name_no_file`` or ``file_name``, ``file_name``. + ''' + name = buffer_name(segment_info['buffer']) + if not name: + if display_no_file: + return [{ + 'contents': no_file_text, + 'highlight_group': ['file_name_no_file', 'file_name'], + }] + else: + return None + return os.path.basename(name).decode('utf-8', 'powerline_vim_strtrans_error') + + +@window_cached +def file_size(pl, suffix='B', si_prefix=False): + '''Return file size in &encoding. + + :param str suffix: + string appended to the file size + :param bool si_prefix: + use SI prefix, e.g. MB instead of MiB + :return: file size or None if the file isn't saved or if the size is too big to fit in a number + ''' + # Note: returns file size in &encoding, not in &fileencoding. But returned + # size is updated immediately; and it is valid for any buffer + file_size = vim_funcs['line2byte'](len(vim.current.buffer) + 1) - 1 + if file_size < 0: + file_size = 0 + return humanize_bytes(file_size, suffix, si_prefix) + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_format(pl, segment_info): + '''Return file format (i.e. line ending type). + + :return: file format or None if unknown or missing file format + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'fileformat') or None + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_encoding(pl, segment_info): + '''Return file encoding/character set. + + :return: file encoding/character set or None if unknown or missing file encoding + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'fileencoding') or None + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_type(pl, segment_info): + '''Return file type. + + :return: file type or None if unknown file type + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'filetype') or None + + +@requires_segment_info +def window_title(pl, segment_info): + '''Return the window title. + + This currently looks at the ``quickfix_title`` window variable, + which is used by Syntastic and Vim itself. + + It is used in the quickfix theme.''' + try: + return vim_getwinvar(segment_info, 'quickfix_title') + except KeyError: + return None + + +@requires_segment_info +def line_percent(pl, segment_info, gradient=False): + '''Return the cursor position in the file as a percentage. + + :param bool gradient: + highlight the percentage with a color gradient (by default a green to red gradient) + + Highlight groups used: ``line_percent_gradient`` (gradient), ``line_percent``. + ''' + line_current = segment_info['window'].cursor[0] + line_last = len(segment_info['buffer']) + percentage = line_current * 100.0 / line_last + if not gradient: + return str(int(round(percentage))) + return [{ + 'contents': str(int(round(percentage))), + 'highlight_group': ['line_percent_gradient', 'line_percent'], + 'gradient_level': percentage, + }] + + +@window_cached +def position(pl, position_strings={'top': 'Top', 'bottom': 'Bot', 'all': 'All'}, gradient=False): + '''Return the position of the current view in the file as a percentage. + + :param dict position_strings: + dict for translation of the position strings, e.g. ``{"top":"Oben", "bottom":"Unten", "all":"Alles"}`` + + :param bool gradient: + highlight the percentage with a color gradient (by default a green to red gradient) + + Highlight groups used: ``position_gradient`` (gradient), ``position``. + ''' + line_last = len(vim.current.buffer) + + winline_first = vim_funcs['line']('w0') + winline_last = vim_funcs['line']('w$') + if winline_first == 1 and winline_last == line_last: + percentage = 0.0 + content = position_strings['all'] + elif winline_first == 1: + percentage = 0.0 + content = position_strings['top'] + elif winline_last == line_last: + percentage = 100.0 + content = position_strings['bottom'] + else: + percentage = winline_first * 100.0 / (line_last - winline_last + winline_first) + content = str(int(round(percentage))) + '%' + + if not gradient: + return content + return [{ + 'contents': content, + 'highlight_group': ['position_gradient', 'position'], + 'gradient_level': percentage, + }] + + +@requires_segment_info +def line_current(pl, segment_info): + '''Return the current cursor line.''' + return str(segment_info['window'].cursor[0]) + + +@requires_segment_info +def line_count(pl, segment_info): + '''Return the line count of the current buffer.''' + return str(len(segment_info['buffer'])) + + +@requires_segment_info +def col_current(pl, segment_info): + '''Return the current cursor column. + ''' + return str(segment_info['window'].cursor[1] + 1) + + +@window_cached +def virtcol_current(pl, gradient=True): + '''Return current visual column with concealed characters ingored + + :param bool gradient: + Determines whether it should show textwidth-based gradient (gradient level is ``virtcol * 100 / textwidth``). + + Highlight groups used: ``virtcol_current_gradient`` (gradient), ``virtcol_current`` or ``col_current``. + ''' + col = vim_funcs['virtcol']('.') + r = [{'contents': str(col), 'highlight_group': ['virtcol_current', 'col_current']}] + if gradient: + textwidth = int(getbufvar('%', '&textwidth')) + r[-1]['gradient_level'] = min(col * 100 / textwidth, 100) if textwidth else 0 + r[-1]['highlight_group'].insert(0, 'virtcol_current_gradient') + return r + + +def modified_buffers(pl, text='+ ', join_str=','): + '''Return a comma-separated list of modified buffers. + + :param str text: + text to display before the modified buffer list + :param str join_str: + string to use for joining the modified buffer list + ''' + buffer_len = vim_funcs['bufnr']('$') + buffer_mod = [str(bufnr) for bufnr in range(1, buffer_len + 1) if int(getbufvar(bufnr, '&modified') or 0)] + if buffer_mod: + return text + join_str.join(buffer_mod) + return None + + +@requires_filesystem_watcher +@requires_segment_info +def branch(pl, segment_info, create_watcher, status_colors=False): + '''Return the current working branch. + + :param bool status_colors: + determines whether repository status will be used to determine highlighting. Default: False. + + Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``. + + Divider highlight group used: ``branch:divider``. + ''' + name = segment_info['buffer'].name + skip = not (name and (not vim_getbufoption(segment_info, 'buftype'))) + if not skip: + repo = guess(path=name, create_watcher=create_watcher) + if repo is not None: + branch = repo.branch() + scol = ['branch'] + if status_colors: + status = tree_status(repo, pl) + scol.insert(0, 'branch_dirty' if status and status.strip() else 'branch_clean') + return [{ + 'contents': branch, + 'highlight_group': scol, + 'divider_highlight_group': 'branch:divider', + }] + + +@requires_filesystem_watcher +@requires_segment_info +def file_vcs_status(pl, segment_info, create_watcher): + '''Return the VCS status for this buffer. + + Highlight groups used: ``file_vcs_status``. + ''' + name = segment_info['buffer'].name + skip = not (name and (not vim_getbufoption(segment_info, 'buftype'))) + if not skip: + repo = guess(path=name, create_watcher=create_watcher) + if repo is not None: + status = repo.status(os.path.relpath(name, repo.directory)) + if not status: + return None + status = status.strip() + ret = [] + for status in status: + ret.append({ + 'contents': status, + 'highlight_group': ['file_vcs_status_' + status, 'file_vcs_status'], + }) + return ret + + +trailing_whitespace_cache = None + + +@requires_segment_info +def trailing_whitespace(pl, segment_info): + '''Return the line number for trailing whitespaces + + It is advised not to use this segment in insert mode: in Insert mode it will + iterate over all lines in buffer each time you happen to type a character + which may cause lags. It will also show you whitespace warning each time you + happen to type space. + + Highlight groups used: ``trailing_whitespace`` or ``warning``. + ''' + global trailing_whitespace_cache + if trailing_whitespace_cache is None: + trailing_whitespace_cache = register_buffer_cache(defaultdict(lambda: (0, None))) + bufnr = segment_info['bufnr'] + changedtick = getbufvar(bufnr, 'changedtick') + if trailing_whitespace_cache[bufnr][0] == changedtick: + return trailing_whitespace_cache[bufnr][1] + else: + buf = segment_info['buffer'] + bws = b' \t' + sws = str(bws) + for i in range(len(buf)): + try: + line = buf[i] + except UnicodeDecodeError: # May happen in Python 3 + if hasattr(vim, 'bindeval'): + line = vim.bindeval('getbufline({0}, {1})'.format( + bufnr, i + 1)) + has_trailing_ws = (line[-1] in bws) + else: + line = vim.eval('strtrans(getbufline({0}, {1}))'.format( + bufnr, i + 1)) + has_trailing_ws = (line[-1] in bws) + else: + has_trailing_ws = (line and line[-1] in sws) + if has_trailing_ws: + break + if has_trailing_ws: + ret = [{ + 'contents': str(i + 1), + 'highlight_group': ['trailing_whitespace', 'warning'], + }] + else: + ret = None + trailing_whitespace_cache[bufnr] = (changedtick, ret) + return ret + + +@requires_segment_info +def tabnr(pl, segment_info, show_current=True): + '''Show tabpage number + + :param bool show_current: + If False do not show current tabpage number. This is default because + tabnr is by default only present in tabline. + ''' + try: + tabnr = segment_info['tabnr'] + except KeyError: + return None + if show_current or tabnr != current_tabpage().number: + return str(tabnr) + + +@requires_segment_info +def bufnr(pl, segment_info, show_current=True): + '''Show buffer number + + :param bool show_current: + If False do not show current window number. + ''' + bufnr = segment_info['bufnr'] + if show_current or bufnr != vim.current.buffer.number: + return str(bufnr) + + +@requires_segment_info +def winnr(pl, segment_info, show_current=True): + '''Show window number + + :param bool show_current: + If False do not show current window number. + ''' + winnr = segment_info['winnr'] + if show_current or winnr != vim.current.window.number: + return str(winnr) diff --git a/powerline/segments/vim/plugin/__init__.py b/powerline/segments/vim/plugin/__init__.py new file mode 100644 index 00000000..b2b9f102 --- /dev/null +++ b/powerline/segments/vim/plugin/__init__.py @@ -0,0 +1,6 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) +from pkgutil import extend_path + + +__path__ = extend_path(__path__, __name__) diff --git a/powerline/segments/vim/plugin/ctrlp.py b/powerline/segments/vim/plugin/ctrlp.py new file mode 100644 index 00000000..80cabb76 --- /dev/null +++ b/powerline/segments/vim/plugin/ctrlp.py @@ -0,0 +1,116 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import getbufvar +from powerline.segments.vim import window_cached + + +@window_cached +def ctrlp(pl, side): + ''' + + Highlight groups used: ``ctrlp.regex`` or ``background``, ``ctrlp.prev`` or ``background``, ``ctrlp.item`` or ``file_name``, ``ctrlp.next`` or ``background``, ``ctrlp.marked`` or ``background``, ``ctrlp.focus`` or ``background``, ``ctrlp.byfname`` or ``background``, ``ctrlp.progress`` or ``file_name``, ``ctrlp.progress`` or ``file_name``. + ''' + ctrlp_type = getbufvar('%', 'powerline_ctrlp_type') + ctrlp_args = getbufvar('%', 'powerline_ctrlp_args') + + return globals()['ctrlp_stl_{0}_{1}'.format(side, ctrlp_type)](pl, *ctrlp_args) + + +def ctrlp_stl_left_main(pl, focus, byfname, regex, prev, item, next, marked): + ''' + + Highlight groups used: ``ctrlp.regex`` or ``background``, ``ctrlp.prev`` or ``background``, ``ctrlp.item`` or ``file_name``, ``ctrlp.next`` or ``background``, ``ctrlp.marked`` or ``background``. + ''' + marked = marked[2:-1] + segments = [] + + if int(regex): + segments.append({ + 'contents': 'regex', + 'highlight_group': ['ctrlp.regex', 'background'], + }) + + segments += [ + { + 'contents': prev + ' ', + 'highlight_group': ['ctrlp.prev', 'background'], + 'draw_inner_divider': True, + 'priority': 40, + }, + { + 'contents': item, + 'highlight_group': ['ctrlp.item', 'file_name'], + 'draw_inner_divider': True, + 'width': 10, + 'align': 'c', + }, + { + 'contents': ' ' + next, + 'highlight_group': ['ctrlp.next', 'background'], + 'draw_inner_divider': True, + 'priority': 40, + }, + ] + + if marked != '-': + segments.append({ + 'contents': marked, + 'highlight_group': ['ctrlp.marked', 'background'], + 'draw_inner_divider': True, + }) + + return segments + + +def ctrlp_stl_right_main(pl, focus, byfname, regex, prev, item, next, marked): + ''' + + Highlight groups used: ``ctrlp.focus`` or ``background``, ``ctrlp.byfname`` or ``background``. + ''' + segments = [ + { + 'contents': focus, + 'highlight_group': ['ctrlp.focus', 'background'], + 'draw_inner_divider': True, + 'priority': 50, + }, + { + 'contents': byfname, + 'highlight_group': ['ctrlp.byfname', 'background'], + 'priority': 50, + }, + ] + + return segments + + +def ctrlp_stl_left_prog(pl, progress): + ''' + + Highlight groups used: ``ctrlp.progress`` or ``file_name``. + ''' + return [ + { + 'contents': 'Loading...', + 'highlight_group': ['ctrlp.progress', 'file_name'], + }, + ] + + +def ctrlp_stl_right_prog(pl, progress): + ''' + + Highlight groups used: ``ctrlp.progress`` or ``file_name``. + ''' + return [ + { + 'contents': progress, + 'highlight_group': ['ctrlp.progress', 'file_name'], + }, + ] diff --git a/powerline/segments/vim/plugin/nerdtree.py b/powerline/segments/vim/plugin/nerdtree.py new file mode 100644 index 00000000..79aba991 --- /dev/null +++ b/powerline/segments/vim/plugin/nerdtree.py @@ -0,0 +1,25 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import bufvar_exists +from powerline.segments.vim import window_cached + + +@window_cached +def nerdtree(pl): + '''Return directory that is shown by the current buffer. + + Highlight groups used: ``nerdtree.path`` or ``file_name``. + ''' + if not bufvar_exists(None, 'NERDTreeRoot'): + return None + path_str = vim.eval('getbufvar("%", "NERDTreeRoot").path.str()') + return [{ + 'contents': path_str, + 'highlight_group': ['nerdtree.path', 'file_name'], + }] diff --git a/powerline/segments/vim/plugin/syntastic.py b/powerline/segments/vim/plugin/syntastic.py new file mode 100644 index 00000000..61324d45 --- /dev/null +++ b/powerline/segments/vim/plugin/syntastic.py @@ -0,0 +1,42 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.segments.vim import window_cached + + +@window_cached +def syntastic(pl, err_format='ERR:  {first_line} ({num}) ', warn_format='WARN:  {first_line} ({num}) '): + '''Show whether syntastic has found any errors or warnings + + :param str err_format: + Format string for errors. + + :param str warn_format: + Format string for warnings. + + Highlight groups used: ``syntastic.warning`` or ``warning``, ``syntastic.error`` or ``error``. + ''' + if not int(vim.eval('exists("g:SyntasticLoclist")')): + return + has_errors = int(vim.eval('g:SyntasticLoclist.current().hasErrorsOrWarningsToDisplay()')) + if not has_errors: + return + errors = vim.eval('g:SyntasticLoclist.current().errors()') + warnings = vim.eval('g:SyntasticLoclist.current().warnings()') + segments = [] + if errors: + segments.append({ + 'contents': err_format.format(first_line=errors[0]['lnum'], num=len(errors)), + 'highlight_group': ['syntastic.error', 'error'], + }) + if warnings: + segments.append({ + 'contents': warn_format.format(first_line=warnings[0]['lnum'], num=len(warnings)), + 'highlight_group': ['syntastic.warning', 'warning'], + }) + return segments diff --git a/powerline/segments/vim/plugin/tagbar.py b/powerline/segments/vim/plugin/tagbar.py new file mode 100644 index 00000000..7421208e --- /dev/null +++ b/powerline/segments/vim/plugin/tagbar.py @@ -0,0 +1,16 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.segments.vim import window_cached + + +@window_cached +def current_tag(pl): + if not int(vim.eval('exists(":Tagbar")')): + return + return vim.eval('tagbar#currenttag("%s", "")') diff --git a/powerline/selectors/__init__.py b/powerline/selectors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline/selectors/vim.py b/powerline/selectors/vim.py new file mode 100644 index 00000000..d111de95 --- /dev/null +++ b/powerline/selectors/vim.py @@ -0,0 +1,10 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.bindings.vim import list_tabpages + + +def single_tab(pl, segment_info, mode): + '''Returns True if Vim has only one tab opened + ''' + return len(list_tabpages()) == 1 diff --git a/powerline/shell.py b/powerline/shell.py new file mode 100644 index 00000000..047f21af --- /dev/null +++ b/powerline/shell.py @@ -0,0 +1,106 @@ +# vim:fileencoding=utf-8:noet +# WARNING: using unicode_literals causes errors in argparse +from __future__ import (division, absolute_import, print_function) + +from powerline import Powerline +from powerline.lib import mergedicts, parsedotval + + +def mergeargs(argvalue): + if not argvalue: + return None + r = {} + for subval in argvalue: + mergedicts(r, dict([subval])) + return r + + +class ShellPowerline(Powerline): + def init(self, args, **kwargs): + self.args = args + self.theme_option = args.theme_option + super(ShellPowerline, self).init(args.ext[0], args.renderer_module, **kwargs) + + def load_main_config(self): + r = super(ShellPowerline, self).load_main_config() + if self.args.config: + mergedicts(r, self.args.config) + return r + + def load_theme_config(self, name): + r = super(ShellPowerline, self).load_theme_config(name) + if self.theme_option and name in self.theme_option: + mergedicts(r, self.theme_option[name]) + return r + + def get_config_paths(self): + return self.args.config_path or super(ShellPowerline, self).get_config_paths() + + 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() + )) + + def do_setup(self, obj): + obj.powerline = self + + +def get_argparser(parser=None, *args, **kwargs): + if not parser: + import argparse + parser = argparse.ArgumentParser + p = parser(*args, **kwargs) + p.add_argument('ext', nargs=1, help='Extension: application for which powerline command is launched (usually `shell\' or `tmux\')') + p.add_argument('side', nargs='?', choices=('left', 'right', 'above', 'aboveleft'), help='Side: `left\' and `right\' represent left and right side respectively, `above\' emits lines that are supposed to be printed just above the prompt and `aboveleft\' is like concatenating `above\' with `left\' with the exception that only one Python instance is used in this case.') + p.add_argument( + '-r', '--renderer_module', metavar='MODULE', type=str, + help='Renderer module. Usually something like `.bash\' or `.zsh\', is supposed to be set only in shell-specific bindings file.' + ) + p.add_argument('-w', '--width', type=int, help='Maximum prompt with. Triggers truncation of some segments') + p.add_argument('--last_exit_code', metavar='INT', type=int, help='Last exit code') + p.add_argument('--last_pipe_status', metavar='LIST', default='', type=lambda s: [int(status) for status in s.split()], help='Like above, but is supposed to contain space-separated array of statuses, representing exit statuses of commands in one pipe.') + p.add_argument('--jobnum', metavar='INT', type=int, help='Number of jobs.') + p.add_argument('-c', '--config', metavar='KEY.KEY=VALUE', action='append', help='Configuration overrides for `config.json\'. Is translated to a dictionary and merged with the dictionary obtained from actual JSON configuration: KEY.KEY=VALUE is translated to `{"KEY": {"KEY": VALUE}}\' and then merged recursively. VALUE may be any JSON value, values that are not `null\', `true\', `false\', start with digit, `{\', `[\' are treated like strings. If VALUE is omitted then corresponding key is removed.') + p.add_argument('-t', '--theme_option', metavar='THEME.KEY.KEY=VALUE', action='append', help='Like above, but theme-specific. THEME should point to an existing and used theme to have any effect, but it is fine to use any theme here.') + p.add_argument('-R', '--renderer_arg', metavar='KEY=VAL', action='append', help='Like above, but provides argument for renderer. Is supposed to be used only by shell bindings to provide various data like last_exit_code or last_pipe_status (they are not using --renderer_arg for historical resons: renderer_arg was added later).') + p.add_argument('-p', '--config_path', action='append', metavar='PATH', help='Path to configuration directory. If it is present then configuration files will only be seeked in the provided path. May be provided multiple times to search in a list of directories.') + p.add_argument('--socket', metavar='ADDRESS', type=str, help='Socket address to use in daemon clients. Is always UNIX domain socket on linux and file socket on Mac OS X. Not used here, present only for compatibility with other powerline clients. This argument must always be the first one and be in a form `--socket ADDRESS\': no `=\' or short form allowed (in other powerline clients, not here).') + return p + + +def finish_args(args): + if args.config: + args.config = mergeargs((parsedotval(v) for v in args.config)) + if args.theme_option: + args.theme_option = mergeargs((parsedotval(v) for v in args.theme_option)) + else: + args.theme_option = {} + if args.renderer_arg: + args.renderer_arg = mergeargs((parsedotval(v) for v in args.renderer_arg)) + + +def write_output(args, powerline, segment_info, write, encoding): + if args.renderer_arg: + segment_info.update(args.renderer_arg) + if args.side.startswith('above'): + for line in powerline.render_above_lines( + width=args.width, + segment_info=segment_info, + mode=segment_info['environ'].get('_POWERLINE_MODE'), + ): + write(line.encode(encoding, 'replace')) + write(b'\n') + args.side = args.side[len('above'):] + + if args.side: + rendered = powerline.render( + width=args.width, + side=args.side, + segment_info=segment_info, + mode=segment_info['environ'].get('_POWERLINE_MODE'), + ) + write(rendered.encode(encoding, 'replace')) diff --git a/powerline/theme.py b/powerline/theme.py new file mode 100644 index 00000000..6bfc9b45 --- /dev/null +++ b/powerline/theme.py @@ -0,0 +1,168 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import itertools + +from powerline.segment import gen_segment_getter, process_segment +from powerline.lib.unicode import u + + +def requires_segment_info(func): + func.powerline_requires_segment_info = True + return func + + +def requires_filesystem_watcher(func): + func.powerline_requires_filesystem_watcher = True + return func + + +def new_empty_segment_line(): + return { + 'left': [], + 'right': [] + } + + +def add_spaces_left(pl, amount, segment): + return (' ' * amount) + segment['contents'] + + +def add_spaces_right(pl, amount, segment): + return segment['contents'] + (' ' * amount) + + +def add_spaces_center(pl, amount, segment): + amount, remainder = divmod(amount, 2) + return (' ' * (amount + remainder)) + segment['contents'] + (' ' * amount) + + +expand_functions = { + 'l': add_spaces_right, + 'r': add_spaces_left, + 'c': add_spaces_center, +} + + +class Theme(object): + def __init__(self, + ext, + theme_config, + common_config, + pl, + get_module_attr, + top_theme, + colorscheme, + main_theme_config=None, + run_once=False, + shutdown_event=None): + self.colorscheme = colorscheme + self.dividers = theme_config['dividers'] + self.dividers = dict(( + (key, dict((k, u(v)) + for k, v in val.items())) + for key, val in self.dividers.items() + )) + try: + self.cursor_space_multiplier = 1 - (theme_config['cursor_space'] / 100) + except KeyError: + self.cursor_space_multiplier = None + self.cursor_columns = theme_config.get('cursor_columns') + self.spaces = theme_config['spaces'] + self.segments = [] + self.EMPTY_SEGMENT = { + 'contents': None, + 'highlight': {'fg': False, 'bg': False, 'attr': 0} + } + self.pl = pl + theme_configs = [theme_config] + if main_theme_config: + theme_configs.append(main_theme_config) + get_segment = gen_segment_getter( + pl, + ext, + common_config, + theme_configs, + theme_config.get('default_module'), + get_module_attr, + top_theme + ) + for segdict in itertools.chain((theme_config['segments'],), + theme_config['segments'].get('above', ())): + self.segments.append(new_empty_segment_line()) + for side in ['left', 'right']: + for segment in segdict.get(side, []): + segment = get_segment(segment, side) + if segment: + if not run_once: + if segment['startup']: + try: + segment['startup'](pl, shutdown_event) + except Exception as e: + pl.error('Exception during {0} startup: {1}', segment['name'], str(e)) + continue + self.segments[-1][side].append(segment) + + def shutdown(self): + for line in self.segments: + for segments in line.values(): + for segment in segments: + try: + segment['shutdown']() + except TypeError: + pass + + def get_divider(self, side='left', type='soft'): + '''Return segment divider.''' + return self.dividers[side][type] + + def get_spaces(self): + return self.spaces + + def get_line_number(self): + return len(self.segments) + + def get_segments(self, side=None, line=0, segment_info=None, mode=None): + '''Return all segments. + + Function segments are called, and all segments get their before/after + and ljust/rjust properties applied. + + :param int line: + Line number for which segments should be obtained. Is counted from + zero (botmost line). + ''' + for side in [side] if side else ['left', 'right']: + parsed_segments = [] + for segment in self.segments[line][side]: + if segment['display_condition'](self.pl, segment_info, mode): + process_segment( + self.pl, + side, + segment_info, + parsed_segments, + segment, + mode, + self.colorscheme, + ) + for segment in parsed_segments: + width = segment['width'] + align = segment['align'] + if width == 'auto' and segment['expand'] is None: + segment['expand'] = expand_functions.get(align) + if segment['expand'] is None: + self.pl.error('Align argument must be “r”, “l” or “c”, not “{0}”', align) + + segment['contents'] = segment['before'] + u(segment['contents'] if segment['contents'] is not None else '') + segment['after'] + # Align segment contents + if segment['width'] and segment['width'] != 'auto': + if segment['align'] == 'l': + segment['contents'] = segment['contents'].ljust(segment['width']) + elif segment['align'] == 'r': + segment['contents'] = segment['contents'].rjust(segment['width']) + elif segment['align'] == 'c': + segment['contents'] = segment['contents'].center(segment['width']) + # We need to yield a copy of the segment, or else mode-dependent + # segment contents can't be cached correctly e.g. when caching + # non-current window contents for vim statuslines + yield segment.copy() diff --git a/powerline/vim.py b/powerline/vim.py new file mode 100644 index 00000000..d0123707 --- /dev/null +++ b/powerline/vim.py @@ -0,0 +1,284 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from itertools import count + +import vim + +from powerline.bindings.vim import vim_get_func, vim_getvar +from powerline import Powerline, FailedUnicode +from powerline.lib import mergedicts + +if not hasattr(vim, 'bindeval'): + import json + + +def _override_from(config, override_varname): + try: + overrides = vim_getvar(override_varname) + except KeyError: + return config + mergedicts(config, overrides) + return config + + +class VimPowerline(Powerline): + def init(self, pyeval='PowerlinePyeval', **kwargs): + super(VimPowerline, self).init('vim', **kwargs) + self.last_window_id = 1 + self.pyeval = pyeval + self.window_statusline = '%!' + pyeval + '(\'powerline.statusline({0})\')' + + default_log_stream = sys.stdout + + def add_local_theme(self, key, config): + '''Add local themes at runtime (during vim session). + + :param str key: + Matcher name (in format ``{matcher_module}.{module_attribute}`` or + ``{module_attribute}`` if ``{matcher_module}`` is + ``powerline.matchers.vim``). Function pointed by + ``{module_attribute}`` should be hashable and accept a dictionary + with information about current buffer and return boolean value + indicating whether current window matched conditions. See also + :ref:`local_themes key description `. + + :param dict config: + :ref:`Theme ` dictionary. + + :return: + ``True`` if theme was added successfully and ``False`` if theme with + the same matcher already exists. + ''' + self.update_renderer() + matcher = self.get_matcher(key) + theme_config = {} + for cfg_path in self.theme_levels: + try: + lvl_config = self.load_config(cfg_path, 'theme') + except IOError: + pass + else: + mergedicts(theme_config, lvl_config) + mergedicts(theme_config, config) + try: + self.renderer.add_local_theme(matcher, {'config': theme_config}) + except KeyError: + return False + else: + # Hack for local themes support: when reloading modules it is not + # guaranteed that .add_local_theme will be called once again, so + # this function arguments will be saved here for calling from + # .do_setup(). + self.setup_kwargs.setdefault('_local_themes', []).append((key, config)) + return True + + @staticmethod + def get_encoding(): + return vim.eval('&encoding') + + def load_main_config(self): + return _override_from(super(VimPowerline, self).load_main_config(), 'powerline_config_overrides') + + def load_theme_config(self, name): + # Note: themes with non-[a-zA-Z0-9_] names are impossible to override + # (though as far as I know exists() won’t throw). Won’t fix, use proper + # theme names. + return _override_from( + super(VimPowerline, self).load_theme_config(name), + 'powerline_theme_overrides__' + name + ) + + def get_local_themes(self, local_themes): + if not local_themes: + return {} + + return dict(( + (matcher, {'config': self.load_theme_config(val)}) + for matcher, key, val in ( + ( + (None if k == '__tabline__' else self.get_matcher(k)), + k, + v + ) + for k, v in local_themes.items() + ) if ( + matcher or + key == '__tabline__' + ) + )) + + def get_matcher(self, match_name): + match_module, separator, match_function = match_name.rpartition('.') + if not separator: + match_module = 'powerline.matchers.{0}'.format(self.ext) + match_function = match_name + return self.get_module_attr(match_module, match_function, prefix='matcher_generator') + + def get_config_paths(self): + try: + return vim_getvar('powerline_config_paths') + except KeyError: + return super(VimPowerline, self).get_config_paths() + + def do_setup(self, pyeval=None, pycmd=None, can_replace_pyeval=True, _local_themes=()): + import __main__ + if not pyeval: + pyeval = 'pyeval' if sys.version_info < (3,) else 'py3eval' + can_replace_pyeval = True + if not pycmd: + pycmd = get_default_pycmd() + + set_pycmd(pycmd) + + # pyeval() and vim.bindeval were both introduced in one patch + if not hasattr(vim, 'bindeval') and can_replace_pyeval: + vim.command((''' + function! PowerlinePyeval(e) + {pycmd} powerline.do_pyeval() + endfunction + ''').format(pycmd=pycmd)) + pyeval = 'PowerlinePyeval' + + self.pyeval = pyeval + self.window_statusline = '%!' + pyeval + '(\'powerline.statusline({0})\')' + + self.update_renderer() + __main__.powerline = self + + if ( + bool(int(vim.eval("has('gui_running') && argc() == 0"))) + and not vim.current.buffer.name + and len(vim.windows) == 1 + ): + # Hack to show startup screen. Problems in GUI: + # - Defining local value of &statusline option while computing global + # value purges startup screen. + # - Defining highlight group while computing statusline purges startup + # screen. + # This hack removes the “while computing statusline” part: both things + # are defined, but they are defined right now. + # + # The above condition disables this hack if no GUI is running, Vim did + # not open any files and there is only one window. Without GUI + # everything works, in other cases startup screen is not shown. + self.new_window() + + # Cannot have this in one line due to weird newline handling (in :execute + # context newline is considered part of the command in just the same cases + # when bar is considered part of the command (unless defining function + # inside :execute)). vim.command is :execute equivalent regarding this case. + vim.command('augroup Powerline') + vim.command(' autocmd! ColorScheme * :{pycmd} powerline.reset_highlight()'.format(pycmd=pycmd)) + vim.command(' autocmd! VimLeavePre * :{pycmd} powerline.shutdown()'.format(pycmd=pycmd)) + vim.command('augroup END') + + # Hack for local themes support after reloading. + for args in _local_themes: + self.add_local_theme(*args) + + @staticmethod + def get_segment_info(): + return {} + + def reset_highlight(self): + try: + self.renderer.reset_highlight() + except AttributeError: + # Renderer object appears only after first `.render()` call. Thus if + # ColorScheme event happens before statusline is drawn for the first + # time AttributeError will be thrown for the self.renderer. It is + # fine to ignore it: no renderer == no colors to reset == no need to + # 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.window_statusline.format(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')) + _vim_setwinvar = staticmethod(vim_get_func('setwinvar')) + + 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.window_statusline.format(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) + if not window: + return FailedUnicode('No window {0}'.format(window_id)) + return self.render(window, window_id, winnr) + + def tabline(self): + return self.render(*self.win_idx(None), is_tabline=True) + + def new_window(self): + return self.render(*self.win_idx(None)) + + if not hasattr(vim, 'bindeval'): + # Method for PowerlinePyeval function. Is here to reduce the number of + # requirements to __main__ globals to just one powerline object + # (previously it required as well vim and json) + @staticmethod + def do_pyeval(): + import __main__ + vim.command('return ' + json.dumps(eval(vim.eval('a:e'), __main__.__dict__))) + + def setup_components(self, components): + if components is None: + components = ('statusline', 'tabline') + if 'statusline' in components: + # Is immediately changed after new_window function is run. Good for + # global value. + vim.command('set statusline=%!{pyeval}(\'powerline.new_window()\')'.format( + pyeval=self.pyeval)) + if 'tabline' in components: + vim.command('set tabline=%!{pyeval}(\'powerline.tabline()\')'.format( + pyeval=self.pyeval)) + + +pycmd = None + + +def set_pycmd(new_pycmd): + global pycmd + pycmd = new_pycmd + + +def get_default_pycmd(): + return 'python' if sys.version_info < (3,) else 'python3' + + +def setup(*args, **kwargs): + powerline = VimPowerline() + return powerline.setup(*args, **kwargs) diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 00000000..f2ffc12a --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +powerline diff --git a/scripts/powerline-config b/scripts/powerline-config new file mode 100755 index 00000000..d64939e4 --- /dev/null +++ b/scripts/powerline-config @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet + +'''Script used to obtain powerline configuration''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import argparse + +try: + import powerline.bindings.config as config +except ImportError: + import sys + import os + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__))))) + import powerline.bindings.config as config + + +TMUX_ACTIONS = { + 'source': config.source_tmux_files, +} + + +SHELL_ACTIONS = { + 'command': config.shell_command, + 'uses': config.uses, +} + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers() + tmux_parser = subparsers.add_parser('tmux', help='Tmux-specific commands') + tmux_parser.add_argument( + 'function', + choices=tuple(TMUX_ACTIONS.values()), + metavar='action', + type=(lambda v: TMUX_ACTIONS.get(v)), + help='If action is "source" then version-specific tmux configuration files are sourced.' + ) + + shell_parser = subparsers.add_parser('shell', help='Shell-specific commands') + shell_parser.add_argument( + 'function', + choices=tuple(SHELL_ACTIONS.values()), + type=(lambda v: SHELL_ACTIONS.get(v)), + metavar='action', + help='If action is "command" then preferred powerline command is output, if it is “uses” then powerline-config script will exit with 1 if specified component is disabled and 0 otherwise.', + ) + shell_parser.add_argument( + 'component', + nargs='?', + choices=('tmux', 'prompt'), + metavar='component', + ) + shell_parser.add_argument( + '-s', '--shell', + nargs='?', + help='Shell for which query is run', + ) + + args = parser.parse_args() + + pl = config.create_powerline_logger(args) + + args.function(pl, args) diff --git a/scripts/powerline-daemon b/scripts/powerline-daemon new file mode 100755 index 00000000..5ac069d8 --- /dev/null +++ b/scripts/powerline-daemon @@ -0,0 +1,428 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import socket +import os +import errno +import sys + +from argparse import ArgumentParser +from select import select +from signal import signal, SIGTERM +from time import sleep +from functools import partial +from locale import getpreferredencoding +from io import BytesIO + +from powerline.shell import get_argparser, finish_args, ShellPowerline, write_output +from powerline.lib.monotonic import monotonic + + +is_daemon = False +platform = sys.platform.lower() +use_filesystem = 'darwin' in platform + +address = None +pidfile = None + + +class NonInteractiveArgParser(ArgumentParser): + def print_usage(self, file=None): + raise Exception(self.format_usage()) + + def print_help(self, file=None): + raise Exception(self.format_help()) + + def exit(self, status=0, message=None): + pass + + def error(self, message): + raise Exception(self.format_usage()) + + +parser = get_argparser(parser=NonInteractiveArgParser, description='powerline daemon') + +EOF = b'EOF\0\0' + +powerlines = {} +logger = None +config_loader = None +home = os.path.expanduser('~') + + +class PowerlineDaemon(ShellPowerline): + def get_log_handler(self): + if not is_daemon: + import logging + return logging.StreamHandler() + return super(PowerlineDaemon, self).get_log_handler() + + +def render(args, environ, cwd): + global logger + global config_loader + cwd = cwd or environ.get('PWD', '/') + segment_info = { + 'getcwd': lambda: cwd, + 'home': environ.get('HOME', home), + 'environ': environ, + 'args': args, + } + key = ( + args.ext[0], + args.renderer_module, + tuple(args.config) if args.config else None, + tuple(args.theme_option) if args.theme_option else None, + ) + finish_args(args) + powerline = None + try: + powerline = powerlines[key] + except KeyError: + try: + powerline = powerlines[key] = PowerlineDaemon( + args, + logger=logger, + config_loader=config_loader, + run_once=False, + ) + if logger is None: + logger = powerline.logger + if config_loader is None: + config_loader = powerline.config_loader + except SystemExit: + # Somebody thought raising system exit was a good idea, + return '' + except Exception as e: + if powerline: + powerline.pl.exception('Failed to render {0}: {1}', str(key), str(e)) + else: + return 'Failed to render {0}: {1}'.format(str(key), str(e)) + s = BytesIO() + write_output(args, powerline, segment_info, s.write, encoding) + s.seek(0) + return s.read() + + +def eintr_retry_call(func, *args, **kwargs): + while True: + try: + return func(*args, **kwargs) + except EnvironmentError as e: + if getattr(e, 'errno', None) == errno.EINTR: + continue + raise + + +def do_read(conn, timeout=2.0): + ''' Read data from the client. If the client fails to send data within + timeout seconds, abort. ''' + read = [] + end_time = monotonic() + timeout + while not read or not read[-1].endswith(b'\0\0'): + r, w, e = select((conn,), (), (conn,), timeout) + if e: + return + if monotonic() > end_time: + return + if not r: + continue + x = eintr_retry_call(conn.recv, 4096) + if x: + read.append(x) + else: + break + return b''.join(read) + + +def do_write(conn, result): + try: + eintr_retry_call(conn.sendall, result) + except Exception: + pass + + +encoding = getpreferredencoding() or 'UTF-8' + + +def safe_bytes(o, encoding=encoding): + '''Return bytes instance without ever throwing an exception.''' + try: + try: + # We are assuming that o is a unicode object + return o.encode(encoding, 'replace') + except Exception: + # Object may have defined __bytes__ (python 3) or __str__ method + # (python 2) + # This also catches problem with non_ascii_bytes.encode('utf-8') + # that first tries to decode to UTF-8 using ascii codec (and fails + # in this case) and then encode to given encoding: errors= argument + # is not used in the first stage. + return bytes(o) + except Exception as e: + return safe_bytes(str(e), encoding) + + +def parse_args(req): + args = [x.decode(encoding) for x in req.split(b'\0') if x] + numargs = int(args[0], 16) + shell_args = parser.parse_args(args[1:numargs + 1]) + cwd = args[numargs + 1] + environ = dict(((k, v) for k, v in (x.partition('=')[0::2] for x in args[numargs + 2:]))) + return shell_args, environ, cwd + + +def do_render(req): + try: + return safe_bytes(render(*parse_args(req))) + except Exception as e: + return safe_bytes(str(e)) + + +def do_one(sock, read_sockets, write_sockets, result_map): + r, w, e = select( + tuple(read_sockets) + (sock,), + tuple(write_sockets), + tuple(read_sockets) + tuple(write_sockets) + (sock,), + 60.0 + ) + + if sock in e: + # We cannot accept any more connections, so we exit + raise SystemExit(1) + + for s in e: + # Discard all broken connections to clients + s.close() + read_sockets.discard(s) + write_sockets.discard(s) + + for s in r: + if s == sock: + # A client wants to connect + conn, _ = eintr_retry_call(sock.accept) + read_sockets.add(conn) + else: + # A client has sent some data + read_sockets.discard(s) + req = do_read(s) + if req == EOF: + raise SystemExit(0) + elif req: + ans = do_render(req) + result_map[s] = ans + write_sockets.add(s) + else: + s.close() + + for s in w: + # A client is ready to receive the result + write_sockets.discard(s) + result = result_map.pop(s) + try: + do_write(s, result) + finally: + s.close() + + +def main_loop(sock): + sock.listen(1) + sock.setblocking(0) + + read_sockets, write_sockets = set(), set() + result_map = {} + try: + while True: + do_one(sock, read_sockets, write_sockets, result_map) + except KeyboardInterrupt: + raise SystemExit(0) + return 0 + + +def daemonize(stdin=os.devnull, stdout=os.devnull, stderr=os.devnull): + try: + pid = os.fork() + if pid > 0: + # exit first parent + sys.exit(0) + except OSError as e: + sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # decouple from parent environment + os.chdir("/") + os.setsid() + os.umask(0) + + # do second fork + try: + pid = os.fork() + if pid > 0: + # exit from second parent + sys.exit(0) + except OSError as e: + sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(1) + + # Redirect standard file descriptors. + si = open(stdin, 'rb') + so = open(stdout, 'a+b') + se = open(stderr, 'a+b', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + global is_daemon + is_daemon = True + + +def check_existing(): + if use_filesystem: + # We cannot bind if the socket file already exists so remove it, we + # already have a lock on pidfile, so this should be safe. + try: + os.unlink(address) + except EnvironmentError: + pass + + sock = socket.socket(family=socket.AF_UNIX) + try: + sock.bind(address) + except socket.error as e: + if getattr(e, 'errno', None) == errno.EADDRINUSE: + return None + raise + return sock + + +def kill_daemon(): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + try: + eintr_retry_call(sock.connect, address) + except socket.error: + return False + else: + eintr_retry_call(sock.sendall, EOF) + finally: + sock.close() + return True + + +def cleanup_lockfile(fd, *args): + try: + # Remove the directory entry for the lock file + os.unlink(pidfile) + # Close the file descriptor + os.close(fd) + except EnvironmentError: + pass + if args: + # Called in signal handler + raise SystemExit(1) + + +def lockpidfile(): + import fcntl + import atexit + import stat + fd = os.open( + pidfile, + os.O_WRONLY | os.O_CREAT, + stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + ) + try: + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except EnvironmentError: + os.close(fd) + return None + os.lseek(fd, 0, os.SEEK_SET) + os.ftruncate(fd, 0) + os.write(fd, ('%d' % os.getpid()).encode('ascii')) + os.fsync(fd) + cleanup = partial(cleanup_lockfile, fd) + signal(SIGTERM, cleanup) + atexit.register(cleanup) + return fd + + +def main(): + global address + global pidfile + p = ArgumentParser(description='Daemon to improve the performance of powerline') + p.add_argument('--quiet', '-q', action='store_true', help='Without other options: do not complain about already running powerline-daemon instance. Will still exit with 1. With `--kill\' and `--replace\': do not show any messages. With `--foreground\': ignored. Does not silence exceptions in any case.') + p.add_argument('--socket', '-s', help='Specify socket which will be used for connecting to daemon.') + a = p.add_mutually_exclusive_group().add_argument + a('--kill', '-k', action='store_true', help='Kill an already running instance') + a('--foreground', '-f', action='store_true', help='Run in the foreground (dont daemonize)') + a('--replace', '-r', action='store_true', help='Replace an already running instance') + args = p.parse_args() + + if args.socket: + address = args.socket + if not use_filesystem: + address = '\0' + address + else: + if use_filesystem: + address = '/tmp/powerline-ipc-%d' + else: + # Use the abstract namespace for sockets rather than the filesystem + # (Available only in linux) + address = '\0powerline-ipc-%d' + + address = address % os.getuid() + + if use_filesystem: + pidfile = address + '.pid' + + if args.kill: + if kill_daemon(): + if not args.quiet: + print ('Kill command sent to daemon, if it does not die in a couple of seconds use kill to kill it') + raise SystemExit(0) + else: + if not args.quiet: + print ('No running daemon found') + raise SystemExit(1) + + if args.replace: + while kill_daemon(): + if not args.quiet: + print ('Kill command sent to daemon, waiting for daemon to exit, press Ctrl-C to terminate wait and exit') + sleep(2) + + if use_filesystem and not args.foreground: + # We must daemonize before creating the locked pidfile, unfortunately, + # this means further print statements are discarded + daemonize() + + if use_filesystem: + # Create a locked pid file containing the daemon's PID + if lockpidfile() is None: + if not args.quiet: + sys.stderr.write( + 'The daemon is already running. Use %s -k to kill it.\n' % ( + os.path.basename(sys.argv[0]))) + raise SystemExit(1) + + # Bind to address or bail if we cannot bind + sock = check_existing() + if sock is None: + if not args.quiet: + sys.stderr.write( + 'The daemon is already running. Use %s -k to kill it.\n' % ( + os.path.basename(sys.argv[0]))) + raise SystemExit(1) + + if args.foreground: + return main_loop(sock) + + if not use_filesystem: + # We daemonize on linux + daemonize() + + main_loop(sock) + + +if __name__ == '__main__': + main() diff --git a/scripts/powerline-lint b/scripts/powerline-lint new file mode 100755 index 00000000..cfebad3e --- /dev/null +++ b/scripts/powerline-lint @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet + +'''Powerline configuration checker.''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import argparse +import sys + +from powerline.lint import check + + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('-p', '--config_path', action='append', metavar='PATH') +parser.add_argument('-d', '--debug', action='store_const', const=True) + +if __name__ == '__main__': + args = parser.parse_args() + sys.exit(check(args.config_path, args.debug)) diff --git a/scripts/powerline-render b/scripts/powerline-render new file mode 100755 index 00000000..048c34ce --- /dev/null +++ b/scripts/powerline-render @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet + +'''Powerline prompt and statusline script.''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os + +from locale import getpreferredencoding + +try: + from powerline.shell import ShellPowerline, get_argparser, finish_args, write_output +except ImportError: + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__))))) + from powerline.shell import ShellPowerline, get_argparser, finish_args, write_output + + +if sys.version_info < (3,): + write = sys.stdout.write +else: + write = sys.stdout.buffer.write + + +if __name__ == '__main__': + args = get_argparser(description=__doc__).parse_args() + finish_args(args) + powerline = ShellPowerline(args, run_once=True) + segment_info = {'args': args, 'environ': os.environ} + write_output(args, powerline, segment_info, write, getpreferredencoding()) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..efb70e38 --- /dev/null +++ b/setup.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import subprocess +import logging + +from setuptools import setup, find_packages + + +CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) +try: + README = open(os.path.join(CURRENT_DIR, 'README.rst'), 'rb').read().decode('utf-8') +except IOError: + README = '' + +OLD_PYTHON = sys.version_info < (2, 7) + + +def compile_client(): + '''Compile the C powerline-client script.''' + + if hasattr(sys, 'getwindowsversion'): + raise NotImplementedError() + else: + from distutils.ccompiler import new_compiler + compiler = new_compiler().compiler + subprocess.check_call(compiler + ['-O3', 'client/powerline.c', '-o', 'scripts/powerline']) + +try: + compile_client() +except Exception as e: + print('Compiling C version of powerline-client failed') + logging.exception(e) + # FIXME Catch more specific exceptions + import shutil + if hasattr(shutil, 'which'): + which = shutil.which + else: + sys.path.append(CURRENT_DIR) + from powerline.lib import which + if which('socat') and which('sed') and which('sh'): + print('Using powerline.sh script instead of C version (requires socat, sed and sh)') + shutil.copyfile('client/powerline.sh', 'scripts/powerline') + can_use_scripts = True + else: + print('Using powerline.py script instead of C version') + shutil.copyfile('client/powerline.py', 'scripts/powerline') + can_use_scripts = True +else: + can_use_scripts = False + +setup( + name='powerline-status', + version='1.0', + description='The ultimate statusline/prompt utility.', + long_description=README, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Environment :: Plugins', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: POSIX', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + download_url='https://github.com/Lokaltog/powerline/archive/develop.zip', + author='Kim Silkebaekken', + author_email='kim.silkebaekken+vim@gmail.com', + url='https://github.com/Lokaltog/powerline', + license='MIT', + # XXX Python 3 doesn't allow compiled C files to be included in the scripts + # list below. This is because Python 3 distutils tries to decode the file to + # ASCII, and fails when powerline-client is a binary. + # + # XXX Python 2 fucks up script contents*. Not using it to install scripts + # any longer. + # * Consider the following input: + # % alias hex1=$'hexdump -e \'"" 1/1 "%02X\n"\'' + # % diff <(hex1 ./scripts/powerline) <(hex1 ~/.local/bin/powerline) + # This will show output like + # 375c375 + # < 0D + # --- + # > 0A + # (repeated, with diff segment header numbers growing up). + # + # FIXME Current solution does not work with `pip install -e`. Still better + # then solution that is not working at all. + scripts=[ + 'scripts/powerline-lint', + 'scripts/powerline-daemon', + 'scripts/powerline-render', + 'scripts/powerline-config', + ] + (['scripts/powerline'] if can_use_scripts else []), + data_files=(None if can_use_scripts else (('bin', ['scripts/powerline']),)), + keywords='', + packages=find_packages(exclude=('tests', 'tests.*')), + include_package_data=True, + zip_safe=False, + install_requires=[], + extras_require={ + 'docs': [ + 'Sphinx', + 'sphinx_rtd_theme', + ], + }, + test_suite='tests' if not OLD_PYTHON else None, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..9a961acd --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,11 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +if sys.version_info < (2, 7): + from unittest2 import TestCase, main # NOQA + from unittest2.case import SkipTest # NOQA +else: + from unittest import TestCase, main # NOQA + from unittest.case import SkipTest # NOQA diff --git a/tests/empty b/tests/empty new file mode 100644 index 00000000..e69de29b diff --git a/tests/install.sh b/tests/install.sh new file mode 100755 index 00000000..42ef7650 --- /dev/null +++ b/tests/install.sh @@ -0,0 +1,28 @@ +#!/bin/sh +pip install . +pip install psutil netifaces +if python -c 'import sys; sys.exit(1 * (sys.version_info[0] != 2))' ; then + # Python 2 + if python -c 'import platform, sys; sys.exit(1 - (platform.python_implementation() == "CPython"))' ; then + # PyPy + pip install mercurial + pip install --allow-external bzr --allow-unverified bzr bzr + fi + if python -c 'import sys; sys.exit(1 * (sys.version_info[1] >= 7))' ; then + # Python 2.6 + pip install unittest2 argparse + else + # Python 2.7 + pip install ipython + fi +else + # Python 3 + if python -c 'import sys; sys.exit(1 * (sys.version_info < (3, 3)))' ; then + # Python 3.3+ + pip install ipython + fi +fi +sudo apt-get install -qq screen zsh tcsh mksh busybox socat +# Travis has too outdated fish. It cannot be used for tests. +# sudo apt-get install fish +true diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py new file mode 100644 index 00000000..80af8097 --- /dev/null +++ b/tests/lib/__init__.py @@ -0,0 +1,155 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import imp +import sys + + +class Pl(object): + def __init__(self): + self.exceptions = [] + self.errors = [] + self.warns = [] + self.debugs = [] + self.infos = [] + self.prefix = None + self.use_daemon_threads = True + + for meth in ('error', 'warn', 'debug', 'exception', 'info'): + exec (( + 'def {0}(self, msg, *args, **kwargs):\n' + ' self.{0}s.append((kwargs.get("prefix") or self.prefix, msg, args, kwargs))\n' + ).format(meth)) + + +class Args(object): + theme_option = {} + config = None + config_path = None + ext = ['shell'] + renderer_module = None + + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +def urllib_read(query_url): + if query_url.startswith('http://ipv'): + if query_url.startswith('http://ipv4.icanhazip.com'): + return '127.0.0.1' + elif query_url.startswith('http://ipv4.icanhazip.com'): + return '2001:4801:7818:6:abc5:ba2c:ff10:275f' + elif query_url.startswith('http://freegeoip.net/json/'): + return '{"city": "Meppen", "region_code": "06", "region_name": "Niedersachsen", "areacode": "", "ip": "82.145.55.16", "zipcode": "49716", "longitude": 7.3167, "country_name": "Germany", "country_code": "DE", "metrocode": "", "latitude": 52.6833}' + elif query_url.startswith('http://query.yahooapis.com/v1/public/'): + return r'{"query":{"count":1,"created":"2013-03-02T13:20:22Z","lang":"en-US","results":{"weather":{"rss":{"version":"2.0","geo":"http://www.w3.org/2003/01/geo/wgs84_pos#","yweather":"http://xml.weather.yahoo.com/ns/rss/1.0","channel":{"title":"Yahoo! Weather - Russia, RU","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Russia__RU/*http://weather.yahoo.com/forecast/RSXX1511_c.html","description":"Yahoo! Weather for Russia, RU","language":"en-us","lastBuildDate":"Sat, 02 Mar 2013 4:58 pm MSK","ttl":"60","location":{"city":"Russia","country":"Russia","region":""},"units":{"distance":"km","pressure":"mb","speed":"km/h","temperature":"C"},"wind":{"chill":"-9","direction":"0","speed":""},"atmosphere":{"humidity":"94","pressure":"1006.1","rising":"0","visibility":""},"astronomy":{"sunrise":"10:04 am","sunset":"7:57 pm"},"image":{"title":"Yahoo! Weather","width":"142","height":"18","link":"http://weather.yahoo.com","url":"http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"},"item":{"title":"Conditions for Russia, RU at 4:58 pm MSK","lat":"59.45","long":"108.83","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Russia__RU/*http://weather.yahoo.com/forecast/RSXX1511_c.html","pubDate":"Sat, 02 Mar 2013 4:58 pm MSK","condition":{"code":"30","date":"Sat, 02 Mar 2013 4:58 pm MSK","temp":"-9","text":"Partly Cloudy"},"description":"
\nCurrent Conditions:
\nPartly Cloudy, -9 C
\n
Forecast:
\nSat - Partly Cloudy. High: -9 Low: -19
\nSun - Partly Cloudy. High: -12 Low: -18
\n
\nFull Forecast at Yahoo! Weather

\n(provided by The Weather Channel)
","forecast":[{"code":"29","date":"2 Mar 2013","day":"Sat","high":"-9","low":"-19","text":"Partly Cloudy"},{"code":"30","date":"3 Mar 2013","day":"Sun","high":"-12","low":"-18","text":"Partly Cloudy"}],"guid":{"isPermaLink":"false","content":"RSXX1511_2013_03_03_7_00_MSK"}}}}}}}}' + else: + raise NotImplementedError + + +class Process(object): + def __init__(self, output, err): + self.output = output + self.err = err + + def communicate(self): + return self.output, self.err + + +class ModuleReplace(object): + def __init__(self, name, new): + self.name = name + self.new = new + + def __enter__(self): + self.old = sys.modules.get(self.name) + if not self.old: + try: + self.old = __import__(self.name) + except ImportError: + pass + sys.modules[self.name] = self.new + + def __exit__(self, *args): + if self.old: + sys.modules[self.name] = self.old + else: + sys.modules.pop(self.name) + + +def replace_module(name, new=None, **kwargs): + if not new: + new = new_module(name, **kwargs) + return ModuleReplace(name, new) + + +def new_module(name, **kwargs): + module = imp.new_module(name) + for k, v in kwargs.items(): + setattr(module, k, v) + return module + + +class AttrReplace(object): + def __init__(self, obj, *args): + self.obj = obj + self.attrs = args[::2] + self.new = args[1::2] + + def __enter__(self): + self.old = {} + for i, attr in enumerate(self.attrs): + try: + self.old[i] = getattr(self.obj, attr) + except AttributeError: + pass + for attr, new in zip(self.attrs, self.new): + setattr(self.obj, attr, new) + + def __exit__(self, *args): + for i, attr in enumerate(self.attrs): + try: + old = self.old[i] + except KeyError: + delattr(self.obj, attr) + else: + setattr(self.obj, attr, old) + + +replace_attr = AttrReplace + + +def replace_module_module(module, name, **kwargs): + return replace_attr(module, name, new_module(name, **kwargs)) + + +class ItemReplace(object): + def __init__(self, d, key, new, r=None): + self.key = key + self.new = new + self.d = d + self.r = r + + def __enter__(self): + self.old = self.d.get(self.key) + self.d[self.key] = self.new + return self.r + + def __exit__(self, *args): + if self.old is None: + try: + self.d.pop(self.key) + except KeyError: + pass + else: + self.d[self.key] = self.old + + +def replace_item(d, key, new): + return ItemReplace(d, key, new, d) + + +def replace_env(key, new, environ=None, **kwargs): + r = kwargs.copy() + r['environ'] = environ or {} + return ItemReplace(r['environ'], key, new, r) diff --git a/tests/lib/config_mock.py b/tests/lib/config_mock.py new file mode 100644 index 00000000..7161ea28 --- /dev/null +++ b/tests/lib/config_mock.py @@ -0,0 +1,219 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from threading import Lock +from copy import deepcopy +from time import sleep +from functools import wraps + +from powerline.renderer import Renderer +from powerline.lib.config import ConfigLoader +from powerline import Powerline + +from tests.lib import Args, replace_attr + + +class TestHelpers(object): + def __init__(self, config): + self.config = config + self.access_log = [] + self.access_lock = Lock() + + def loader_condition(self, path): + return (path in self.config) and path + + def find_config_files(self, cfg_path, config_loader, loader_callback): + if cfg_path.endswith('.json'): + cfg_path = cfg_path[:-5] + if cfg_path.startswith('/'): + cfg_path = cfg_path.lstrip('/') + with self.access_lock: + self.access_log.append('check:' + cfg_path) + if cfg_path in self.config: + yield cfg_path + else: + if config_loader: + config_loader.register_missing(self.loader_condition, loader_callback, cfg_path) + raise IOError(('fcf:' if cfg_path.endswith('raise') else '') + cfg_path) + + def load_json_config(self, config_file_path, *args, **kwargs): + if config_file_path.endswith('.json'): + config_file_path = config_file_path[:-5] + if config_file_path.startswith('/'): + config_file_path = config_file_path.lstrip('/') + with self.access_lock: + self.access_log.append('load:' + config_file_path) + try: + return deepcopy(self.config[config_file_path]) + except KeyError: + raise IOError(config_file_path) + + def pop_events(self): + with self.access_lock: + r = self.access_log[:] + self.access_log = [] + return r + + +def log_call(func): + @wraps(func) + def ret(self, *args, **kwargs): + self._calls.append((func.__name__, args, kwargs)) + return func(self, *args, **kwargs) + return ret + + +class TestWatcher(object): + events = set() + lock = Lock() + + def __init__(self): + self._calls = [] + + @log_call + def watch(self, file): + pass + + @log_call + def __call__(self, file): + with self.lock: + if file in self.events: + self.events.remove(file) + return True + return False + + def _reset(self, files): + with self.lock: + self.events.clear() + self.events.update(files) + + @log_call + def unsubscribe(self): + pass + + +class Logger(object): + def __init__(self): + self.messages = [] + self.lock = Lock() + + def _add_msg(self, attr, msg): + with self.lock: + self.messages.append(attr + ':' + msg) + + def _pop_msgs(self): + with self.lock: + r = self.messages + self.messages = [] + return r + + def __getattr__(self, attr): + return lambda *args, **kwargs: self._add_msg(attr, *args, **kwargs) + + +class SimpleRenderer(Renderer): + def hlstyle(self, fg=None, bg=None, attr=None): + return '<{fg} {bg} {attr}>'.format(fg=fg and fg[0], bg=bg and bg[0], attr=attr) + + +class EvenSimplerRenderer(Renderer): + def hlstyle(self, fg=None, bg=None, attr=None): + return '{{{fg}{bg}{attr}}}'.format( + fg=fg and fg[0] or '-', + bg=bg and bg[0] or '-', + attr=attr if attr else '', + ) + + +class TestPowerline(Powerline): + _created = False + + def __init__(self, _helpers, **kwargs): + super(TestPowerline, self).__init__(**kwargs) + self._helpers = _helpers + self.find_config_files = _helpers.find_config_files + + @staticmethod + def get_local_themes(local_themes): + return local_themes + + @staticmethod + def get_config_paths(): + return [''] + + def _will_create_renderer(self): + return self.cr_kwargs + + def _pop_events(self): + return self._helpers.pop_events() + + +renderer = EvenSimplerRenderer + + +class TestConfigLoader(ConfigLoader): + def __init__(self, _helpers, **kwargs): + watcher = TestWatcher() + super(TestConfigLoader, self).__init__( + load=_helpers.load_json_config, + watcher=watcher, + watcher_type='test', + **kwargs + ) + + +def get_powerline(config, **kwargs): + helpers = TestHelpers(config) + return get_powerline_raw( + helpers, + TestPowerline, + _helpers=helpers, + ext='test', + renderer_module='tests.lib.config_mock', + logger=Logger(), + **kwargs + ) + + +def select_renderer(simpler_renderer=False): + global renderer + renderer = EvenSimplerRenderer if simpler_renderer else SimpleRenderer + + +def get_powerline_raw(helpers, PowerlineClass, **kwargs): + if not isinstance(helpers, TestHelpers): + helpers = TestHelpers(helpers) + select_renderer(kwargs.pop('simpler_renderer', False)) + pl = PowerlineClass( + config_loader=TestConfigLoader( + _helpers=helpers, + run_once=kwargs.get('run_once') + ), + **kwargs + ) + pl._watcher = pl.config_loader.watcher + return pl + + +def swap_attributes(config, powerline_module): + return replace_attr(powerline_module, 'os', Args( + path=Args( + isfile=lambda path: path.lstrip('/').replace('.json', '') in config, + join=os.path.join, + expanduser=lambda path: path, + realpath=lambda path: path, + dirname=os.path.dirname, + ), + environ={}, + )) + + +def add_watcher_events(p, *args, **kwargs): + if isinstance(p._watcher, TestWatcher): + p._watcher._reset(args) + while not p._will_create_renderer(): + sleep(kwargs.get('interval', 0.1)) + if not kwargs.get('wait', True): + return diff --git a/tests/lib/fsconfig.py b/tests/lib/fsconfig.py new file mode 100644 index 00000000..a1066f65 --- /dev/null +++ b/tests/lib/fsconfig.py @@ -0,0 +1,80 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import json + +from subprocess import check_call +from operator import add +from shutil import rmtree + +from powerline import Powerline + + +CONFIG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config') + + +class TestPowerline(Powerline): + def __init__(self, _paths, *args, **kwargs): + super(TestPowerline, self).__init__(*args, **kwargs) + self._paths = _paths + + def get_config_paths(self): + return self._paths + + +def mkdir_recursive(directory): + if os.path.isdir(directory): + return + mkdir_recursive(os.path.dirname(directory)) + os.mkdir(directory) + + +class FSTree(object): + __slots__ = ('tree', 'p', 'p_kwargs', 'create_p', 'get_config_paths', 'root') + + def __init__( + self, + tree, + p_kwargs={'run_once': True}, + root=CONFIG_DIR, + get_config_paths=lambda p: (p,), + create_p=False + ): + self.tree = tree + self.root = root + self.get_config_paths = get_config_paths + self.create_p = create_p + self.p = None + self.p_kwargs = p_kwargs + + def __enter__(self, *args): + os.mkdir(self.root) + for k, v in self.tree.items(): + fname = os.path.join(self.root, k) + '.json' + mkdir_recursive(os.path.dirname(fname)) + with open(fname, 'w') as F: + json.dump(v, F) + if self.create_p: + self.p = TestPowerline( + _paths=self.get_config_paths(self.root), + ext='test', + renderer_module='tests.lib.config_mock', + **self.p_kwargs + ) + if os.environ.get('POWERLINE_RUN_LINT_DURING_TESTS'): + try: + check_call(['scripts/powerline-lint'] + reduce(add, ( + ['-p', d] for d in self.p.get_config_paths() + ))) + except: + self.__exit__() + raise + return self.p and self.p.__enter__(*args) + + def __exit__(self, *args): + try: + rmtree(self.root) + finally: + if self.p: + self.p.__exit__(*args) diff --git a/tests/matchers.py b/tests/matchers.py new file mode 100644 index 00000000..e905de32 --- /dev/null +++ b/tests/matchers.py @@ -0,0 +1,6 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + + +def always_true(matcher_info): + return True diff --git a/tests/path/vim.py b/tests/path/vim.py new file mode 100644 index 00000000..1de56240 --- /dev/null +++ b/tests/path/vim.py @@ -0,0 +1,7 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from tests import vim + + +globals().update(vim._init()) diff --git a/tests/run_lint_tests.sh b/tests/run_lint_tests.sh new file mode 100755 index 00000000..09157910 --- /dev/null +++ b/tests/run_lint_tests.sh @@ -0,0 +1,7 @@ +#!/bin/sh +FAILED=0 +if ! ${PYTHON} scripts/powerline-lint -p powerline/config_files ; then + echo "Failed powerline-lint" + FAILED=1 +fi +exit $FAILED diff --git a/tests/run_python_tests.sh b/tests/run_python_tests.sh new file mode 100755 index 00000000..62eac30a --- /dev/null +++ b/tests/run_python_tests.sh @@ -0,0 +1,9 @@ +#!/bin/sh +FAILED=0 +for file in tests/test_*.py ; do + if ! ${PYTHON} $file --verbose --catch ; then + echo "Failed test(s) from $file" + FAILED=1 + fi +done +exit $FAILED diff --git a/tests/run_shell_tests.sh b/tests/run_shell_tests.sh new file mode 100755 index 00000000..08655312 --- /dev/null +++ b/tests/run_shell_tests.sh @@ -0,0 +1,9 @@ +#!/bin/sh +FAILED=0 +if ! sh tests/test_shells/test.sh --fast ; then + echo "Failed shells" + if ${PYTHON} -c 'import platform, sys; sys.exit(1 * (platform.python_implementation() == "PyPy"))' ; then + FAILED=1 + fi +fi +exit $FAILED diff --git a/tests/run_vim_tests.sh b/tests/run_vim_tests.sh new file mode 100755 index 00000000..ef82ab35 --- /dev/null +++ b/tests/run_vim_tests.sh @@ -0,0 +1,11 @@ +#!/bin/sh +FAILED=0 +for script in tests/*.vim ; do + if ! vim -u NONE -S $script || test -f message.fail ; then + echo "Failed script $script" >&2 + cat message.fail >&2 + rm message.fail + FAILED=1 + fi +done +exit $FAILED diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 00000000..c1195f3f --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,11 @@ +#!/bin/sh +FAILED=0 +export PYTHON="${PYTHON:=python}" +export PYTHONPATH="${PYTHONPATH}:`realpath .`" +for script in tests/run_*_tests.sh ; do + if ! sh $script ; then + echo "Failed $script" + FAILED=1 + fi +done +exit $FAILED diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py new file mode 100644 index 00000000..f28c3896 --- /dev/null +++ b/tests/test_cmdline.py @@ -0,0 +1,131 @@ +# vim:fileencoding=utf-8:noet + +'''Tests for shell.py parser''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +if sys.version_info < (3,): + from io import BytesIO as StrIO +else: + from io import StringIO as StrIO + +from powerline.shell import get_argparser, finish_args + +from tests import TestCase +from tests.lib import replace_attr + + +class TestParser(TestCase): + def test_main_err(self): + parser = get_argparser() + out = StrIO() + err = StrIO() + + def flush(): + out.truncate(0) + err.truncate(0) + + with replace_attr(sys, 'stdout', out, 'stderr', err): + for raising_args, raising_reg in [ + ([], 'too few arguments|the following arguments are required: ext'), + (['-r'], 'expected one argument'), + (['shell', '-r'], 'expected one argument'), + (['shell', '-w'], 'expected one argument'), + (['shell', '-c'], 'expected one argument'), + (['shell', '-t'], 'expected one argument'), + (['shell', '-p'], 'expected one argument'), + (['shell', '-R'], 'expected one argument'), + (['shell', '--renderer_module'], 'expected one argument'), + (['shell', '--width'], 'expected one argument'), + (['shell', '--last_exit_code'], 'expected one argument'), + (['shell', '--last_pipe_status'], 'expected one argument'), + (['shell', '--config'], 'expected one argument'), + (['shell', '--theme_option'], 'expected one argument'), + (['shell', '--config_path'], 'expected one argument'), + (['shell', '--renderer_arg'], 'expected one argument'), + (['shell', '--jobnum'], 'expected one argument'), + (['-r', '.zsh'], 'too few arguments|the following arguments are required: ext'), + (['shell', '--last_exit_code', 'i'], 'invalid int value'), + (['shell', '--last_pipe_status', '1 i'], 'invalid value'), + ]: + self.assertRaises(SystemExit, parser.parse_args, raising_args) + self.assertFalse(out.getvalue()) + self.assertRegexpMatches(err.getvalue(), raising_reg) + flush() + + def test_main_normal(self): + parser = get_argparser() + out = StrIO() + err = StrIO() + with replace_attr(sys, 'stdout', out, 'stderr', err): + for argv, expargs in [ + (['shell'], {'ext': ['shell']}), + (['shell', '-r', '.zsh'], {'ext': ['shell'], 'renderer_module': '.zsh'}), + ([ + 'shell', + 'left', + '-r', '.zsh', + '--last_exit_code', '10', + '--last_pipe_status', '10 20 30', + '--jobnum=10', + '-w', '100', + '-c', 'common.term_truecolor=true', + '-c', 'common.spaces=4', + '-t', 'default.segment_data.hostname.before=H:', + '-p', '.', + '-p', '..', + '-R', 'smth={"abc":"def"}', + ], { + 'ext': ['shell'], + 'side': 'left', + 'renderer_module': '.zsh', + 'last_exit_code': 10, + 'last_pipe_status': [10, 20, 30], + 'jobnum': 10, + 'width': 100, + 'config': {'common': {'term_truecolor': True, 'spaces': 4}}, + 'theme_option': { + 'default': { + 'segment_data': { + 'hostname': { + 'before': 'H:' + } + } + } + }, + 'config_path': ['.', '..'], + 'renderer_arg': {'smth': {'abc': 'def'}}, + }), + (['shell', '-R', 'arg=true'], {'ext': ['shell'], 'renderer_arg': {'arg': True}}), + (['shell', '-R', 'arg=true', '-R', 'arg='], {'ext': ['shell'], 'renderer_arg': {}}), + (['shell', '-R', 'arg='], {'ext': ['shell'], 'renderer_arg': {}}), + (['shell', '-t', 'default.segment_info={"hostname": {}}'], { + 'ext': ['shell'], + 'theme_option': { + 'default': { + 'segment_info': { + 'hostname': {} + } + } + }, + }), + (['shell', '-c', 'common={ }'], {'ext': ['shell'], 'config': {'common': {}}}), + ]: + args = parser.parse_args(argv) + finish_args(args) + for key, val in expargs.items(): + self.assertEqual(getattr(args, key), val) + for key, val in args.__dict__.items(): + if key not in expargs: + self.assertFalse(val, msg='key {0} is {1} while it should be something false'.format(key, val)) + self.assertFalse(err.getvalue() + out.getvalue(), msg='unexpected output: {0!r} {1!r}'.format( + err.getvalue(), + out.getvalue(), + )) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_config_merging.py b/tests/test_config_merging.py new file mode 100644 index 00000000..ffdc4b51 --- /dev/null +++ b/tests/test_config_merging.py @@ -0,0 +1,270 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import json + +from subprocess import check_call +from operator import add +from shutil import rmtree + +from powerline.lib import mergedicts_copy as mdc +from powerline import Powerline + +from tests import TestCase +from tests.lib.config_mock import select_renderer + + +CONFIG_DIR = 'tests/config' + + +root_config = lambda: { + 'common': { + 'interval': None, + 'watcher': 'auto', + }, + 'ext': { + 'test': { + 'theme': 'default', + 'colorscheme': 'default', + }, + }, +} + + +colors_config = lambda: { + 'colors': { + 'c1': 1, + 'c2': 2, + }, + 'gradients': { + }, +} + + +colorscheme_config = lambda: { + 'groups': { + 'g': {'fg': 'c1', 'bg': 'c2', 'attr': []}, + } +} + + +theme_config = lambda: { + 'segment_data': { + 's': { + 'before': 'b', + }, + }, + 'segments': { + 'left': [ + { + 'type': 'string', + 'name': 's', + 'contents': 't', + 'highlight_group': ['g'], + }, + ], + 'right': [], + } +} + +top_theme_config = lambda: { + 'dividers': { + 'left': { + 'hard': '#>', + 'soft': '|>', + }, + 'right': { + 'hard': '<#', + 'soft': '<|', + }, + }, + 'spaces': 0, +} + + +main_tree = lambda: { + '1/config': root_config(), + '1/colors': colors_config(), + '1/colorschemes/default': colorscheme_config(), + '1/themes/test/default': theme_config(), + '1/themes/powerline': top_theme_config(), + '1/themes/other1': mdc(top_theme_config(), { + 'dividers': { + 'left': { + 'hard': '!>', + } + } + }), + '1/themes/other2': mdc(top_theme_config(), { + 'dividers': { + 'left': { + 'hard': '>>', + } + } + }), +} + + +def mkdir_recursive(directory): + if os.path.isdir(directory): + return + mkdir_recursive(os.path.dirname(directory)) + os.mkdir(directory) + + +class TestPowerline(Powerline): + def get_config_paths(self): + return tuple(sorted([ + os.path.join(CONFIG_DIR, d) + for d in os.listdir(CONFIG_DIR) + ])) + + +class WithConfigTree(object): + __slots__ = ('tree', 'p', 'p_kwargs') + + def __init__(self, tree, p_kwargs={'run_once': True}): + self.tree = tree + self.p = None + self.p_kwargs = p_kwargs + + def __enter__(self, *args): + os.mkdir(CONFIG_DIR) + for k, v in self.tree.items(): + fname = os.path.join(CONFIG_DIR, k) + '.json' + mkdir_recursive(os.path.dirname(fname)) + with open(fname, 'w') as F: + json.dump(v, F) + select_renderer(simpler_renderer=True) + self.p = TestPowerline( + ext='test', + renderer_module='tests.lib.config_mock', + **self.p_kwargs + ) + if os.environ.get('POWERLINE_RUN_LINT_DURING_TESTS'): + try: + check_call(['scripts/powerline-lint'] + reduce(add, ( + ['-p', d] for d in self.p.get_config_paths() + ))) + except: + self.__exit__() + raise + return self.p.__enter__(*args) + + def __exit__(self, *args): + try: + rmtree(CONFIG_DIR) + finally: + if self.p: + self.p.__exit__(*args) + + +class TestMerging(TestCase): + def assertRenderEqual(self, p, output, **kwargs): + self.assertEqual(p.render(**kwargs).replace(' ', ' '), output) + + def test_not_merged_config(self): + with WithConfigTree(main_tree()) as p: + self.assertRenderEqual(p, '{12} bt{2-}#>{--}') + + def test_root_config_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/config': { + 'common': { + 'default_top_theme': 'other1', + } + }, + })) as p: + self.assertRenderEqual(p, '{12} bt{2-}!>{--}') + with WithConfigTree(mdc(main_tree(), { + '2/config': { + 'common': { + 'default_top_theme': 'other1', + } + }, + '3/config': { + 'common': { + 'default_top_theme': 'other2', + } + }, + })) as p: + self.assertRenderEqual(p, '{12} bt{2-}>>{--}') + + def test_top_theme_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/themes/powerline': { + 'spaces': 1, + }, + '3/themes/powerline': { + 'dividers': { + 'left': { + 'hard': '>>', + } + } + }, + })) as p: + self.assertRenderEqual(p, '{12} bt {2-}>>{--}') + + def test_colors_config_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/colors': { + 'colors': { + 'c1': 3, + } + }, + })) as p: + self.assertRenderEqual(p, '{32} bt{2-}#>{--}') + with WithConfigTree(mdc(main_tree(), { + '2/colors': { + 'colors': { + 'c1': 3, + } + }, + '3/colors': { + 'colors': { + 'c1': 4, + } + }, + })) as p: + self.assertRenderEqual(p, '{42} bt{2-}#>{--}') + with WithConfigTree(mdc(main_tree(), { + '2/colors': { + 'colors': { + 'c1': 3, + } + }, + '3/colors': { + 'colors': { + 'c2': 4, + } + }, + })) as p: + self.assertRenderEqual(p, '{34} bt{4-}#>{--}') + + def test_colorschemes_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/colorschemes/default': { + 'groups': { + 'g': {'fg': 'c2', 'bg': 'c1', 'attr': []}, + } + }, + })) as p: + self.assertRenderEqual(p, '{21} bt{1-}#>{--}') + + def test_theme_merging(self): + with WithConfigTree(mdc(main_tree(), { + '2/themes/test/default': { + 'segment_data': { + 's': { + 'after': 'a', + } + } + }, + })) as p: + self.assertRenderEqual(p, '{12} bta{2-}#>{--}') + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_config_reload.py b/tests/test_config_reload.py new file mode 100644 index 00000000..3bcfc9e2 --- /dev/null +++ b/tests/test_config_reload.py @@ -0,0 +1,319 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from time import sleep +from copy import deepcopy +from functools import wraps + +from tests import TestCase +from tests.lib.config_mock import get_powerline, add_watcher_events + + +config = { + 'config': { + 'common': { + 'interval': 0, + 'watcher': 'test', + }, + 'ext': { + 'test': { + 'theme': 'default', + 'colorscheme': 'default', + }, + }, + }, + 'colors': { + 'colors': { + "col1": 1, + "col2": 2, + "col3": 3, + "col4": 4, + }, + 'gradients': { + }, + }, + 'colorschemes/test/default': { + 'groups': { + "str1": {"fg": "col1", "bg": "col2", "attr": ["bold"]}, + "str2": {"fg": "col3", "bg": "col4", "attr": ["underline"]}, + }, + }, + 'colorschemes/test/2': { + 'groups': { + "str1": {"fg": "col2", "bg": "col3", "attr": ["bold"]}, + "str2": {"fg": "col1", "bg": "col4", "attr": ["underline"]}, + }, + }, + 'themes/test/default': { + 'segments': { + "left": [ + { + "type": "string", + "contents": "s", + "highlight_group": ["str1"], + }, + { + "type": "string", + "contents": "g", + "highlight_group": ["str2"], + }, + ], + "right": [ + ], + }, + }, + 'themes/powerline': { + 'dividers': { + "left": { + "hard": ">>", + "soft": ">", + }, + "right": { + "hard": "<<", + "soft": "<", + }, + }, + 'spaces': 0, + }, + 'themes/other': { + 'dividers': { + "left": { + "hard": ">>", + "soft": ">", + }, + "right": { + "hard": "<<", + "soft": "<", + }, + }, + 'spaces': 1, + }, + 'themes/test/2': { + 'segments': { + "left": [ + { + "type": "string", + "contents": "t", + "highlight_group": ["str1"], + }, + { + "type": "string", + "contents": "b", + "highlight_group": ["str2"], + }, + ], + "right": [ + ], + }, + }, +} + + +def with_new_config(func): + @wraps(func) + def f(self): + return func(self, deepcopy(config)) + return f + + +class TestConfigReload(TestCase): + def assertAccessEvents(self, p, *args): + events = set() + for event in args: + if ':' not in event: + events.add('check:' + event) + events.add('load:' + event) + else: + events.add(event) + self.assertEqual(set(p._pop_events()), events) + + @with_new_config + def test_noreload(self, config): + with get_powerline(config, run_once=True) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + config['config']['common']['spaces'] = 1 + add_watcher_events(p, 'config', wait=False, interval=0.05) + # When running once thread should not start + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p) + self.assertEqual(p.logger._pop_msgs(), []) + + @with_new_config + def test_reload_main(self, config): + with get_powerline(config, run_once=False) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + + config['config']['common']['default_top_theme'] = 'other' + add_watcher_events(p, 'config') + p.render() + self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>>') + self.assertAccessEvents(p, 'config', 'themes/other', 'check:themes/test/__main__', 'themes/test/default') + self.assertEqual(p.logger._pop_msgs(), []) + + config['config']['ext']['test']['theme'] = 'nonexistent' + add_watcher_events(p, 'config') + self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>>') + self.assertAccessEvents(p, 'config', 'check:themes/test/nonexistent', 'themes/other', 'check:themes/test/__main__') + # It should normally handle file missing error + self.assertEqual(p.logger._pop_msgs(), [ + 'exception:test:powerline:Failed to load theme: themes/test/__main__', + 'exception:test:powerline:Failed to load theme: themes/test/nonexistent', + 'exception:test:powerline:Failed to create renderer: themes/test/nonexistent' + ]) + + config['config']['ext']['test']['theme'] = 'default' + add_watcher_events(p, 'config') + self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>>') + self.assertAccessEvents(p, 'config', 'themes/test/default', 'themes/other', 'check:themes/test/__main__') + self.assertEqual(p.logger._pop_msgs(), []) + + config['config']['ext']['test']['colorscheme'] = 'nonexistent' + add_watcher_events(p, 'config') + self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>>') + self.assertAccessEvents(p, 'config', 'check:colorschemes/nonexistent', 'check:colorschemes/test/__main__', 'check:colorschemes/test/nonexistent') + # It should normally handle file missing error + self.assertEqual(p.logger._pop_msgs(), [ + 'exception:test:powerline:Failed to load colorscheme: colorschemes/nonexistent', + 'exception:test:powerline:Failed to load colorscheme: colorschemes/test/__main__', + 'exception:test:powerline:Failed to load colorscheme: colorschemes/test/nonexistent', + 'exception:test:powerline:Failed to create renderer: colorschemes/test/nonexistent' + ]) + + config['config']['ext']['test']['colorscheme'] = '2' + add_watcher_events(p, 'config') + self.assertEqual(p.render(), '<2 3 1> s <3 4 False>>><1 4 4>g <4 False False>>>') + self.assertAccessEvents(p, 'config', 'check:colorschemes/2', 'check:colorschemes/test/__main__', 'colorschemes/test/2') + self.assertEqual(p.logger._pop_msgs(), []) + + config['config']['ext']['test']['theme'] = '2' + add_watcher_events(p, 'config') + self.assertEqual(p.render(), '<2 3 1> t <3 4 False>>><1 4 4>b <4 False False>>>') + self.assertAccessEvents(p, 'config', 'themes/test/2', 'themes/other', 'check:themes/test/__main__') + self.assertEqual(p.logger._pop_msgs(), []) + + self.assertEqual(p.renderer.local_themes, None) + config['config']['ext']['test']['local_themes'] = 'something' + add_watcher_events(p, 'config') + self.assertEqual(p.render(), '<2 3 1> t <3 4 False>>><1 4 4>b <4 False False>>>') + self.assertAccessEvents(p, 'config') + self.assertEqual(p.logger._pop_msgs(), []) + self.assertEqual(p.renderer.local_themes, 'something') + + @with_new_config + def test_reload_unexistent(self, config): + with get_powerline(config, run_once=False) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + + config['config']['ext']['test']['colorscheme'] = 'nonexistentraise' + add_watcher_events(p, 'config') + # It may appear that p.logger._pop_msgs() is called after given + # exception is added to the mesagges, but before config_loader + # exception was added (this one: + # “exception:test:config_loader:Error while running condition + # function for key colorschemes/test/nonexistentraise: + # fcf:colorschemes/test/nonexistentraise”). + # sleep(0.1) + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + # For colorschemes/{test/,}*raise find_config_file raises + # IOError, but it does not do so for check:colorschemes/test/__main__, + # so powerline is trying to load this, but not other + # colorschemes/* + self.assertAccessEvents(p, 'config', 'check:colorschemes/test/__main__', 'check:colorschemes/nonexistentraise', 'check:colorschemes/test/nonexistentraise') + self.assertIn('exception:test:powerline:Failed to create renderer: fcf:colorschemes/test/nonexistentraise', p.logger._pop_msgs()) + + config['colorschemes/nonexistentraise'] = {} + config['colorschemes/test/nonexistentraise'] = { + 'groups': { + "str1": {"fg": "col1", "bg": "col3", "attr": ["bold"]}, + "str2": {"fg": "col2", "bg": "col4", "attr": ["underline"]}, + }, + } + while not p._will_create_renderer(): + sleep(0.1) + self.assertEqual(p.render(), '<1 3 1> s<3 4 False>>><2 4 4>g<4 False False>>>') + # Same as above + self.assertAccessEvents(p, 'colorschemes/nonexistentraise', 'colorschemes/test/nonexistentraise', 'check:colorschemes/test/__main__') + self.assertEqual(p.logger._pop_msgs(), []) + + @with_new_config + def test_reload_colors(self, config): + with get_powerline(config, run_once=False) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + + config['colors']['colors']['col1'] = 5 + add_watcher_events(p, 'colors') + self.assertEqual(p.render(), '<5 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'colors') + self.assertEqual(p.logger._pop_msgs(), []) + + @with_new_config + def test_reload_colorscheme(self, config): + with get_powerline(config, run_once=False) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + + config['colorschemes/test/default']['groups']['str1']['bg'] = 'col3' + add_watcher_events(p, 'colorschemes/test/default') + self.assertEqual(p.render(), '<1 3 1> s<3 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default') + self.assertEqual(p.logger._pop_msgs(), []) + + @with_new_config + def test_reload_theme(self, config): + with get_powerline(config, run_once=False) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + + config['themes/test/default']['segments']['left'][0]['contents'] = 'col3' + add_watcher_events(p, 'themes/test/default') + self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + self.assertEqual(p.logger._pop_msgs(), []) + + @with_new_config + def test_reload_top_theme(self, config): + with get_powerline(config, run_once=False) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + + config['themes/powerline']['dividers']['left']['hard'] = '|>' + add_watcher_events(p, 'themes/powerline') + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>|><3 4 4>g<4 False False>|>') + self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + self.assertEqual(p.logger._pop_msgs(), []) + + @with_new_config + def test_reload_theme_main(self, config): + config['config']['common']['interval'] = None + with get_powerline(config, run_once=False) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + + config['themes/test/default']['segments']['left'][0]['contents'] = 'col3' + add_watcher_events(p, 'themes/test/default', wait=False) + self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + self.assertEqual(p.logger._pop_msgs(), []) + self.assertTrue(p._watcher._calls) + + @with_new_config + def test_run_once_no_theme_reload(self, config): + config['config']['common']['interval'] = None + with get_powerline(config, run_once=True) as p: + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__') + + config['themes/test/default']['segments']['left'][0]['contents'] = 'col3' + add_watcher_events(p, 'themes/test/default', wait=False) + self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>>') + self.assertAccessEvents(p) + self.assertEqual(p.logger._pop_msgs(), []) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 00000000..5172fb42 --- /dev/null +++ b/tests/test_configuration.py @@ -0,0 +1,698 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os + +from functools import wraps +from copy import deepcopy + +import tests.vim as vim_module + +from tests import TestCase +from tests.lib.config_mock import get_powerline, get_powerline_raw, swap_attributes +from tests.lib import Args, replace_item + + +def highlighted_string(s, group, **kwargs): + ret = { + 'type': 'string', + 'contents': s, + 'highlight_group': [group], + } + ret.update(kwargs) + return ret + + +config = { + 'config': { + 'common': { + 'interval': 0, + 'watcher': 'test', + }, + 'ext': { + 'test': { + 'theme': 'default', + 'colorscheme': 'default', + }, + 'vim': { + 'theme': 'default', + 'colorscheme': 'default', + }, + }, + }, + 'colors': { + 'colors': { + 'col1': 1, + 'col2': 2, + 'col3': 3, + 'col4': 4, + 'col5': 5, + 'col6': 6, + 'col7': 7, + 'col8': 8, + 'col9': 9, + 'col10': 10, + 'col11': 11, + 'col12': 12, + }, + 'gradients': { + }, + }, + 'colorschemes/test/__main__': { + 'groups': { + 'm1': 'g1', + 'm2': 'g3', + 'm3': {'fg': 'col11', 'bg': 'col12', 'attr': []}, + } + }, + 'colorschemes/default': { + 'groups': { + 'g1': {'fg': 'col5', 'bg': 'col6', 'attr': []}, + 'g2': {'fg': 'col7', 'bg': 'col8', 'attr': []}, + 'g3': {'fg': 'col9', 'bg': 'col10', 'attr': []}, + } + }, + 'colorschemes/test/default': { + 'groups': { + 'str1': {'fg': 'col1', 'bg': 'col2', 'attr': ['bold']}, + 'str2': {'fg': 'col3', 'bg': 'col4', 'attr': ['underline']}, + 'd1': 'g2', + 'd2': 'm2', + 'd3': 'm3', + }, + }, + 'colorschemes/vim/default': { + 'groups': { + 'environment': {'fg': 'col3', 'bg': 'col4', 'attr': ['underline']}, + }, + }, + 'themes/test/default': { + 'segments': { + 'left': [ + highlighted_string('s', 'str1', width='auto'), + highlighted_string('g', 'str2'), + ], + 'right': [ + highlighted_string('f', 'str2', width='auto', align='r'), + ], + }, + }, + 'themes/powerline': { + 'dividers': { + 'left': { + 'hard': '>>', + 'soft': '>', + }, + 'right': { + 'hard': '<<', + 'soft': '<', + }, + }, + 'spaces': 0, + }, + 'themes/test/__main__': { + 'dividers': { + 'right': { + 'soft': '|', + }, + }, + }, + 'themes/vim/default': { + 'default_module': 'powerline.segments.common', + 'segments': { + 'left': [ + { + 'function': 'environment', + 'args': { + 'variable': 'TEST', + }, + }, + ], + }, + }, +} + + +def with_new_config(func): + @wraps(func) + def f(self): + return func(self, deepcopy(config)) + return f + + +def add_args(func): + @wraps(func) + def f(self): + new_config = deepcopy(config) + with get_powerline(new_config, run_once=True, simpler_renderer=True) as p: + func(self, p, new_config) + return f + + +class TestRender(TestCase): + def assertRenderEqual(self, p, output, **kwargs): + self.assertEqual(p.render(**kwargs).replace(' ', ' '), output) + + def assertRenderLinesEqual(self, p, output, **kwargs): + self.assertEqual([l.replace(' ', ' ') for l in p.render_above_lines(**kwargs)], output) + + +class TestLines(TestRender): + @add_args + def test_without_above(self, p, config): + self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}|{344}f {--}') + self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}|{344}f {--}', width=10) + # self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}|{344} f {--}', width=11) + self.assertEqual(list(p.render_above_lines()), []) + + @with_new_config + def test_with_above(self, config): + old_segments = deepcopy(config['themes/test/default']['segments']) + config['themes/test/default']['segments']['above'] = [old_segments] + with get_powerline(config, run_once=True, simpler_renderer=True) as p: + self.assertRenderLinesEqual(p, [ + '{121} s{24}>>{344}g{34}>{34}|{344}f {--}', + ]) + self.assertRenderLinesEqual(p, [ + '{121} s {24}>>{344}g{34}>{34}|{344}f {--}', + ], width=10) + + config['themes/test/default']['segments']['above'] = [old_segments] * 2 + with get_powerline(config, run_once=True, simpler_renderer=True) as p: + self.assertRenderLinesEqual(p, [ + '{121} s{24}>>{344}g{34}>{34}|{344}f {--}', + '{121} s{24}>>{344}g{34}>{34}|{344}f {--}', + ]) + self.assertRenderLinesEqual(p, [ + '{121} s {24}>>{344}g{34}>{34}|{344}f {--}', + '{121} s {24}>>{344}g{34}>{34}|{344}f {--}', + ], width=10) + + +class TestSegments(TestRender): + @add_args + def test_display(self, p, config): + config['themes/test/default']['segments']['left'][0]['display'] = False + config['themes/test/default']['segments']['left'][1]['display'] = True + config['themes/test/default']['segments']['right'][0]['display'] = False + self.assertRenderEqual(p, '{344} g{4-}>>{--}') + + +class TestColorschemesHierarchy(TestRender): + @add_args + def test_group_redirects(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('a', 'd1', draw_hard_divider=False), + highlighted_string('b', 'd2', draw_hard_divider=False), + highlighted_string('c', 'd3', draw_hard_divider=False), + highlighted_string('A', 'm1', draw_hard_divider=False), + highlighted_string('B', 'm2', draw_hard_divider=False), + highlighted_string('C', 'm3', draw_hard_divider=False), + highlighted_string('1', 'g1', draw_hard_divider=False), + highlighted_string('2', 'g2', draw_hard_divider=False), + highlighted_string('3', 'g3', draw_hard_divider=False), + ], + 'right': [], + } + self.assertRenderEqual(p, '{78} a{910}b{1112}c{56}A{910}B{1112}C{56}1{78}2{910}3{--}') + self.assertEqual(p.logger._pop_msgs(), []) + + @add_args + def test_group_redirects_no_main(self, p, config): + del config['colorschemes/test/__main__'] + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('a', 'd1', draw_hard_divider=False), + highlighted_string('1', 'g1', draw_hard_divider=False), + highlighted_string('2', 'g2', draw_hard_divider=False), + highlighted_string('3', 'g3', draw_hard_divider=False), + ], + 'right': [], + } + self.assertRenderEqual(p, '{78} a{56}1{78}2{910}3{--}') + self.assertEqual(p.logger._pop_msgs(), []) + + @add_args + def test_group_redirects_no_top_default(self, p, config): + del config['colorschemes/default'] + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('c', 'd3', draw_soft_divider=False), + highlighted_string('C', 'm3', draw_hard_divider=False), + ], + 'right': [], + } + self.assertRenderEqual(p, '{1112} c{1112}C{--}') + self.assertEqual(p.logger._pop_msgs(), []) + + @add_args + def test_group_redirects_no_test_default(self, p, config): + del config['colorschemes/test/default'] + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('A', 'm1', draw_hard_divider=False), + highlighted_string('B', 'm2', draw_hard_divider=False), + highlighted_string('C', 'm3', draw_hard_divider=False), + highlighted_string('1', 'g1', draw_hard_divider=False), + highlighted_string('2', 'g2', draw_hard_divider=False), + highlighted_string('3', 'g3', draw_hard_divider=False), + ], + 'right': [], + } + self.assertRenderEqual(p, '{56} A{910}B{1112}C{56}1{78}2{910}3{--}') + self.assertEqual(p.logger._pop_msgs(), []) + + @add_args + def test_group_redirects_only_main(self, p, config): + del config['colorschemes/default'] + del config['colorschemes/test/default'] + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('C', 'm3', draw_hard_divider=False), + ], + 'right': [], + } + # Powerline is not able to work without default colorscheme + # somewhere, thus it will output error string + self.assertRenderEqual(p, 'colorschemes/test/default') + self.assertEqual(p.logger._pop_msgs(), [ + 'exception:test:powerline:Failed to load colorscheme: colorschemes/default', + 'exception:test:powerline:Failed to load colorscheme: colorschemes/test/default', + 'exception:test:powerline:Failed to create renderer: colorschemes/test/default', + 'exception:test:powerline:Failed to render: colorschemes/test/default', + ]) + + @add_args + def test_group_redirects_only_top_default(self, p, config): + del config['colorschemes/test/__main__'] + del config['colorschemes/test/default'] + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('1', 'g1', draw_hard_divider=False), + highlighted_string('2', 'g2', draw_hard_divider=False), + highlighted_string('3', 'g3', draw_hard_divider=False), + ], + 'right': [], + } + self.assertRenderEqual(p, '{56} 1{78}2{910}3{--}') + self.assertEqual(p.logger._pop_msgs(), []) + + @add_args + def test_group_redirects_only_test_default(self, p, config): + del config['colorschemes/default'] + del config['colorschemes/test/__main__'] + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s', 'str1', draw_hard_divider=False), + ], + 'right': [], + } + self.assertRenderEqual(p, '{121} s{--}') + self.assertEqual(p.logger._pop_msgs(), []) + + +class TestThemeHierarchy(TestRender): + @add_args + def test_hierarchy(self, p, config): + self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}|{344}f {--}') + + @add_args + def test_no_main(self, p, config): + del config['themes/test/__main__'] + self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}<{344}f {--}') + self.assertEqual(p.logger._pop_msgs(), []) + + @add_args + def test_no_powerline(self, p, config): + config['themes/test/__main__']['dividers'] = config['themes/powerline']['dividers'] + config['themes/test/__main__']['spaces'] = 1 + del config['themes/powerline'] + self.assertRenderEqual(p, '{121} s {24}>>{344}g {34}>{34}<{344} f {--}') + self.assertEqual(p.logger._pop_msgs(), []) + + @add_args + def test_no_default(self, p, config): + del config['themes/test/default'] + self.assertRenderEqual(p, 'themes/test/default') + self.assertEqual(p.logger._pop_msgs(), [ + 'exception:test:powerline:Failed to load theme: themes/test/default', + 'exception:test:powerline:Failed to create renderer: themes/test/default', + 'exception:test:powerline:Failed to render: themes/test/default', + ]) + + @add_args + def test_only_default(self, p, config): + config['themes/test/default']['dividers'] = config['themes/powerline']['dividers'] + config['themes/test/default']['spaces'] = 1 + del config['themes/test/__main__'] + del config['themes/powerline'] + self.assertRenderEqual(p, '{121} s {24}>>{344}g {34}>{34}<{344} f {--}') + + @add_args + def test_only_main(self, p, config): + del config['themes/test/default'] + del config['themes/powerline'] + self.assertRenderEqual(p, 'themes/test/default') + self.assertEqual(p.logger._pop_msgs(), [ + 'exception:test:powerline:Failed to load theme: themes/powerline', + 'exception:test:powerline:Failed to load theme: themes/test/default', + 'exception:test:powerline:Failed to create renderer: themes/test/default', + 'exception:test:powerline:Failed to render: themes/test/default', + ]) + + @add_args + def test_only_powerline(self, p, config): + del config['themes/test/default'] + del config['themes/test/__main__'] + self.assertRenderEqual(p, 'themes/test/default') + self.assertEqual(p.logger._pop_msgs(), [ + 'exception:test:powerline:Failed to load theme: themes/test/__main__', + 'exception:test:powerline:Failed to load theme: themes/test/default', + 'exception:test:powerline:Failed to create renderer: themes/test/default', + 'exception:test:powerline:Failed to render: themes/test/default', + ]) + + @add_args + def test_nothing(self, p, config): + del config['themes/test/default'] + del config['themes/powerline'] + del config['themes/test/__main__'] + self.assertRenderEqual(p, 'themes/test/default') + self.assertEqual(p.logger._pop_msgs(), [ + 'exception:test:powerline:Failed to load theme: themes/powerline', + 'exception:test:powerline:Failed to load theme: themes/test/__main__', + 'exception:test:powerline:Failed to load theme: themes/test/default', + 'exception:test:powerline:Failed to create renderer: themes/test/default', + 'exception:test:powerline:Failed to render: themes/test/default', + ]) + + +class TestDisplayCondition(TestRender): + @add_args + def test_include_modes(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', include_modes=['m1']), + highlighted_string('s2', 'g1', include_modes=['m1', 'm2']), + highlighted_string('s3', 'g1', include_modes=['m3']), + ] + } + self.assertRenderEqual(p, '{--}') + self.assertRenderEqual(p, '{56} s1{56}>{56}s2{6-}>>{--}', mode='m1') + self.assertRenderEqual(p, '{56} s2{6-}>>{--}', mode='m2') + self.assertRenderEqual(p, '{56} s3{6-}>>{--}', mode='m3') + + @add_args + def test_exclude_modes(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', exclude_modes=['m1']), + highlighted_string('s2', 'g1', exclude_modes=['m1', 'm2']), + highlighted_string('s3', 'g1', exclude_modes=['m3']), + ] + } + self.assertRenderEqual(p, '{56} s1{56}>{56}s2{56}>{56}s3{6-}>>{--}') + self.assertRenderEqual(p, '{56} s3{6-}>>{--}', mode='m1') + self.assertRenderEqual(p, '{56} s1{56}>{56}s3{6-}>>{--}', mode='m2') + self.assertRenderEqual(p, '{56} s1{56}>{56}s2{6-}>>{--}', mode='m3') + + @add_args + def test_exinclude_modes(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', exclude_modes=['m1'], include_modes=['m2']), + highlighted_string('s2', 'g1', exclude_modes=['m1', 'm2'], include_modes=['m3']), + highlighted_string('s3', 'g1', exclude_modes=['m3'], include_modes=['m3']), + ] + } + self.assertRenderEqual(p, '{--}') + self.assertRenderEqual(p, '{--}', mode='m1') + self.assertRenderEqual(p, '{56} s1{6-}>>{--}', mode='m2') + self.assertRenderEqual(p, '{56} s2{6-}>>{--}', mode='m3') + + @add_args + def test_exinclude_function_nonexistent_module(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', exclude_function='xxx_nonexistent_module.foo'), + highlighted_string('s2', 'g1', exclude_function='xxx_nonexistent_module.foo', include_function='xxx_nonexistent_module.bar'), + highlighted_string('s3', 'g1', include_function='xxx_nonexistent_module.bar'), + ] + } + self.assertRenderEqual(p, '{56} s1{56}>{56}s2{56}>{56}s3{6-}>>{--}') + + @add_args + def test_exinclude_function(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', exclude_function='mod.foo'), + highlighted_string('s2', 'g1', exclude_function='mod.foo', include_function='mod.bar'), + highlighted_string('s3', 'g1', include_function='mod.bar'), + ] + } + launched = set() + fool = [None] + barl = [None] + + def foo(*args, **kwargs): + launched.add('foo') + self.assertEqual(set(kwargs.keys()), set(('pl', 'segment_info', 'mode'))) + self.assertEqual(args, ()) + return fool[0] + + def bar(*args, **kwargs): + launched.add('bar') + self.assertEqual(set(kwargs.keys()), set(('pl', 'segment_info', 'mode'))) + self.assertEqual(args, ()) + return barl[0] + + with replace_item(sys.modules, 'mod', Args(foo=foo, bar=bar)): + fool[0] = True + barl[0] = True + self.assertRenderEqual(p, '{56} s3{6-}>>{--}') + self.assertEqual(launched, set(('foo', 'bar'))) + + fool[0] = False + barl[0] = True + self.assertRenderEqual(p, '{56} s1{56}>{56}s2{56}>{56}s3{6-}>>{--}') + self.assertEqual(launched, set(('foo', 'bar'))) + + fool[0] = False + barl[0] = False + self.assertRenderEqual(p, '{56} s1{6-}>>{--}') + self.assertEqual(launched, set(('foo', 'bar'))) + + fool[0] = True + barl[0] = False + self.assertRenderEqual(p, '{--}') + self.assertEqual(launched, set(('foo', 'bar'))) + + @add_args + def test_exinclude_modes_override_functions(self, p, config): + config['themes/test/default']['segments'] = { + 'left': [ + highlighted_string('s1', 'g1', exclude_function='mod.foo', exclude_modes=['m2']), + highlighted_string('s2', 'g1', exclude_function='mod.foo', include_modes=['m2']), + highlighted_string('s3', 'g1', include_function='mod.foo', exclude_modes=['m2']), + highlighted_string('s4', 'g1', include_function='mod.foo', include_modes=['m2']), + ] + } + fool = [None] + + def foo(*args, **kwargs): + return fool[0] + + with replace_item(sys.modules, 'mod', Args(foo=foo)): + fool[0] = True + self.assertRenderEqual(p, '{56} s4{6-}>>{--}', mode='m2') + self.assertRenderEqual(p, '{56} s3{56}>{56}s4{6-}>>{--}', mode='m1') + + fool[0] = False + self.assertRenderEqual(p, '{56} s2{56}>{56}s4{6-}>>{--}', mode='m2') + self.assertRenderEqual(p, '{56} s1{6-}>>{--}', mode='m1') + + +class TestSegmentAttributes(TestRender): + @add_args + def test_no_attributes(self, p, config): + def m1(divider=',', **kwargs): + return divider.join(kwargs.keys()) + divider + config['themes/test/default']['segments'] = { + 'left': [ + { + 'function': 'bar.m1' + } + ] + } + with replace_item(sys.modules, 'bar', Args(m1=m1)): + self.assertRenderEqual(p, '{56} pl,{6-}>>{--}') + + @add_args + def test_segment_datas(self, p, config): + def m1(divider=',', **kwargs): + return divider.join(kwargs.keys()) + divider + m1.powerline_segment_datas = { + 'powerline': { + 'args': { + 'divider': ';' + } + }, + 'ascii': { + 'args': { + 'divider': '--' + } + } + } + config['themes/test/default']['segments'] = { + 'left': [ + { + 'function': 'bar.m1' + } + ] + } + with replace_item(sys.modules, 'bar', Args(m1=m1)): + self.assertRenderEqual(p, '{56} pl;{6-}>>{--}') + + @add_args + def test_expand(self, p, config): + def m1(divider=',', **kwargs): + return divider.join(kwargs.keys()) + divider + + def expand(pl, amount, segment, **kwargs): + return ('-' * amount) + segment['contents'] + + m1.expand = expand + config['themes/test/default']['segments'] = { + 'left': [ + { + 'function': 'bar.m1', + 'width': 'auto' + } + ] + } + with replace_item(sys.modules, 'bar', Args(m1=m1)): + self.assertRenderEqual(p, '{56} ----pl,{6-}>>{--}', width=10) + + @add_args + def test_truncate(self, p, config): + def m1(divider=',', **kwargs): + return divider.join(kwargs.keys()) + divider + + def truncate(pl, amount, segment, **kwargs): + return segment['contents'][:-amount] + + m1.truncate = truncate + config['themes/test/default']['segments'] = { + 'left': [ + { + 'function': 'bar.m1' + } + ] + } + with replace_item(sys.modules, 'bar', Args(m1=m1)): + self.assertRenderEqual(p, '{56} p{6-}>>{--}', width=4) + + +class TestSegmentData(TestRender): + @add_args + def test_segment_data(self, p, config): + def m1(**kwargs): + return 'S' + + def m2(**kwargs): + return 'S' + sys.modules['bar'] = Args(m1=m1, m2=m2) + config['themes/powerline']['segment_data'] = { + 'm1': { + 'before': '1' + }, + 'bar.m2': { + 'before': '2' + }, + 'n': { + 'before': '3' + }, + 'm2': { + 'before': '4' + }, + } + config['themes/test/default']['segments'] = { + 'left': [ + { + 'function': 'bar.m1' + }, + { + 'function': 'bar.m1', + 'name': 'n' + }, + { + 'function': 'bar.m2', + 'name': 'n' + }, + { + 'function': 'bar.m2' + } + ] + } + self.assertRenderEqual(p, '{56} 1S{56}>{56}3S{610}>>{910}3S{910}>{910}2S{10-}>>{--}') + + +class TestVim(TestCase): + def test_environ_update(self): + # Regression test: test that segment obtains environment from vim, not + # from os.environ. + from powerline.vim import VimPowerline + import powerline as powerline_module + import vim + vim.vars['powerline_config_paths'] = ['/'] + with swap_attributes(config, powerline_module): + with vim_module._with('environ', TEST='abc'): + with get_powerline_raw(config, VimPowerline) as powerline: + window = vim_module.current.window + window_id = 1 + winnr = window.number + self.assertEqual(powerline.render(window, window_id, winnr), '%#Pl_3_8404992_4_192_underline#\xa0abc%#Pl_4_192_NONE_None_NONE#>>') + vim_module._environ['TEST'] = 'def' + self.assertEqual(powerline.render(window, window_id, winnr), '%#Pl_3_8404992_4_192_underline#\xa0def%#Pl_4_192_NONE_None_NONE#>>') + + def test_local_themes(self): + # Regression test: VimPowerline.add_local_theme did not work properly. + from powerline.vim import VimPowerline + import powerline as powerline_module + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, VimPowerline) as powerline: + powerline.add_local_theme('tests.matchers.always_true', { + 'segment_data': { + 'foo': { + 'contents': '“bar”' + } + }, + 'segments': { + 'left': [ + { + 'type': 'string', + 'name': 'foo', + 'highlight_group': ['g1'] + } + ] + } + }) + window = vim_module.current.window + window_id = 1 + winnr = window.number + self.assertEqual(powerline.render(window, window_id, winnr), '%#Pl_5_12583104_6_32896_NONE#\xa0\u201cbar\u201d%#Pl_6_32896_NONE_None_NONE#>>') + + @classmethod + def setUpClass(cls): + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'path'))) + + @classmethod + def tearDownClass(cls): + sys.path.pop(0) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_lib.py b/tests/test_lib.py new file mode 100644 index 00000000..ef8dc737 --- /dev/null +++ b/tests/test_lib.py @@ -0,0 +1,596 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import threading +import os +import re +import shutil + +from time import sleep +from subprocess import call, PIPE + +from powerline.lib import mergedicts, add_divider_highlight_group, REMOVE_THIS_KEY +from powerline.lib.humanize_bytes import humanize_bytes +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 tests.lib import Pl +from tests import TestCase, SkipTest + + +try: + __import__('bzrlib') +except ImportError: + use_bzr = False +else: + use_bzr = True + +try: + __import__('mercurial') +except ImportError: + use_mercurial = False +else: + use_mercurial = True + + +GIT_REPO = 'git_repo' + os.environ.get('PYTHON', '') +HG_REPO = 'hg_repo' + os.environ.get('PYTHON', '') +BZR_REPO = 'bzr_repo' + os.environ.get('PYTHON', '') + + +def thread_number(): + return len(threading.enumerate()) + + +class TestThreaded(TestCase): + def test_threaded_segment(self): + log = [] + pl = Pl() + updates = [(None,)] + lock = threading.Lock() + event = threading.Event() + block_event = threading.Event() + + class TestSegment(ThreadedSegment): + interval = 10 + + def set_state(self, **kwargs): + event.clear() + log.append(('set_state', kwargs)) + return super(TestSegment, self).set_state(**kwargs) + + def update(self, update_value): + block_event.wait() + event.set() + # Make sleep first to prevent some race conditions + log.append(('update', update_value)) + with lock: + ret = updates[0] + if isinstance(ret, Exception): + raise ret + else: + return ret[0] + + def render(self, update, **kwargs): + log.append(('render', update, kwargs)) + if isinstance(update, Exception): + raise update + else: + return update + + # Non-threaded tests + segment = TestSegment() + block_event.set() + updates[0] = (None,) + self.assertEqual(segment(pl=pl), None) + self.assertEqual(thread_number(), 1) + self.assertEqual(log, [ + ('set_state', {}), + ('update', None), + ('render', None, {'pl': pl, 'update_first': True}), + ]) + log[:] = () + + segment = TestSegment() + block_event.set() + updates[0] = ('abc',) + self.assertEqual(segment(pl=pl), 'abc') + self.assertEqual(thread_number(), 1) + self.assertEqual(log, [ + ('set_state', {}), + ('update', None), + ('render', 'abc', {'pl': pl, 'update_first': True}), + ]) + log[:] = () + + segment = TestSegment() + block_event.set() + updates[0] = ('abc',) + self.assertEqual(segment(pl=pl, update_first=False), 'abc') + self.assertEqual(thread_number(), 1) + self.assertEqual(log, [ + ('set_state', {}), + ('update', None), + ('render', 'abc', {'pl': pl, 'update_first': False}), + ]) + log[:] = () + + segment = TestSegment() + block_event.set() + updates[0] = ValueError('abc') + self.assertEqual(segment(pl=pl), None) + self.assertEqual(thread_number(), 1) + self.assertEqual(len(pl.exceptions), 1) + self.assertEqual(log, [ + ('set_state', {}), + ('update', None), + ]) + log[:] = () + pl.exceptions[:] = () + + segment = TestSegment() + block_event.set() + updates[0] = (TypeError('def'),) + self.assertRaises(TypeError, segment, pl=pl) + self.assertEqual(thread_number(), 1) + self.assertEqual(log, [ + ('set_state', {}), + ('update', None), + ('render', updates[0][0], {'pl': pl, 'update_first': True}), + ]) + log[:] = () + + # Threaded tests + segment = TestSegment() + block_event.clear() + kwargs = {'pl': pl, 'update_first': False, 'other': 1} + with lock: + updates[0] = ('abc',) + segment.startup(**kwargs) + ret = segment(**kwargs) + self.assertEqual(thread_number(), 2) + block_event.set() + event.wait() + segment.shutdown_event.set() + segment.thread.join() + self.assertEqual(ret, None) + self.assertEqual(log, [ + ('set_state', {'update_first': False, 'other': 1}), + ('render', None, {'pl': pl, 'update_first': False, 'other': 1}), + ('update', None), + ]) + log[:] = () + + segment = TestSegment() + block_event.set() + kwargs = {'pl': pl, 'update_first': True, 'other': 1} + with lock: + updates[0] = ('def',) + segment.startup(**kwargs) + ret = segment(**kwargs) + self.assertEqual(thread_number(), 2) + segment.shutdown_event.set() + segment.thread.join() + self.assertEqual(ret, 'def') + self.assertEqual(log, [ + ('set_state', {'update_first': True, 'other': 1}), + ('update', None), + ('render', 'def', {'pl': pl, 'update_first': True, 'other': 1}), + ]) + log[:] = () + + segment = TestSegment() + block_event.set() + kwargs = {'pl': pl, 'update_first': True, 'interval': 0.2} + with lock: + updates[0] = ('abc',) + segment.startup(**kwargs) + start = monotonic() + ret1 = segment(**kwargs) + with lock: + updates[0] = ('def',) + self.assertEqual(thread_number(), 2) + sleep(0.5) + ret2 = segment(**kwargs) + segment.shutdown_event.set() + segment.thread.join() + end = monotonic() + duration = end - start + self.assertEqual(ret1, 'abc') + self.assertEqual(ret2, 'def') + self.assertEqual(log[:5], [ + ('set_state', {'update_first': True, 'interval': 0.2}), + ('update', None), + ('render', 'abc', {'pl': pl, 'update_first': True, 'interval': 0.2}), + ('update', 'abc'), + ('update', 'def'), + ]) + num_runs = len([e for e in log if e[0] == 'update']) + self.assertAlmostEqual(duration / 0.2, num_runs, delta=1) + log[:] = () + + segment = TestSegment() + block_event.set() + kwargs = {'pl': pl, 'update_first': True, 'interval': 0.2} + with lock: + updates[0] = ('ghi',) + segment.startup(**kwargs) + start = monotonic() + ret1 = segment(**kwargs) + with lock: + updates[0] = TypeError('jkl') + self.assertEqual(thread_number(), 2) + sleep(0.5) + ret2 = segment(**kwargs) + segment.shutdown_event.set() + segment.thread.join() + end = monotonic() + duration = end - start + self.assertEqual(ret1, 'ghi') + self.assertEqual(ret2, None) + self.assertEqual(log[:5], [ + ('set_state', {'update_first': True, 'interval': 0.2}), + ('update', None), + ('render', 'ghi', {'pl': pl, 'update_first': True, 'interval': 0.2}), + ('update', 'ghi'), + ('update', 'ghi'), + ]) + num_runs = len([e for e in log if e[0] == 'update']) + self.assertAlmostEqual(duration / 0.2, num_runs, delta=1) + self.assertEqual(num_runs - 1, len(pl.exceptions)) + log[:] = () + + def test_kw_threaded_segment(self): + log = [] + pl = Pl() + event = threading.Event() + + class TestSegment(KwThreadedSegment): + interval = 10 + + @staticmethod + def key(_key=(None,), **kwargs): + log.append(('key', _key, kwargs)) + return _key + + def compute_state(self, key): + event.set() + sleep(0.1) + log.append(('compute_state', key)) + ret = key + if isinstance(ret, Exception): + raise ret + else: + return ret[0] + + def render_one(self, state, **kwargs): + log.append(('render_one', state, kwargs)) + if isinstance(state, Exception): + raise state + else: + return state + + # Non-threaded tests + segment = TestSegment() + event.clear() + self.assertEqual(segment(pl=pl), None) + self.assertEqual(thread_number(), 1) + self.assertEqual(log, [ + ('key', (None,), {'pl': pl}), + ('compute_state', (None,)), + ('render_one', None, {'pl': pl}), + ]) + log[:] = () + + segment = TestSegment() + kwargs = {'pl': pl, '_key': ('abc',), 'update_first': False} + event.clear() + self.assertEqual(segment(**kwargs), 'abc') + kwargs.update(_key=('def',)) + self.assertEqual(segment(**kwargs), 'def') + self.assertEqual(thread_number(), 1) + self.assertEqual(log, [ + ('key', ('abc',), {'pl': pl}), + ('compute_state', ('abc',)), + ('render_one', 'abc', {'pl': pl, '_key': ('abc',)}), + ('key', ('def',), {'pl': pl}), + ('compute_state', ('def',)), + ('render_one', 'def', {'pl': pl, '_key': ('def',)}), + ]) + log[:] = () + + segment = TestSegment() + kwargs = {'pl': pl, '_key': ValueError('xyz'), 'update_first': False} + event.clear() + self.assertEqual(segment(**kwargs), None) + self.assertEqual(thread_number(), 1) + self.assertEqual(log, [ + ('key', kwargs['_key'], {'pl': pl}), + ('compute_state', kwargs['_key']), + ]) + log[:] = () + + segment = TestSegment() + kwargs = {'pl': pl, '_key': (ValueError('abc'),), 'update_first': False} + event.clear() + self.assertRaises(ValueError, segment, **kwargs) + self.assertEqual(thread_number(), 1) + self.assertEqual(log, [ + ('key', kwargs['_key'], {'pl': pl}), + ('compute_state', kwargs['_key']), + ('render_one', kwargs['_key'][0], {'pl': pl, '_key': kwargs['_key']}), + ]) + log[:] = () + + # Threaded tests + segment = TestSegment() + kwargs = {'pl': pl, 'update_first': False, '_key': ('_abc',)} + event.clear() + segment.startup(**kwargs) + ret = segment(**kwargs) + self.assertEqual(thread_number(), 2) + segment.shutdown_event.set() + segment.thread.join() + self.assertEqual(ret, None) + self.assertEqual(log[:2], [ + ('key', kwargs['_key'], {'pl': pl}), + ('render_one', None, {'pl': pl, '_key': kwargs['_key']}), + ]) + self.assertLessEqual(len(log), 3) + if len(log) > 2: + self.assertEqual(log[2], ('compute_state', kwargs['_key'])) + log[:] = () + + segment = TestSegment() + kwargs = {'pl': pl, 'update_first': True, '_key': ('_abc',)} + event.clear() + segment.startup(**kwargs) + ret1 = segment(**kwargs) + kwargs.update(_key=('_def',)) + ret2 = segment(**kwargs) + self.assertEqual(thread_number(), 2) + segment.shutdown_event.set() + segment.thread.join() + self.assertEqual(ret1, '_abc') + self.assertEqual(ret2, '_def') + self.assertEqual(log, [ + ('key', ('_abc',), {'pl': pl}), + ('compute_state', ('_abc',)), + ('render_one', '_abc', {'pl': pl, '_key': ('_abc',)}), + ('key', ('_def',), {'pl': pl}), + ('compute_state', ('_def',)), + ('render_one', '_def', {'pl': pl, '_key': ('_def',)}), + ]) + log[:] = () + + +class TestLib(TestCase): + def test_mergedicts(self): + d = {} + mergedicts(d, {'abc': {'def': 'ghi'}}) + self.assertEqual(d, {'abc': {'def': 'ghi'}}) + mergedicts(d, {'abc': {'def': {'ghi': 'jkl'}}}) + self.assertEqual(d, {'abc': {'def': {'ghi': 'jkl'}}}) + mergedicts(d, {}) + self.assertEqual(d, {'abc': {'def': {'ghi': 'jkl'}}}) + mergedicts(d, {'abc': {'mno': 'pqr'}}) + self.assertEqual(d, {'abc': {'def': {'ghi': 'jkl'}, 'mno': 'pqr'}}) + mergedicts(d, {'abc': {'def': REMOVE_THIS_KEY}}) + self.assertEqual(d, {'abc': {'mno': 'pqr'}}) + + def test_add_divider_highlight_group(self): + def decorated_function_name(**kwargs): + return str(kwargs) + func = add_divider_highlight_group('hl_group')(decorated_function_name) + self.assertEqual(func.__name__, 'decorated_function_name') + self.assertEqual(func(kw={}), [{'contents': repr({str('kw'): {}}), 'divider_highlight_group': 'hl_group'}]) + + def test_humanize_bytes(self): + self.assertEqual(humanize_bytes(0), '0 B') + self.assertEqual(humanize_bytes(1), '1 B') + self.assertEqual(humanize_bytes(1, suffix='bit'), '1 bit') + self.assertEqual(humanize_bytes(1000, si_prefix=True), '1 kB') + self.assertEqual(humanize_bytes(1024, si_prefix=True), '1 kB') + self.assertEqual(humanize_bytes(1000000000, si_prefix=True), '1.00 GB') + self.assertEqual(humanize_bytes(1000000000, si_prefix=False), '953.7 MiB') + + +class TestVCS(TestCase): + def do_branch_rename_test(self, repo, q): + st = monotonic() + while monotonic() - st < 1: + # Give inotify time to deliver events + ans = repo.branch() + if hasattr(q, '__call__'): + if q(ans): + break + else: + if ans == q: + break + sleep(0.01) + if hasattr(q, '__call__'): + self.assertTrue(q(ans)) + else: + self.assertEqual(ans, q) + + def test_git(self): + create_watcher = get_fallback_create_watcher() + repo = guess(path=GIT_REPO, create_watcher=create_watcher) + self.assertNotEqual(repo, None) + self.assertEqual(repo.branch(), 'master') + self.assertEqual(repo.status(), None) + self.assertEqual(repo.status('file'), None) + with open(os.path.join(GIT_REPO, 'file'), 'w') as f: + f.write('abc') + f.flush() + self.assertEqual(repo.status(), ' U') + self.assertEqual(repo.status('file'), '??') + call(['git', 'add', '.'], cwd=GIT_REPO) + self.assertEqual(repo.status(), ' I ') + self.assertEqual(repo.status('file'), 'A ') + f.write('def') + f.flush() + self.assertEqual(repo.status(), 'DI ') + self.assertEqual(repo.status('file'), 'AM') + os.remove(os.path.join(GIT_REPO, 'file')) + # Test changing branch + self.assertEqual(repo.branch(), 'master') + try: + call(['git', 'branch', 'branch1'], cwd=GIT_REPO) + call(['git', 'checkout', '-q', 'branch1'], cwd=GIT_REPO) + self.do_branch_rename_test(repo, 'branch1') + call(['git', 'branch', 'branch2'], cwd=GIT_REPO) + call(['git', 'checkout', '-q', 'branch2'], cwd=GIT_REPO) + self.do_branch_rename_test(repo, 'branch2') + call(['git', 'checkout', '-q', '--detach', 'branch1'], cwd=GIT_REPO) + self.do_branch_rename_test(repo, lambda b: re.match(br'^[a-f0-9]+$', b)) + finally: + call(['git', 'checkout', '-q', 'master'], cwd=GIT_REPO) + + def test_git_sym(self): + create_watcher = get_fallback_create_watcher() + dotgit = os.path.join(GIT_REPO, '.git') + spacegit = os.path.join(GIT_REPO, ' .git ') + os.rename(dotgit, spacegit) + try: + with open(dotgit, 'w') as F: + F.write('gitdir: .git \n') + gitdir = git_directory(GIT_REPO) + self.assertTrue(os.path.isdir(gitdir)) + self.assertEqual(gitdir, os.path.abspath(spacegit)) + repo = guess(path=GIT_REPO, create_watcher=create_watcher) + self.assertEqual(repo.branch(), 'master') + finally: + os.remove(dotgit) + os.rename(spacegit, dotgit) + + def test_mercurial(self): + if not use_mercurial: + raise SkipTest('Mercurial is not available') + create_watcher = get_fallback_create_watcher() + repo = guess(path=HG_REPO, create_watcher=create_watcher) + self.assertNotEqual(repo, None) + self.assertEqual(repo.branch(), 'default') + self.assertEqual(repo.status(), None) + with open(os.path.join(HG_REPO, 'file'), 'w') as f: + f.write('abc') + f.flush() + self.assertEqual(repo.status(), ' U') + self.assertEqual(repo.status('file'), 'U') + call(['hg', 'add', '.'], cwd=HG_REPO, stdout=PIPE) + self.assertEqual(repo.status(), 'D ') + self.assertEqual(repo.status('file'), 'A') + os.remove(os.path.join(HG_REPO, 'file')) + + def test_bzr(self): + if not use_bzr: + raise SkipTest('Bazaar is not available') + create_watcher = get_fallback_create_watcher() + repo = guess(path=BZR_REPO, create_watcher=create_watcher) + self.assertNotEqual(repo, None, 'No bzr repo found. Do you have bzr installed?') + self.assertEqual(repo.branch(), 'test_powerline') + self.assertEqual(repo.status(), None) + with open(os.path.join(BZR_REPO, 'file'), 'w') as f: + f.write('abc') + self.assertEqual(repo.status(), ' U') + self.assertEqual(repo.status('file'), '? ') + call(['bzr', 'add', '-q', '.'], cwd=BZR_REPO, stdout=PIPE) + self.assertEqual(repo.status(), 'D ') + self.assertEqual(repo.status('file'), '+N') + call(['bzr', 'commit', '-q', '-m', 'initial commit'], cwd=BZR_REPO) + self.assertEqual(repo.status(), None) + with open(os.path.join(BZR_REPO, 'file'), 'w') as f: + f.write('def') + self.assertEqual(repo.status(), 'D ') + self.assertEqual(repo.status('file'), ' M') + self.assertEqual(repo.status('notexist'), None) + with open(os.path.join(BZR_REPO, 'ignored'), 'w') as f: + f.write('abc') + self.assertEqual(repo.status('ignored'), '? ') + # Test changing the .bzrignore file should update status + with open(os.path.join(BZR_REPO, '.bzrignore'), 'w') as f: + f.write('ignored') + self.assertEqual(repo.status('ignored'), None) + # Test changing the dirstate file should invalidate the cache for + # all files in the repo + with open(os.path.join(BZR_REPO, 'file2'), 'w') as f: + f.write('abc') + call(['bzr', 'add', 'file2'], cwd=BZR_REPO, stdout=PIPE) + call(['bzr', 'commit', '-q', '-m', 'file2 added'], cwd=BZR_REPO) + with open(os.path.join(BZR_REPO, 'file'), 'a') as f: + f.write('hello') + with open(os.path.join(BZR_REPO, 'file2'), 'a') as f: + f.write('hello') + self.assertEqual(repo.status('file'), ' M') + self.assertEqual(repo.status('file2'), ' M') + call(['bzr', 'commit', '-q', '-m', 'multi'], cwd=BZR_REPO) + self.assertEqual(repo.status('file'), None) + self.assertEqual(repo.status('file2'), None) + + # Test changing branch + call(['bzr', 'nick', 'branch1'], cwd=BZR_REPO, stdout=PIPE, stderr=PIPE) + self.do_branch_rename_test(repo, 'branch1') + + # Test branch name/status changes when swapping repos + for x in ('b1', 'b2'): + d = os.path.join(BZR_REPO, x) + os.mkdir(d) + call(['bzr', 'init', '-q'], cwd=d) + call(['bzr', 'nick', '-q', x], cwd=d) + repo = guess(path=d, create_watcher=create_watcher) + self.assertEqual(repo.branch(), x) + self.assertFalse(repo.status()) + if x == 'b1': + open(os.path.join(d, 'dirty'), 'w').close() + self.assertTrue(repo.status()) + os.rename(os.path.join(BZR_REPO, 'b1'), os.path.join(BZR_REPO, 'b')) + os.rename(os.path.join(BZR_REPO, 'b2'), os.path.join(BZR_REPO, 'b1')) + os.rename(os.path.join(BZR_REPO, 'b'), os.path.join(BZR_REPO, 'b2')) + for x, y in (('b1', 'b2'), ('b2', 'b1')): + d = os.path.join(BZR_REPO, x) + repo = guess(path=d, create_watcher=create_watcher) + self.do_branch_rename_test(repo, y) + if x == 'b1': + self.assertFalse(repo.status()) + else: + self.assertTrue(repo.status()) + + @classmethod + def setUpClass(cls): + cls.powerline_old_cwd = os.getcwd() + os.chdir(os.path.dirname(__file__)) + call(['git', 'init', '--quiet', GIT_REPO]) + assert os.path.isdir(GIT_REPO) + call(['git', 'config', '--local', 'user.name', 'Foo'], cwd=GIT_REPO) + call(['git', 'config', '--local', 'user.email', 'bar@example.org'], cwd=GIT_REPO) + call(['git', 'commit', '--allow-empty', '--message', 'Initial commit', '--quiet'], cwd=GIT_REPO) + if use_mercurial: + cls.powerline_old_HGRCPATH = os.environ.get('HGRCPATH') + os.environ['HGRCPATH'] = '' + call(['hg', 'init', HG_REPO]) + with open(os.path.join(HG_REPO, '.hg', 'hgrc'), 'w') as hgrc: + hgrc.write('[ui]\n') + hgrc.write('username = Foo \n') + if use_bzr: + call(['bzr', 'init', '--quiet', BZR_REPO]) + call(['bzr', 'config', 'email=Foo '], cwd=BZR_REPO) + call(['bzr', 'config', 'nickname=test_powerline'], cwd=BZR_REPO) + call(['bzr', 'config', 'create_signatures=0'], cwd=BZR_REPO) + + @classmethod + def tearDownClass(cls): + for repo_dir in [GIT_REPO] + ([HG_REPO] if use_mercurial else []) + ([BZR_REPO] if use_bzr else []): + shutil.rmtree(repo_dir) + if use_mercurial: + if cls.powerline_old_HGRCPATH is None: + os.environ.pop('HGRCPATH') + else: + os.environ['HGRCPATH'] = cls.powerline_old_HGRCPATH + os.chdir(cls.powerline_old_cwd) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_lib_config.py b/tests/test_lib_config.py new file mode 100644 index 00000000..2b3db707 --- /dev/null +++ b/tests/test_lib_config.py @@ -0,0 +1,52 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from powerline.lib.config import ConfigLoader + +from tests import TestCase +from tests.lib.fsconfig import FSTree + + +FILE_ROOT = os.path.join(os.path.dirname(__file__), 'cfglib') + + +class LoadedList(list): + def pop_all(self): + try: + return self[:] + finally: + self[:] = () + + +loaded = LoadedList() + + +def on_load(key): + loaded.append(key) + + +def check_file(path): + if os.path.exists(path): + return path + else: + raise IOError + + +class TestLoaderCondition(TestCase): + def test_update_missing(self): + loader = ConfigLoader(run_once=True) + fpath = os.path.join(FILE_ROOT, 'file.json') + self.assertRaises(IOError, loader.load, fpath) + loader.register_missing(check_file, on_load, fpath) + loader.update() # This line must not raise IOError + with FSTree({'file': {'test': 1}}, root=FILE_ROOT): + loader.update() + self.assertEqual(loader.load(fpath), {'test': 1}) + self.assertEqual(loaded.pop_all(), [fpath]) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_local_overrides.vim b/tests/test_local_overrides.vim new file mode 100755 index 00000000..f6cf0931 --- /dev/null +++ b/tests/test_local_overrides.vim @@ -0,0 +1,48 @@ +#!/usr/bin/vim -S +set encoding=utf-8 +let g:powerline_config_paths = [expand(':p:h:h') . '/powerline/config_files'] +let g:powerline_config_overrides = {'common': {'default_top_theme': 'ascii'}} +let g:powerline_theme_overrides__default = {'segment_data': {'line_current_symbol': {'contents': 'LN '}, 'branch': {'before': 'B '}}} + +redir => g:messages + +try + python import powerline.vim + let pycmd = 'python' +catch + try + python3 import powerline.vim + let pycmd = 'python3' + catch + call writefile(['Unable to determine python version', v:exception], 'message.fail') + cquit + endtry +endtry + +try + execute pycmd 'powerline.vim.setup()' +catch + call writefile(['Failed to run setup function', v:exception], 'message.fail') + cquit +endtry + +try + let &columns = 80 + let result = eval(&statusline[2:]) +catch + call writefile(['Exception while evaluating &stl', v:exception], 'message.fail') + cquit +endtry + +if result isnot# '%#Pl_22_24320_148_11523840_bold# NORMAL %#Pl_148_11523840_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE# %#Pl_247_10395294_236_3158064_NONE#unix%#Pl_240_5789784_236_3158064_NONE# %#Pl_247_10329757_240_5789784_NONE# 100%%%#Pl_252_13684944_240_5789784_NONE# %#Pl_235_2500134_252_13684944_NONE# LN %#Pl_235_2500134_252_13684944_bold# 1%#Pl_22_24576_252_13684944_NONE#:1 ' + call writefile(['Unexpected result', result], 'message.fail') + cquit +endif + +redir END +if g:messages =~ '\S' + call writefile(['Non-empty messages:', g:messages], 'message.fail') + cquit +endif + +qall! diff --git a/tests/test_plugin_file.vim b/tests/test_plugin_file.vim new file mode 100755 index 00000000..e139c44f --- /dev/null +++ b/tests/test_plugin_file.vim @@ -0,0 +1,22 @@ +#!/usr/bin/vim -S +set encoding=utf-8 +let g:powerline_config_paths = [expand(':p:h:h') . '/powerline/config_files'] +tabedit abc +tabedit def +try + source powerline/bindings/vim/plugin/powerline.vim +catch + call writefile([v:exception], 'message.fail') + cquit +endtry +set ls=2 +redrawstatus! +redir =>mes + messages +redir END +let mess=split(mes, "\n") +if len(mess)>1 + call writefile(mess, 'message.fail') + cquit +endif +qall! diff --git a/tests/test_provided_config_files.py b/tests/test_provided_config_files.py new file mode 100644 index 00000000..381cd842 --- /dev/null +++ b/tests/test_provided_config_files.py @@ -0,0 +1,178 @@ +# vim:fileencoding=utf-8:noet + +'''Dynamic configuration files tests.''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os +import json + +import tests.vim as vim_module + +from tests.lib import Args, urllib_read, replace_attr +from tests import TestCase + + +VBLOCK = chr(ord('V') - 0x40) +SBLOCK = chr(ord('S') - 0x40) + + +class TestVimConfig(TestCase): + def test_vim(self): + from powerline.vim import VimPowerline + cfg_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'powerline', 'config_files') + buffers = ( + (('bufoptions',), {'buftype': 'help'}), + (('bufname', '[Command Line]'), {}), + (('bufoptions',), {'buftype': 'quickfix'}), + (('bufname', 'NERD_tree_1'), {}), + (('bufname', '__Gundo__'), {}), + (('bufname', '__Gundo_Preview__'), {}), + (('bufname', 'ControlP'), {}), + ) + with open(os.path.join(cfg_path, 'config.json'), 'r') as f: + local_themes_raw = json.load(f)['ext']['vim']['local_themes'] + # Don't run tests on external/plugin segments + local_themes = dict((k, v) for (k, v) in local_themes_raw.items()) + self.assertEqual(len(buffers), len(local_themes) - 1) + outputs = {} + i = 0 + + with vim_module._with('split'): + with VimPowerline() as powerline: + def check_output(mode, args, kwargs): + if mode == 'nc': + window = vim_module.windows[0] + window_id = 2 + else: + vim_module._start_mode(mode) + window = vim_module.current.window + window_id = 1 + winnr = window.number + out = powerline.render(window, window_id, winnr) + if out in outputs: + self.fail('Duplicate in set #{0} ({1}) for mode {2!r} (previously defined in set #{3} ({4!r}) for mode {5!r})'.format(i, (args, kwargs), mode, *outputs[out])) + outputs[out] = (i, (args, kwargs), mode) + + with vim_module._with('bufname', '/tmp/foo.txt'): + out = powerline.render(vim_module.current.window, 1, vim_module.current.window.number, is_tabline=True) + outputs[out] = (-1, (None, None), 'tab') + with vim_module._with('globals', powerline_config_paths=[cfg_path]): + exclude = set(('no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!')) + try: + for mode in ['n', 'nc', 'no', 'v', 'V', VBLOCK, 's', 'S', SBLOCK, 'i', 'R', 'Rv', 'c', 'cv', 'ce', 'r', 'rm', 'r?', '!']: + check_output(mode, None, None) + for args, kwargs in buffers: + i += 1 + if mode in exclude: + continue + if mode == 'nc' and args == ('bufname', 'ControlP'): + # ControlP window is not supposed to not + # be in the focus + continue + with vim_module._with(*args, **kwargs): + check_output(mode, args, kwargs) + finally: + vim_module._start_mode('n') + + @classmethod + def setUpClass(cls): + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'path'))) + + @classmethod + def tearDownClass(cls): + sys.path.pop(0) + + +class TestConfig(TestCase): + def test_tmux(self): + from powerline.segments import common + from imp import reload + reload(common) + from powerline.shell import ShellPowerline + with replace_attr(common, 'urllib_read', urllib_read): + with ShellPowerline(Args(ext=['tmux']), run_once=False) as powerline: + powerline.render() + with ShellPowerline(Args(ext=['tmux']), run_once=False) as powerline: + powerline.render() + + def test_zsh(self): + from powerline.shell import ShellPowerline + args = Args(last_pipe_status=[1, 0], jobnum=0, ext=['shell'], renderer_module='.zsh') + segment_info = {'args': args} + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=segment_info) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=segment_info) + segment_info['local_theme'] = 'select' + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=segment_info) + segment_info['local_theme'] = 'continuation' + segment_info['parser_state'] = 'if cmdsubst' + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info=segment_info) + + def test_bash(self): + from powerline.shell import ShellPowerline + args = Args(last_exit_code=1, jobnum=0, ext=['shell'], renderer_module='.bash', config={'ext': {'shell': {'theme': 'default_leftonly'}}}) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info={'args': args}) + with ShellPowerline(args, run_once=False) as powerline: + powerline.render(segment_info={'args': args}) + + def test_ipython(self): + from powerline.ipython import IPythonPowerline + + class IpyPowerline(IPythonPowerline): + paths = None + config_overrides = None + theme_overrides = {} + + segment_info = Args(prompt_count=1) + + with IpyPowerline() as powerline: + for prompt_type in ['in', 'in2']: + powerline.render(is_prompt=True, matcher_info=prompt_type, segment_info=segment_info) + powerline.render(is_prompt=True, matcher_info=prompt_type, segment_info=segment_info) + with IpyPowerline() as powerline: + for prompt_type in ['out', 'rewrite']: + powerline.render(is_prompt=False, matcher_info=prompt_type, segment_info=segment_info) + powerline.render(is_prompt=False, matcher_info=prompt_type, segment_info=segment_info) + + def test_wm(self): + from powerline.segments import common + from imp import reload + reload(common) + from powerline import Powerline + with replace_attr(common, 'urllib_read', urllib_read): + Powerline(ext='wm', renderer_module='pango_markup', run_once=True).render() + reload(common) + + +old_cwd = None +saved_get_config_paths = None + + +def setUpModule(): + global old_cwd + global saved_get_config_paths + import powerline + saved_get_config_paths = powerline.get_config_paths + path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'powerline', 'config_files') + powerline.get_config_paths = lambda: [path] + old_cwd = os.getcwd() + + +def tearDownModule(): + global old_cwd + global saved_get_config_paths + import powerline + powerline.get_config_paths = saved_get_config_paths + os.chdir(old_cwd) + old_cwd = None + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_segments.py b/tests/test_segments.py new file mode 100644 index 00000000..2cc6545b --- /dev/null +++ b/tests/test_segments.py @@ -0,0 +1,1131 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os + +from functools import partial + +from powerline.segments import shell, tmux, common +from powerline.lib.vcs import get_fallback_create_watcher + +import tests.vim as vim_module + +from tests.lib import Args, urllib_read, replace_attr, new_module, replace_module_module, replace_env, Pl +from tests import TestCase, SkipTest + + +def get_dummy_guess(**kwargs): + if 'directory' in kwargs: + def guess(path, create_watcher): + return Args(branch=lambda: os.path.basename(path), **kwargs) + else: + def guess(path, create_watcher): + return Args(branch=lambda: os.path.basename(path), directory=path, **kwargs) + return guess + + +class TestShell(TestCase): + def test_last_status(self): + pl = Pl() + segment_info = {'args': Args(last_exit_code=10)} + self.assertEqual(shell.last_status(pl=pl, segment_info=segment_info), [ + {'contents': '10', 'highlight_group': ['exit_fail']} + ]) + segment_info['args'].last_exit_code = 0 + self.assertEqual(shell.last_status(pl=pl, segment_info=segment_info), None) + segment_info['args'].last_exit_code = None + self.assertEqual(shell.last_status(pl=pl, segment_info=segment_info), None) + + def test_last_pipe_status(self): + pl = Pl() + segment_info = {'args': Args(last_pipe_status=[])} + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), None) + segment_info['args'].last_pipe_status = [0, 0, 0] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), None) + segment_info['args'].last_pipe_status = [0, 2, 0] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': '0', 'highlight_group': ['exit_success'], 'draw_inner_divider': True}, + {'contents': '2', 'highlight_group': ['exit_fail'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_group': ['exit_success'], 'draw_inner_divider': True} + ]) + + def test_jobnum(self): + pl = Pl() + segment_info = {'args': Args(jobnum=0)} + self.assertEqual(shell.jobnum(pl=pl, segment_info=segment_info), None) + self.assertEqual(shell.jobnum(pl=pl, segment_info=segment_info, show_zero=False), None) + self.assertEqual(shell.jobnum(pl=pl, segment_info=segment_info, show_zero=True), '0') + segment_info = {'args': Args(jobnum=1)} + self.assertEqual(shell.jobnum(pl=pl, segment_info=segment_info), '1') + self.assertEqual(shell.jobnum(pl=pl, segment_info=segment_info, show_zero=False), '1') + self.assertEqual(shell.jobnum(pl=pl, segment_info=segment_info, show_zero=True), '1') + + def test_continuation(self): + pl = Pl() + self.assertEqual(shell.continuation(pl=pl, segment_info={}), [{ + 'contents': '', + 'width': 'auto', + 'highlight_group': ['continuation:current', 'continuation'], + }]) + segment_info = {'parser_state': 'if cmdsubst'} + self.assertEqual(shell.continuation(pl=pl, segment_info=segment_info), [ + { + 'contents': 'if', + 'draw_inner_divider': True, + 'highlight_group': ['continuation:current', 'continuation'], + 'width': 'auto', + 'align': 'l', + }, + ]) + self.assertEqual(shell.continuation(pl=pl, segment_info=segment_info, right_align=True), [ + { + 'contents': 'if', + 'draw_inner_divider': True, + 'highlight_group': ['continuation:current', 'continuation'], + 'width': 'auto', + 'align': 'r', + }, + ]) + self.assertEqual(shell.continuation(pl=pl, segment_info=segment_info, omit_cmdsubst=False), [ + { + 'contents': 'if', + 'draw_inner_divider': True, + 'highlight_group': ['continuation'], + }, + { + 'contents': 'cmdsubst', + 'draw_inner_divider': True, + 'highlight_group': ['continuation:current', 'continuation'], + 'width': 'auto', + 'align': 'l', + }, + ]) + self.assertEqual(shell.continuation(pl=pl, segment_info=segment_info, omit_cmdsubst=False, right_align=True), [ + { + 'contents': 'if', + 'draw_inner_divider': True, + 'highlight_group': ['continuation'], + 'width': 'auto', + 'align': 'r', + }, + { + 'contents': 'cmdsubst', + 'draw_inner_divider': True, + 'highlight_group': ['continuation:current', 'continuation'], + }, + ]) + self.assertEqual(shell.continuation(pl=pl, segment_info=segment_info, omit_cmdsubst=True, right_align=True), [ + { + 'contents': 'if', + 'draw_inner_divider': True, + 'highlight_group': ['continuation:current', 'continuation'], + 'width': 'auto', + 'align': 'r', + }, + ]) + self.assertEqual(shell.continuation(pl=pl, segment_info=segment_info, omit_cmdsubst=True, right_align=True, renames={'if': 'IF'}), [ + { + 'contents': 'IF', + 'draw_inner_divider': True, + 'highlight_group': ['continuation:current', 'continuation'], + 'width': 'auto', + 'align': 'r', + }, + ]) + self.assertEqual(shell.continuation(pl=pl, segment_info=segment_info, omit_cmdsubst=True, right_align=True, renames={'if': None}), [ + { + 'contents': '', + 'highlight_group': ['continuation:current', 'continuation'], + 'width': 'auto', + 'align': 'r', + }, + ]) + segment_info = {'parser_state': 'then then then cmdsubst'} + self.assertEqual(shell.continuation(pl=pl, segment_info=segment_info), [ + { + 'contents': 'then', + 'draw_inner_divider': True, + 'highlight_group': ['continuation'], + }, + { + 'contents': 'then', + 'draw_inner_divider': True, + 'highlight_group': ['continuation'], + }, + { + 'contents': 'then', + 'draw_inner_divider': True, + 'highlight_group': ['continuation:current', 'continuation'], + 'width': 'auto', + 'align': 'l', + }, + ]) + + def test_cwd(self): + new_os = new_module('os', path=os.path, sep='/') + pl = Pl() + cwd = [None] + + def getcwd(): + wd = cwd[0] + if isinstance(wd, Exception): + raise wd + else: + return wd + + segment_info = {'getcwd': getcwd, 'home': None} + with replace_attr(shell, 'os', new_os): + cwd[0] = '/abc/def/ghi/foo/bar' + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'abc', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'def', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'ghi', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + segment_info['home'] = '/abc/def/ghi' + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info), [ + {'contents': '~', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + segment_info.update(shortened_path='~foo/ghi') + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info), [ + {'contents': '~foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'ghi', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, use_shortened_path=False), [ + {'contents': '~', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + segment_info.pop('shortened_path') + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=3), [ + {'contents': '~', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=3, shorten_home=False), [ + {'contents': '...', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'ghi', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1), [ + {'contents': '...', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, ellipsis='---'), [ + {'contents': '---', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, ellipsis=None), [ + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, use_path_separator=True), [ + {'contents': '.../', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, use_path_separator=True, ellipsis='---'), [ + {'contents': '---/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, use_path_separator=True, ellipsis=None), [ + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2), [ + {'contents': '~', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'fo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2, use_path_separator=True), [ + {'contents': '~/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'fo/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + cwd[0] = '/etc' + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, use_path_separator=False), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'etc', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, use_path_separator=True), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'etc', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + cwd[0] = '/' + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, use_path_separator=False), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, use_path_separator=True), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + ose = OSError() + ose.errno = 2 + cwd[0] = ose + self.assertEqual(shell.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2), [ + {'contents': '[not found]', 'divider_highlight_group': 'cwd:divider', 'highlight_group': ['cwd:current_folder', 'cwd'], 'draw_inner_divider': True} + ]) + cwd[0] = OSError() + self.assertRaises(OSError, shell.cwd, pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2) + cwd[0] = ValueError() + self.assertRaises(ValueError, shell.cwd, pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2) + + def test_date(self): + pl = Pl() + with replace_attr(common, 'datetime', Args(now=lambda: Args(strftime=lambda fmt: fmt))): + self.assertEqual(common.date(pl=pl), [{'contents': '%Y-%m-%d', 'highlight_group': ['date'], 'divider_highlight_group': None}]) + self.assertEqual(common.date(pl=pl, format='%H:%M', istime=True), [{'contents': '%H:%M', 'highlight_group': ['time', 'date'], 'divider_highlight_group': 'time:divider'}]) + + +class TestTmux(TestCase): + def test_attached_clients(self): + def get_tmux_output(cmd, *args): + if cmd == 'list-panes': + return 'session_name\n' + elif cmd == 'list-clients': + return '/dev/pts/2: 0 [191x51 xterm-256color] (utf8)\n/dev/pts/3: 0 [191x51 xterm-256color] (utf8)' + + pl = Pl() + with replace_attr(tmux, 'get_tmux_output', get_tmux_output): + self.assertEqual(tmux.attached_clients(pl=pl), '2') + self.assertEqual(tmux.attached_clients(pl=pl, minimum=3), None) + + +class TestCommon(TestCase): + def test_hostname(self): + pl = Pl() + with replace_env('SSH_CLIENT', '192.168.0.12 40921 22') as segment_info: + with replace_module_module(common, 'socket', gethostname=lambda: 'abc'): + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info), 'abc') + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True), 'abc') + with replace_module_module(common, 'socket', gethostname=lambda: 'abc.mydomain'): + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info), 'abc.mydomain') + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info, exclude_domain=True), 'abc') + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True), 'abc.mydomain') + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True, exclude_domain=True), 'abc') + segment_info['environ'].pop('SSH_CLIENT') + with replace_module_module(common, 'socket', gethostname=lambda: 'abc'): + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info), 'abc') + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True), None) + with replace_module_module(common, 'socket', gethostname=lambda: 'abc.mydomain'): + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info), 'abc.mydomain') + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info, exclude_domain=True), 'abc') + self.assertEqual(common.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True, exclude_domain=True), None) + + def test_user(self): + new_os = new_module('os', getpid=lambda: 1) + + class Process(object): + def __init__(self, pid): + pass + + def username(self): + return 'def' + + if hasattr(common, 'psutil') and not callable(common.psutil.Process.username): + username = property(username) + + new_psutil = new_module('psutil', Process=Process) + pl = Pl() + with replace_env('USER', 'def') as segment_info: + common.username = False + with replace_attr(common, 'os', new_os): + with replace_attr(common, 'psutil', new_psutil): + with replace_attr(common, '_geteuid', lambda: 5): + self.assertEqual(common.user(pl=pl, segment_info=segment_info), [ + {'contents': 'def', 'highlight_group': ['user']} + ]) + self.assertEqual(common.user(pl=pl, segment_info=segment_info, hide_user='abc'), [ + {'contents': 'def', 'highlight_group': ['user']} + ]) + self.assertEqual(common.user(pl=pl, segment_info=segment_info, hide_user='def'), None) + with replace_attr(common, '_geteuid', lambda: 0): + self.assertEqual(common.user(pl=pl, segment_info=segment_info), [ + {'contents': 'def', 'highlight_group': ['superuser', 'user']} + ]) + + def test_branch(self): + pl = Pl() + create_watcher = get_fallback_create_watcher() + segment_info = {'getcwd': os.getcwd} + branch = partial(common.branch, pl=pl, create_watcher=create_watcher) + with replace_attr(common, 'guess', get_dummy_guess(status=lambda: None, directory='/tmp/tests')): + with replace_attr(common, 'tree_status', lambda repo, pl: None): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [ + {'highlight_group': ['branch'], 'contents': 'tests'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [ + {'contents': 'tests', 'highlight_group': ['branch_clean', 'branch']} + ]) + with replace_attr(common, 'guess', get_dummy_guess(status=lambda: 'D ', directory='/tmp/tests')): + with replace_attr(common, 'tree_status', lambda repo, pl: 'D '): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [ + {'highlight_group': ['branch'], 'contents': 'tests'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [ + {'contents': 'tests', 'highlight_group': ['branch_dirty', 'branch']} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [ + {'highlight_group': ['branch'], 'contents': 'tests'} + ]) + with replace_attr(common, 'guess', lambda path, create_watcher: None): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), None) + + def test_cwd(self): + new_os = new_module('os', path=os.path, sep='/') + pl = Pl() + cwd = [None] + + def getcwd(): + wd = cwd[0] + if isinstance(wd, Exception): + raise wd + else: + return wd + + segment_info = {'getcwd': getcwd, 'home': None} + with replace_attr(common, 'os', new_os): + cwd[0] = '/abc/def/ghi/foo/bar' + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'abc', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'def', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'ghi', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + segment_info['home'] = '/abc/def/ghi' + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info), [ + {'contents': '~', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=3), [ + {'contents': '~', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=3, shorten_home=False), [ + {'contents': '...', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'ghi', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'foo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1), [ + {'contents': '...', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, ellipsis='---'), [ + {'contents': '---', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, ellipsis=None), [ + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, use_path_separator=True), [ + {'contents': '.../', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, use_path_separator=True, ellipsis='---'), [ + {'contents': '---/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=1, use_path_separator=True, ellipsis=None), [ + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2), [ + {'contents': '~', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'fo', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2, use_path_separator=True), [ + {'contents': '~/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'fo/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'bar', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']} + ]) + cwd[0] = '/etc' + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, use_path_separator=False), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True}, + {'contents': 'etc', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, use_path_separator=True), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False}, + {'contents': 'etc', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + cwd[0] = '/' + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, use_path_separator=False), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, use_path_separator=True), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_group': ['cwd:current_folder', 'cwd']}, + ]) + ose = OSError() + ose.errno = 2 + cwd[0] = ose + self.assertEqual(common.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2), [ + {'contents': '[not found]', 'divider_highlight_group': 'cwd:divider', 'highlight_group': ['cwd:current_folder', 'cwd'], 'draw_inner_divider': True} + ]) + cwd[0] = OSError() + self.assertRaises(OSError, common.cwd, pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2) + cwd[0] = ValueError() + self.assertRaises(ValueError, common.cwd, pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2) + + def test_date(self): + pl = Pl() + with replace_attr(common, 'datetime', Args(now=lambda: Args(strftime=lambda fmt: fmt))): + self.assertEqual(common.date(pl=pl), [{'contents': '%Y-%m-%d', 'highlight_group': ['date'], 'divider_highlight_group': None}]) + self.assertEqual(common.date(pl=pl, format='%H:%M', istime=True), [{'contents': '%H:%M', 'highlight_group': ['time', 'date'], 'divider_highlight_group': 'time:divider'}]) + + def test_fuzzy_time(self): + time = Args(hour=0, minute=45) + pl = Pl() + with replace_attr(common, 'datetime', Args(now=lambda: time)): + self.assertEqual(common.fuzzy_time(pl=pl), 'quarter to one') + time.hour = 23 + time.minute = 59 + self.assertEqual(common.fuzzy_time(pl=pl), 'round about midnight') + time.minute = 33 + self.assertEqual(common.fuzzy_time(pl=pl), 'twenty-five to twelve') + time.minute = 60 + self.assertEqual(common.fuzzy_time(pl=pl), 'twelve o\'clock') + time.minute = 33 + self.assertEqual(common.fuzzy_time(pl=pl, unicode_text=False), 'twenty-five to twelve') + time.minute = 60 + self.assertEqual(common.fuzzy_time(pl=pl, unicode_text=False), 'twelve o\'clock') + time.minute = 33 + self.assertEqual(common.fuzzy_time(pl=pl, unicode_text=True), 'twenty‐five to twelve') + time.minute = 60 + self.assertEqual(common.fuzzy_time(pl=pl, unicode_text=True), 'twelve o’clock') + + def test_external_ip(self): + pl = Pl() + with replace_attr(common, 'urllib_read', urllib_read): + self.assertEqual(common.external_ip(pl=pl), [{'contents': '127.0.0.1', 'divider_highlight_group': 'background:divider'}]) + + def test_uptime(self): + pl = Pl() + with replace_attr(common, '_get_uptime', lambda: 259200): + self.assertEqual(common.uptime(pl=pl), [{'contents': '3d', 'divider_highlight_group': 'background:divider'}]) + with replace_attr(common, '_get_uptime', lambda: 93784): + self.assertEqual(common.uptime(pl=pl), [{'contents': '1d 2h 3m', 'divider_highlight_group': 'background:divider'}]) + self.assertEqual(common.uptime(pl=pl, shorten_len=4), [{'contents': '1d 2h 3m 4s', 'divider_highlight_group': 'background:divider'}]) + with replace_attr(common, '_get_uptime', lambda: 65536): + self.assertEqual(common.uptime(pl=pl), [{'contents': '18h 12m 16s', 'divider_highlight_group': 'background:divider'}]) + self.assertEqual(common.uptime(pl=pl, shorten_len=2), [{'contents': '18h 12m', 'divider_highlight_group': 'background:divider'}]) + self.assertEqual(common.uptime(pl=pl, shorten_len=1), [{'contents': '18h', 'divider_highlight_group': 'background:divider'}]) + + def _get_uptime(): + raise NotImplementedError + + with replace_attr(common, '_get_uptime', _get_uptime): + self.assertEqual(common.uptime(pl=pl), None) + + def test_weather(self): + pl = Pl() + with replace_attr(common, 'urllib_read', urllib_read): + self.assertEqual(common.weather(pl=pl), [ + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 30.0} + ]) + self.assertEqual(common.weather(pl=pl, temp_coldest=0, temp_hottest=100), [ + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 0} + ]) + self.assertEqual(common.weather(pl=pl, temp_coldest=-100, temp_hottest=-50), [ + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 100} + ]) + self.assertEqual(common.weather(pl=pl, icons={'cloudy': 'o'}), [ + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'o '}, + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 30.0} + ]) + self.assertEqual(common.weather(pl=pl, icons={'partly_cloudy_day': 'x'}), [ + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'x '}, + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9°C', 'gradient_level': 30.0} + ]) + self.assertEqual(common.weather(pl=pl, unit='F'), [ + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '16°F', 'gradient_level': 30.0} + ]) + self.assertEqual(common.weather(pl=pl, unit='K'), [ + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '264K', 'gradient_level': 30.0} + ]) + self.assertEqual(common.weather(pl=pl, temp_format='{temp:.1e}C'), [ + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_condition_partly_cloudy_day', 'weather_condition_cloudy', 'weather_conditions', 'weather'], 'contents': 'CLOUDS '}, + {'divider_highlight_group': 'background:divider', 'highlight_group': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '-9.0e+00C', 'gradient_level': 30.0} + ]) + + def test_system_load(self): + pl = Pl() + with replace_module_module(common, 'os', getloadavg=lambda: (7.5, 3.5, 1.5)): + with replace_attr(common, '_cpu_count', lambda: 2): + self.assertEqual(common.system_load(pl=pl), [ + {'contents': '7.5 ', 'highlight_group': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + {'contents': '3.5 ', 'highlight_group': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 75.0}, + {'contents': '1.5', 'highlight_group': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 0} + ]) + self.assertEqual(common.system_load(pl=pl, format='{avg:.0f}', threshold_good=0, threshold_bad=1), [ + {'contents': '8 ', 'highlight_group': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + {'contents': '4 ', 'highlight_group': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + {'contents': '2', 'highlight_group': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 75.0} + ]) + + def test_cpu_load_percent(self): + pl = Pl() + with replace_module_module(common, 'psutil', cpu_percent=lambda **kwargs: 52.3): + self.assertEqual(common.cpu_load_percent(pl=pl), [{ + 'contents': '52%', + 'gradient_level': 52.3, + 'highlight_group': ['cpu_load_percent_gradient', 'cpu_load_percent'], + }]) + self.assertEqual(common.cpu_load_percent(pl=pl, format='{0:.1f}%'), [{ + 'contents': '52.3%', + 'gradient_level': 52.3, + 'highlight_group': ['cpu_load_percent_gradient', 'cpu_load_percent'], + }]) + + def test_network_load(self): + from time import sleep + + def gb(interface): + return None + + f = [gb] + + def _get_bytes(interface): + return f[0](interface) + + pl = Pl() + + with replace_attr(common, '_get_bytes', _get_bytes): + common.network_load.startup(pl=pl) + try: + self.assertEqual(common.network_load(pl=pl, interface='eth0'), None) + sleep(common.network_load.interval) + self.assertEqual(common.network_load(pl=pl, interface='eth0'), None) + while 'prev' not in common.network_load.interfaces.get('eth0', {}): + sleep(0.1) + self.assertEqual(common.network_load(pl=pl, interface='eth0'), None) + + l = [0, 0] + + def gb2(interface): + l[0] += 1200 + l[1] += 2400 + return tuple(l) + f[0] = gb2 + + while not common.network_load.interfaces.get('eth0', {}).get('prev', (None, None))[1]: + sleep(0.1) + self.assertEqual(common.network_load(pl=pl, interface='eth0'), [ + {'divider_highlight_group': 'background:divider', 'contents': 'DL 1 KiB/s', 'highlight_group': ['network_load_recv', 'network_load']}, + {'divider_highlight_group': 'background:divider', 'contents': 'UL 2 KiB/s', 'highlight_group': ['network_load_sent', 'network_load']}, + ]) + self.assertEqual(common.network_load(pl=pl, interface='eth0', recv_format='r {value}', sent_format='s {value}'), [ + {'divider_highlight_group': 'background:divider', 'contents': 'r 1 KiB/s', 'highlight_group': ['network_load_recv', 'network_load']}, + {'divider_highlight_group': 'background:divider', 'contents': 's 2 KiB/s', 'highlight_group': ['network_load_sent', 'network_load']}, + ]) + self.assertEqual(common.network_load(pl=pl, recv_format='r {value}', sent_format='s {value}', suffix='bps', interface='eth0'), [ + {'divider_highlight_group': 'background:divider', 'contents': 'r 1 Kibps', 'highlight_group': ['network_load_recv', 'network_load']}, + {'divider_highlight_group': 'background:divider', 'contents': 's 2 Kibps', 'highlight_group': ['network_load_sent', 'network_load']}, + ]) + self.assertEqual(common.network_load(pl=pl, recv_format='r {value}', sent_format='s {value}', si_prefix=True, interface='eth0'), [ + {'divider_highlight_group': 'background:divider', 'contents': 'r 1 kB/s', 'highlight_group': ['network_load_recv', 'network_load']}, + {'divider_highlight_group': 'background:divider', 'contents': 's 2 kB/s', 'highlight_group': ['network_load_sent', 'network_load']}, + ]) + self.assertEqual(common.network_load(pl=pl, recv_format='r {value}', sent_format='s {value}', recv_max=0, interface='eth0'), [ + {'divider_highlight_group': 'background:divider', 'contents': 'r 1 KiB/s', 'highlight_group': ['network_load_recv_gradient', 'network_load_gradient', 'network_load_recv', 'network_load'], 'gradient_level': 100}, + {'divider_highlight_group': 'background:divider', 'contents': 's 2 KiB/s', 'highlight_group': ['network_load_sent', 'network_load']}, + ]) + + class ApproxEqual(object): + def __eq__(self, i): + return abs(i - 50.0) < 1 + + self.assertEqual(common.network_load(pl=pl, recv_format='r {value}', sent_format='s {value}', sent_max=4800, interface='eth0'), [ + {'divider_highlight_group': 'background:divider', 'contents': 'r 1 KiB/s', 'highlight_group': ['network_load_recv', 'network_load']}, + {'divider_highlight_group': 'background:divider', 'contents': 's 2 KiB/s', 'highlight_group': ['network_load_sent_gradient', 'network_load_gradient', 'network_load_sent', 'network_load'], 'gradient_level': ApproxEqual()}, + ]) + finally: + common.network_load.shutdown() + + def test_virtualenv(self): + pl = Pl() + with replace_env('VIRTUAL_ENV', '/abc/def/ghi') as segment_info: + self.assertEqual(common.virtualenv(pl=pl, segment_info=segment_info), 'ghi') + segment_info['environ'].pop('VIRTUAL_ENV') + self.assertEqual(common.virtualenv(pl=pl, segment_info=segment_info), None) + + def test_environment(self): + pl = Pl() + variable = 'FOO' + value = 'bar' + with replace_env(variable, value) as segment_info: + self.assertEqual(common.environment(pl=pl, segment_info=segment_info, variable=variable), value) + segment_info['environ'].pop(variable) + self.assertEqual(common.environment(pl=pl, segment_info=segment_info, variable=variable), None) + + def test_email_imap_alert(self): + # TODO + pass + + def test_now_playing(self): + # TODO + pass + + def test_battery(self): + pl = Pl() + + def _get_capacity(pl): + return 86 + + with replace_attr(common, '_get_capacity', _get_capacity): + self.assertEqual(common.battery(pl=pl), [{ + 'contents': '86%', + 'highlight_group': ['battery_gradient', 'battery'], + 'gradient_level': 14, + }]) + self.assertEqual(common.battery(pl=pl, format='{capacity:.2f}'), [{ + 'contents': '0.86', + 'highlight_group': ['battery_gradient', 'battery'], + 'gradient_level': 14, + }]) + self.assertEqual(common.battery(pl=pl, steps=7), [{ + 'contents': '86%', + 'highlight_group': ['battery_gradient', 'battery'], + 'gradient_level': 14, + }]) + self.assertEqual(common.battery(pl=pl, gamify=True), [ + { + 'contents': 'OOOO', + 'draw_inner_divider': False, + 'highlight_group': ['battery_full', 'battery_gradient', 'battery'], + 'gradient_level': 0 + }, + { + 'contents': 'O', + 'draw_inner_divider': False, + 'highlight_group': ['battery_empty', 'battery_gradient', 'battery'], + 'gradient_level': 100 + } + ]) + self.assertEqual(common.battery(pl=pl, gamify=True, full_heart='+', empty_heart='-', steps='10'), [ + { + 'contents': '++++++++', + 'draw_inner_divider': False, + 'highlight_group': ['battery_full', 'battery_gradient', 'battery'], + 'gradient_level': 0 + }, + { + 'contents': '--', + 'draw_inner_divider': False, + 'highlight_group': ['battery_empty', 'battery_gradient', 'battery'], + 'gradient_level': 100 + } + ]) + + def test_internal_ip(self): + try: + import netifaces + except ImportError: + raise SkipTest() + pl = Pl() + addr = { + 'enp2s0': { + netifaces.AF_INET: [{'addr': '192.168.100.200'}], + netifaces.AF_INET6: [{'addr': 'feff::5446:5eff:fe5a:7777%enp2s0'}] + }, + 'lo': { + netifaces.AF_INET: [{'addr': '127.0.0.1'}], + netifaces.AF_INET6: [{'addr': '::1'}] + }, + 'teredo': { + netifaces.AF_INET6: [{'addr': 'feff::5446:5eff:fe5a:7777'}] + }, + } + interfaces = ['lo', 'enp2s0', 'teredo'] + with replace_module_module( + common, 'netifaces', + interfaces=(lambda: interfaces), + ifaddresses=(lambda interface: addr[interface]), + AF_INET=netifaces.AF_INET, + AF_INET6=netifaces.AF_INET6, + ): + self.assertEqual(common.internal_ip(pl=pl), '192.168.100.200') + self.assertEqual(common.internal_ip(pl=pl, interface='detect'), '192.168.100.200') + self.assertEqual(common.internal_ip(pl=pl, interface='lo'), '127.0.0.1') + self.assertEqual(common.internal_ip(pl=pl, interface='teredo'), None) + self.assertEqual(common.internal_ip(pl=pl, ipv=4), '192.168.100.200') + self.assertEqual(common.internal_ip(pl=pl, interface='detect', ipv=4), '192.168.100.200') + self.assertEqual(common.internal_ip(pl=pl, interface='lo', ipv=4), '127.0.0.1') + self.assertEqual(common.internal_ip(pl=pl, interface='teredo', ipv=4), None) + self.assertEqual(common.internal_ip(pl=pl, ipv=6), 'feff::5446:5eff:fe5a:7777%enp2s0') + self.assertEqual(common.internal_ip(pl=pl, interface='detect', ipv=6), 'feff::5446:5eff:fe5a:7777%enp2s0') + self.assertEqual(common.internal_ip(pl=pl, interface='lo', ipv=6), '::1') + self.assertEqual(common.internal_ip(pl=pl, interface='teredo', ipv=6), 'feff::5446:5eff:fe5a:7777') + interfaces[1:2] = () + self.assertEqual(common.internal_ip(pl=pl, ipv=6), 'feff::5446:5eff:fe5a:7777') + interfaces[1:2] = () + self.assertEqual(common.internal_ip(pl=pl, ipv=6), '::1') + interfaces[:] = () + self.assertEqual(common.internal_ip(pl=pl, ipv=6), None) + + +class TestVim(TestCase): + def test_mode(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.mode(pl=pl, segment_info=segment_info), 'NORMAL') + self.assertEqual(self.vim.mode(pl=pl, segment_info=segment_info, override={'i': 'INS'}), 'NORMAL') + self.assertEqual(self.vim.mode(pl=pl, segment_info=segment_info, override={'n': 'NORM'}), 'NORM') + with vim_module._with('mode', 'i') as segment_info: + self.assertEqual(self.vim.mode(pl=pl, segment_info=segment_info), 'INSERT') + with vim_module._with('mode', chr(ord('V') - 0x40)) as segment_info: + self.assertEqual(self.vim.mode(pl=pl, segment_info=segment_info), 'V-BLCK') + self.assertEqual(self.vim.mode(pl=pl, segment_info=segment_info, override={'^V': 'VBLK'}), 'VBLK') + + def test_visual_range(self): + pl = Pl() + vr = partial(self.vim.visual_range, pl=pl) + vim_module.current.window.cursor = [0, 0] + try: + with vim_module._with('mode', 'i') as segment_info: + self.assertEqual(vr(segment_info=segment_info), '') + with vim_module._with('mode', '^V') as segment_info: + self.assertEqual(vr(segment_info=segment_info), '1 x 1') + with vim_module._with('vpos', line=5, col=5, off=0): + self.assertEqual(vr(segment_info=segment_info), '5 x 5') + with vim_module._with('vpos', line=5, col=4, off=0): + self.assertEqual(vr(segment_info=segment_info), '5 x 4') + with vim_module._with('mode', '^S') as segment_info: + self.assertEqual(vr(segment_info=segment_info), '1 x 1') + with vim_module._with('vpos', line=5, col=5, off=0): + self.assertEqual(vr(segment_info=segment_info), '5 x 5') + with vim_module._with('vpos', line=5, col=4, off=0): + self.assertEqual(vr(segment_info=segment_info), '5 x 4') + with vim_module._with('mode', 'V') as segment_info: + self.assertEqual(vr(segment_info=segment_info), 'L:1') + with vim_module._with('vpos', line=5, col=5, off=0): + self.assertEqual(vr(segment_info=segment_info), 'L:5') + with vim_module._with('vpos', line=5, col=4, off=0): + self.assertEqual(vr(segment_info=segment_info), 'L:5') + with vim_module._with('mode', 'S') as segment_info: + self.assertEqual(vr(segment_info=segment_info), 'L:1') + with vim_module._with('vpos', line=5, col=5, off=0): + self.assertEqual(vr(segment_info=segment_info), 'L:5') + with vim_module._with('vpos', line=5, col=4, off=0): + self.assertEqual(vr(segment_info=segment_info), 'L:5') + with vim_module._with('mode', 'v') as segment_info: + self.assertEqual(vr(segment_info=segment_info), 'C:1') + with vim_module._with('vpos', line=5, col=5, off=0): + self.assertEqual(vr(segment_info=segment_info), 'L:5') + with vim_module._with('vpos', line=5, col=4, off=0): + self.assertEqual(vr(segment_info=segment_info), 'L:5') + with vim_module._with('mode', 's') as segment_info: + self.assertEqual(vr(segment_info=segment_info), 'C:1') + with vim_module._with('vpos', line=5, col=5, off=0): + self.assertEqual(vr(segment_info=segment_info), 'L:5') + with vim_module._with('vpos', line=5, col=4, off=0): + self.assertEqual(vr(segment_info=segment_info), 'L:5') + finally: + vim_module._close(1) + + def test_modified_indicator(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.modified_indicator(pl=pl, segment_info=segment_info), None) + segment_info['buffer'][0] = 'abc' + try: + self.assertEqual(self.vim.modified_indicator(pl=pl, segment_info=segment_info), '+') + self.assertEqual(self.vim.modified_indicator(pl=pl, segment_info=segment_info, text='-'), '-') + finally: + vim_module._bw(segment_info['bufnr']) + + def test_paste_indicator(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.paste_indicator(pl=pl, segment_info=segment_info), None) + with vim_module._with('options', paste=1): + self.assertEqual(self.vim.paste_indicator(pl=pl, segment_info=segment_info), 'PASTE') + self.assertEqual(self.vim.paste_indicator(pl=pl, segment_info=segment_info, text='P'), 'P') + + def test_readonly_indicator(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.readonly_indicator(pl=pl, segment_info=segment_info), None) + with vim_module._with('bufoptions', readonly=1): + self.assertEqual(self.vim.readonly_indicator(pl=pl, segment_info=segment_info), 'RO') + self.assertEqual(self.vim.readonly_indicator(pl=pl, segment_info=segment_info, text='L'), 'L') + + def test_file_scheme(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.file_scheme(pl=pl, segment_info=segment_info), None) + with vim_module._with('buffer', '/tmp/’’/abc') as segment_info: + self.assertEqual(self.vim.file_scheme(pl=pl, segment_info=segment_info), None) + with vim_module._with('buffer', 'zipfile:/tmp/abc.zip::abc/abc.vim') as segment_info: + self.assertEqual(self.vim.file_scheme(pl=pl, segment_info=segment_info), 'zipfile') + + def test_file_directory(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info), None) + with replace_env('HOME', '/home/foo', os.environ): + with vim_module._with('buffer', '/tmp/’’/abc') as segment_info: + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info), '/tmp/’’/') + with vim_module._with('buffer', b'/tmp/\xFF\xFF/abc') as segment_info: + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info), '/tmp//') + with vim_module._with('buffer', '/tmp/abc') as segment_info: + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info), '/tmp/') + os.environ['HOME'] = '/tmp' + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info), '~/') + with vim_module._with('buffer', 'zipfile:/tmp/abc.zip::abc/abc.vim') as segment_info: + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info, remove_scheme=False), 'zipfile:/tmp/abc.zip::abc/') + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info, remove_scheme=True), '/tmp/abc.zip::abc/') + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info), '/tmp/abc.zip::abc/') + os.environ['HOME'] = '/tmp' + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info, remove_scheme=False), 'zipfile:/tmp/abc.zip::abc/') + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info, remove_scheme=True), '/tmp/abc.zip::abc/') + self.assertEqual(self.vim.file_directory(pl=pl, segment_info=segment_info), '/tmp/abc.zip::abc/') + + def test_file_name(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.file_name(pl=pl, segment_info=segment_info), None) + self.assertEqual(self.vim.file_name(pl=pl, segment_info=segment_info, display_no_file=True), [ + {'contents': '[No file]', 'highlight_group': ['file_name_no_file', 'file_name']} + ]) + self.assertEqual(self.vim.file_name(pl=pl, segment_info=segment_info, display_no_file=True, no_file_text='X'), [ + {'contents': 'X', 'highlight_group': ['file_name_no_file', 'file_name']} + ]) + with vim_module._with('buffer', '/tmp/abc') as segment_info: + self.assertEqual(self.vim.file_name(pl=pl, segment_info=segment_info), 'abc') + with vim_module._with('buffer', '/tmp/’’') as segment_info: + self.assertEqual(self.vim.file_name(pl=pl, segment_info=segment_info), '’’') + with vim_module._with('buffer', b'/tmp/\xFF\xFF') as segment_info: + self.assertEqual(self.vim.file_name(pl=pl, segment_info=segment_info), '') + + def test_file_size(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.file_size(pl=pl, segment_info=segment_info), '0 B') + with vim_module._with('buffer', os.path.join(os.path.dirname(__file__), 'empty')) as segment_info: + self.assertEqual(self.vim.file_size(pl=pl, segment_info=segment_info), '0 B') + + def test_file_opts(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.file_format(pl=pl, segment_info=segment_info), [ + {'divider_highlight_group': 'background:divider', 'contents': 'unix'} + ]) + self.assertEqual(self.vim.file_encoding(pl=pl, segment_info=segment_info), [ + {'divider_highlight_group': 'background:divider', 'contents': 'utf-8'} + ]) + self.assertEqual(self.vim.file_type(pl=pl, segment_info=segment_info), None) + with vim_module._with('bufoptions', filetype='python'): + self.assertEqual(self.vim.file_type(pl=pl, segment_info=segment_info), [ + {'divider_highlight_group': 'background:divider', 'contents': 'python'} + ]) + + def test_window_title(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.window_title(pl=pl, segment_info=segment_info), None) + with vim_module._with('wvars', quickfix_title='Abc'): + self.assertEqual(self.vim.window_title(pl=pl, segment_info=segment_info), 'Abc') + + def test_line_percent(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + segment_info['buffer'][0:-1] = [str(i) for i in range(100)] + try: + self.assertEqual(self.vim.line_percent(pl=pl, segment_info=segment_info), '1') + vim_module._set_cursor(50, 0) + self.assertEqual(self.vim.line_percent(pl=pl, segment_info=segment_info), '50') + self.assertEqual(self.vim.line_percent(pl=pl, segment_info=segment_info, gradient=True), [ + {'contents': '50', 'highlight_group': ['line_percent_gradient', 'line_percent'], 'gradient_level': 50 * 100.0 / 101} + ]) + finally: + vim_module._bw(segment_info['bufnr']) + + def test_line_count(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + segment_info['buffer'][0:-1] = [str(i) for i in range(99)] + try: + self.assertEqual(self.vim.line_count(pl=pl, segment_info=segment_info), '100') + vim_module._set_cursor(50, 0) + self.assertEqual(self.vim.line_count(pl=pl, segment_info=segment_info), '100') + finally: + vim_module._bw(segment_info['bufnr']) + + def test_position(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + try: + segment_info['buffer'][0:-1] = [str(i) for i in range(99)] + vim_module._set_cursor(49, 0) + self.assertEqual(self.vim.position(pl=pl, segment_info=segment_info), '50%') + self.assertEqual(self.vim.position(pl=pl, segment_info=segment_info, gradient=True), [ + {'contents': '50%', 'highlight_group': ['position_gradient', 'position'], 'gradient_level': 50.0} + ]) + vim_module._set_cursor(0, 0) + self.assertEqual(self.vim.position(pl=pl, segment_info=segment_info), 'Top') + vim_module._set_cursor(97, 0) + self.assertEqual(self.vim.position(pl=pl, segment_info=segment_info, position_strings={'top': 'Comienzo', 'bottom': 'Final', 'all': 'Todo'}), 'Final') + segment_info['buffer'][0:-1] = [str(i) for i in range(2)] + vim_module._set_cursor(0, 0) + self.assertEqual(self.vim.position(pl=pl, segment_info=segment_info, position_strings={'top': 'Comienzo', 'bottom': 'Final', 'all': 'Todo'}), 'Todo') + self.assertEqual(self.vim.position(pl=pl, segment_info=segment_info, gradient=True), [ + {'contents': 'All', 'highlight_group': ['position_gradient', 'position'], 'gradient_level': 0.0} + ]) + finally: + vim_module._bw(segment_info['bufnr']) + + def test_cursor_current(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.line_current(pl=pl, segment_info=segment_info), '1') + self.assertEqual(self.vim.col_current(pl=pl, segment_info=segment_info), '1') + self.assertEqual(self.vim.virtcol_current(pl=pl, segment_info=segment_info), [{ + 'highlight_group': ['virtcol_current_gradient', 'virtcol_current', 'col_current'], 'contents': '1', 'gradient_level': 100.0 / 80, + }]) + self.assertEqual(self.vim.virtcol_current(pl=pl, segment_info=segment_info, gradient=False), [{ + 'highlight_group': ['virtcol_current', 'col_current'], 'contents': '1', + }]) + + def test_modified_buffers(self): + pl = Pl() + self.assertEqual(self.vim.modified_buffers(pl=pl), None) + + def test_branch(self): + pl = Pl() + create_watcher = get_fallback_create_watcher() + branch = partial(self.vim.branch, pl=pl, create_watcher=create_watcher) + with vim_module._with('buffer', '/foo') as segment_info: + with replace_attr(self.vim, 'guess', get_dummy_guess(status=lambda: None)): + with replace_attr(self.vim, 'tree_status', lambda repo, pl: None): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [ + {'divider_highlight_group': 'branch:divider', 'highlight_group': ['branch'], 'contents': 'foo'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [ + {'divider_highlight_group': 'branch:divider', 'highlight_group': ['branch_clean', 'branch'], 'contents': 'foo'} + ]) + with replace_attr(self.vim, 'guess', get_dummy_guess(status=lambda: 'DU')): + with replace_attr(self.vim, 'tree_status', lambda repo, pl: 'DU'): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [ + {'divider_highlight_group': 'branch:divider', 'highlight_group': ['branch'], 'contents': 'foo'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [ + {'divider_highlight_group': 'branch:divider', 'highlight_group': ['branch_dirty', 'branch'], 'contents': 'foo'} + ]) + + def test_file_vcs_status(self): + pl = Pl() + create_watcher = get_fallback_create_watcher() + file_vcs_status = partial(self.vim.file_vcs_status, pl=pl, create_watcher=create_watcher) + with vim_module._with('buffer', '/foo') as segment_info: + with replace_attr(self.vim, 'guess', get_dummy_guess(status=lambda file: 'M')): + self.assertEqual(file_vcs_status(segment_info=segment_info), [ + {'highlight_group': ['file_vcs_status_M', 'file_vcs_status'], 'contents': 'M'} + ]) + with replace_attr(self.vim, 'guess', get_dummy_guess(status=lambda file: None)): + self.assertEqual(file_vcs_status(segment_info=segment_info), None) + with vim_module._with('buffer', '/bar') as segment_info: + with vim_module._with('bufoptions', buftype='nofile'): + with replace_attr(self.vim, 'guess', get_dummy_guess(status=lambda file: 'M')): + self.assertEqual(file_vcs_status(segment_info=segment_info), None) + + def test_trailing_whitespace(self): + pl = Pl() + with vim_module._with('buffer', 'tws') as segment_info: + trailing_whitespace = partial(self.vim.trailing_whitespace, pl=pl, segment_info=segment_info) + self.assertEqual(trailing_whitespace(), None) + self.assertEqual(trailing_whitespace(), None) + vim_module.current.buffer[0] = ' ' + self.assertEqual(trailing_whitespace(), [{ + 'highlight_group': ['trailing_whitespace', 'warning'], + 'contents': '1', + }]) + self.assertEqual(trailing_whitespace(), [{ + 'highlight_group': ['trailing_whitespace', 'warning'], + 'contents': '1', + }]) + vim_module.current.buffer[0] = '' + self.assertEqual(trailing_whitespace(), None) + self.assertEqual(trailing_whitespace(), None) + + def test_tabnr(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.tabnr(pl=pl, segment_info=segment_info, show_current=True), '1') + self.assertEqual(self.vim.tabnr(pl=pl, segment_info=segment_info, show_current=False), None) + + def test_bufnr(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.bufnr(pl=pl, segment_info=segment_info, show_current=True), str(segment_info['bufnr'])) + self.assertEqual(self.vim.bufnr(pl=pl, segment_info=segment_info, show_current=False), None) + + def test_winnr(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.winnr(pl=pl, segment_info=segment_info, show_current=True), str(segment_info['winnr'])) + self.assertEqual(self.vim.winnr(pl=pl, segment_info=segment_info, show_current=False), None) + + def test_segment_info(self): + pl = Pl() + with vim_module._with('tabpage'): + with vim_module._with('buffer', '1') as segment_info: + self.assertEqual(self.vim.tab_modified_indicator(pl=pl, segment_info=segment_info), None) + vim_module.current.buffer[0] = ' ' + self.assertEqual(self.vim.tab_modified_indicator(pl=pl, segment_info=segment_info), [{ + 'contents': '+', + 'highlight_group': ['tab_modified_indicator', 'modified_indicator'], + }]) + vim_module._undo() + self.assertEqual(self.vim.tab_modified_indicator(pl=pl, segment_info=segment_info), None) + old_buffer = vim_module.current.buffer + vim_module._new('2') + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.tab_modified_indicator(pl=pl, segment_info=segment_info), None) + old_buffer[0] = ' ' + self.assertEqual(self.vim.modified_indicator(pl=pl, segment_info=segment_info), None) + self.assertEqual(self.vim.tab_modified_indicator(pl=pl, segment_info=segment_info), [{ + 'contents': '+', + 'highlight_group': ['tab_modified_indicator', 'modified_indicator'], + }]) + + @classmethod + def setUpClass(cls): + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'path'))) + from powerline.segments import vim + cls.vim = vim + + @classmethod + def tearDownClass(cls): + sys.path.pop(0) + + +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_selectors.py b/tests/test_selectors.py new file mode 100644 index 00000000..a127ae9e --- /dev/null +++ b/tests/test_selectors.py @@ -0,0 +1,36 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys + +from functools import partial + +import tests.vim as vim_module + +from tests.lib import Pl +from tests import TestCase + + +class TestVim(TestCase): + def test_single_tab(self): + pl = Pl() + single_tab = partial(self.vim.single_tab, pl=pl, segment_info=None, mode=None) + with vim_module._with('tabpage'): + self.assertEqual(single_tab(), False) + self.assertEqual(single_tab(), True) + + @classmethod + def setUpClass(cls): + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'path'))) + from powerline.selectors import vim + cls.vim = vim + + @classmethod + def tearDownClass(cls): + sys.path.pop(0) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/test_shells/bash.daemon.ok b/tests/test_shells/bash.daemon.ok new file mode 100644 index 00000000..e9132e60 --- /dev/null +++ b/tests/test_shells/bash.daemon.ok @@ -0,0 +1,29 @@ +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" + USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +  BRANCH  ⋯  tests  shell  3rd  echo ' +                                     abc +                                     def +                                     ' + +abc +def + +  BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  BRANCH  ⋯  shell  3rd  `echo`  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +  BRANCH $ABC⋯  shell  3rd  `echo` $ABCfalse diff --git a/tests/test_shells/bash.nodaemon.ok b/tests/test_shells/bash.nodaemon.ok new file mode 100644 index 00000000..09b21b5c --- /dev/null +++ b/tests/test_shells/bash.nodaemon.ok @@ -0,0 +1,29 @@ +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" + USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +  BRANCH  ⋯  tests  shell  3rd  echo ' +   abc +   def +   ' + +abc +def + +  BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  BRANCH  ⋯  shell  3rd  `echo`  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +  BRANCH $ABC⋯  shell  3rd  `echo` $ABCfalse diff --git a/tests/test_shells/bgscript.sh b/tests/test_shells/bgscript.sh new file mode 100755 index 00000000..71886e62 --- /dev/null +++ b/tests/test_shells/bgscript.sh @@ -0,0 +1,5 @@ +#!/bin/sh +echo $$ > pid +while true ; do + sleep 0.1s +done diff --git a/tests/test_shells/busybox.daemon.ok b/tests/test_shells/busybox.daemon.ok new file mode 100644 index 00000000..60939a71 --- /dev/null +++ b/tests/test_shells/busybox.daemon.ok @@ -0,0 +1,28 @@ +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" + USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +  BRANCH  ⋯  tests  shell  3rd  echo ' +                                     abc +                                     def +                                     ' + +abc +def + +  BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  BRANCH  ⋯  shell  3rd  `echo`  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +  BRANCH $ABC⋯  shell  3rd  `echo` $ABCfalse diff --git a/tests/test_shells/busybox.nodaemon.ok b/tests/test_shells/busybox.nodaemon.ok new file mode 100644 index 00000000..b3a90542 --- /dev/null +++ b/tests/test_shells/busybox.nodaemon.ok @@ -0,0 +1,28 @@ +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" + USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +  BRANCH  ⋯  tests  shell  3rd  echo ' +   abc +   def +   ' + +abc +def + +  BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  BRANCH  ⋯  shell  3rd  `echo`  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +  BRANCH $ABC⋯  shell  3rd  `echo` $ABCfalse diff --git a/tests/test_shells/dash.daemon.ok b/tests/test_shells/dash.daemon.ok new file mode 100644 index 00000000..93b364ed --- /dev/null +++ b/tests/test_shells/dash.daemon.ok @@ -0,0 +1,27 @@ +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1s +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1   USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +  BRANCH  ⋯  tests  shell  3rd  echo ' +                                     abc +                                     def +                                     ' + +abc +def + +  BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  BRANCH  ⋯  shell  3rd  `echo`  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +  BRANCH $ABC⋯  shell  3rd  `echo` $ABCfalse diff --git a/tests/test_shells/dash.nodaemon.ok b/tests/test_shells/dash.nodaemon.ok new file mode 100644 index 00000000..79770982 --- /dev/null +++ b/tests/test_shells/dash.nodaemon.ok @@ -0,0 +1,27 @@ +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1s +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1   USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +  BRANCH  ⋯  tests  shell  3rd  echo ' +   abc +   def +   ' + +abc +def + +  BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  BRANCH  ⋯  shell  3rd  `echo`  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +  BRANCH $ABC⋯  shell  3rd  `echo` $ABCfalse diff --git a/tests/test_shells/fish.ok b/tests/test_shells/fish.ok new file mode 100644 index 00000000..8364f6a9 --- /dev/null +++ b/tests/test_shells/fish.ok @@ -0,0 +1,35 @@ +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  ^[[32m   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  ^H   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  \[\]   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  %%   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  #[bold]   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  (echo)   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  $(echo)   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  `echo`   + USER  ⋯  shell  3rd  `echo`   +   BRANCH  +   BRANCH  +   BRANCH  + INSERT  USER  ⋯  shell  3rd  `echo`   +   BRANCH  + DEFAULT  USER  ⋯  shell  3rd  `echo`   +   BRANCH  + INSERT  USER  ⋯  shell  3rd  `echo`   +   BRANCH  + DEFAULT  USER  ⋯  shell  3rd  `echo`   +   BRANCH  + INSERT  USER  ⋯  shell  3rd  `echo`   +   BRANCH  +   BRANCH  + INSERT  USER  ⋯  shell  3rd  `echo`   +   BRANCH  +   BRANCH  +   BRANCH  diff --git a/tests/test_shells/input.bash b/tests/test_shells/input.bash new file mode 100644 index 00000000..d224e6e7 --- /dev/null +++ b/tests/test_shells/input.bash @@ -0,0 +1,30 @@ +export VIRTUAL_ENV= +source powerline/bindings/bash/powerline.sh +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.args.only_if_ssh=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -c ext.shell.theme=default_leftonly" +cd tests/shell/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1s +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +echo ' +abc +def +' +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +false +true is the last line +exit diff --git a/tests/test_shells/input.busybox b/tests/test_shells/input.busybox new file mode 100644 index 00000000..220d3b59 --- /dev/null +++ b/tests/test_shells/input.busybox @@ -0,0 +1,30 @@ +. powerline/bindings/shell/powerline.sh +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.args.only_if_ssh=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -c ext.shell.theme=default_leftonly" +export VIRTUAL_ENV= +cd tests/shell/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1s +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +echo ' +abc +def +' +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +false +true is the last line +exit diff --git a/tests/test_shells/input.dash b/tests/test_shells/input.dash new file mode 100644 index 00000000..220d3b59 --- /dev/null +++ b/tests/test_shells/input.dash @@ -0,0 +1,30 @@ +. powerline/bindings/shell/powerline.sh +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.args.only_if_ssh=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -c ext.shell.theme=default_leftonly" +export VIRTUAL_ENV= +cd tests/shell/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1s +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +echo ' +abc +def +' +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +false +true is the last line +exit diff --git a/tests/test_shells/input.fish b/tests/test_shells/input.fish new file mode 100644 index 00000000..d1d9d62e --- /dev/null +++ b/tests/test_shells/input.fish @@ -0,0 +1,30 @@ +set fish_function_path "$PWD/powerline/bindings/fish" $fish_function_path +while jobs | grep fish_update_completions + sleep 1 +end +powerline-setup +set POWERLINE_COMMAND "$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.args.only_if_ssh=false" +set POWERLINE_COMMAND "$POWERLINE_COMMAND -c ext.shell.theme=default_leftonly" +setenv VIRTUAL_ENV +cd tests/shell/3rd +cd .git +cd .. +setenv VIRTUAL_ENV "$HOME/.virtenvs/some-virtual-environment" +setenv VIRTUAL_ENV +bgscript.sh & waitpid.sh +false +kill (cat pid) ; sleep 1s +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +set POWERLINE_COMMAND "$POWERLINE_COMMAND -c ext.shell.theme=default" +set -g fish_key_bindings fish_vi_key_bindings +ii +false +true is the last line +exit diff --git a/tests/test_shells/input.ipython b/tests/test_shells/input.ipython new file mode 100644 index 00000000..23b80198 --- /dev/null +++ b/tests/test_shells/input.ipython @@ -0,0 +1,7 @@ +print ('cd ' + 'tests/shell/3rd') # Start of the test marker +bool 42 +bool 44 +class Test(object): +pass + +exit diff --git a/tests/test_shells/input.mksh b/tests/test_shells/input.mksh new file mode 100644 index 00000000..2da58cee --- /dev/null +++ b/tests/test_shells/input.mksh @@ -0,0 +1,31 @@ +. powerline/bindings/shell/powerline.sh +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.args.only_if_ssh=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -c ext.shell.theme=default_leftonly" +export VIRTUAL_ENV= +cd tests/shell/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1 +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +echo -n +echo ' +abc +def +' +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +false +true is the last line +exit diff --git a/tests/test_shells/input.tcsh b/tests/test_shells/input.tcsh new file mode 100644 index 00000000..a9d2cf30 --- /dev/null +++ b/tests/test_shells/input.tcsh @@ -0,0 +1,22 @@ +source powerline/bindings/tcsh/powerline.tcsh +set POWERLINE_COMMAND=$POWERLINE_COMMAND:q" -t default_leftonly.segment_data.hostname.args.only_if_ssh=false -c ext.shell.theme=default_leftonly" +unsetenv VIRTUAL_ENV +cd tests/shell/3rd +cd .git +cd .. +setenv VIRTUAL_ENV "/home/foo/.virtenvs/some-virtual-environment" +unsetenv VIRTUAL_ENV +bgscript.sh & waitpid.sh +false # Warning: currently tcsh bindings do not support job count +kill `cat pid` ; sleep 1s +cd $DIR1:q +cd ../$DIR2:q +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +false +true is the last line +exit diff --git a/tests/test_shells/input.zsh b/tests/test_shells/input.zsh new file mode 100644 index 00000000..b4fa6487 --- /dev/null +++ b/tests/test_shells/input.zsh @@ -0,0 +1,45 @@ +unsetopt promptsp notransientrprompt +setopt interactivecomments +# POWERLINE_CONFIG_PATH=$PWD/powerline/config_files +# POWERLINE_THEME_CONFIG=( default_leftonly.segment_data.hostname.args.only_if_ssh=false ) +# POWERLINE_CONFIG=( ext.shell.theme=default_leftonly ) +POWERLINE_NO_ZSH_ZPYTHON=1 # TODO: make tests work with zsh/zpython +source powerline/bindings/zsh/powerline.zsh +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.args.only_if_ssh=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -c ext.shell.theme=default_leftonly" +export VIRTUAL_ENV= +cd tests/shell/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1s +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +cd .. +POWERLINE_COMMAND="${POWERLINE_COMMAND//_leftonly}" ; bindkey -v + + +echo abc +false +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.segment_data.hostname.display=false" +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.segment_data.user.display=false" +select abc in def ghi jkl +do + echo $abc + break +done +1 +hash -d foo=$PWD:h ; cd . +POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.dividers.left.hard=\$ABC" +true +true is the last line +exit diff --git a/tests/test_shells/ipython.ok b/tests/test_shells/ipython.ok new file mode 100644 index 00000000..166604df --- /dev/null +++ b/tests/test_shells/ipython.ok @@ -0,0 +1,14 @@ + + In [2]  bool 42 + 2>  bool(42) + Out[2]  True + + In [3]  bool 44 + 3>  bool(44) + Out[3]  True + + In [4]  class Test(object): +   pass +   + + In [5]  exit diff --git a/tests/test_shells/ipython_home/profile_default/ipython_config.py b/tests/test_shells/ipython_home/profile_default/ipython_config.py new file mode 100644 index 00000000..9f6bb567 --- /dev/null +++ b/tests/test_shells/ipython_home/profile_default/ipython_config.py @@ -0,0 +1,19 @@ +import os +c = get_config() +c.InteractiveShellApp.extensions = ['powerline.bindings.ipython.post_0_11'] +c.TerminalInteractiveShell.autocall = 1 +c.Powerline.paths = [os.path.abspath('powerline/config_files')] +c.Powerline.theme_overrides = { + 'in': { + 'segment_data': { + 'virtualenv': { + 'display': False + } + } + } +} +c.Powerline.config_overrides = { + 'common': { + 'default_top_theme': 'ascii' + } +} diff --git a/tests/test_shells/mksh.daemon.ok b/tests/test_shells/mksh.daemon.ok new file mode 100644 index 00000000..be403a46 --- /dev/null +++ b/tests/test_shells/mksh.daemon.ok @@ -0,0 +1,31 @@ + +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1 +[1] + Terminated bash -c ... +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" + USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +  BRANCH  ⋯  tests  shell  3rd  echo -n +  BRANCH  ⋯  tests  shell  3rd  echo ' +                                     abc +                                     def +                                     ' + +abc +def + +  BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  BRANCH  ⋯  shell  3rd  `echo`  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +  BRANCH $ABC⋯  shell  3rd  `echo` $ABCfalse diff --git a/tests/test_shells/mksh.nodaemon.ok b/tests/test_shells/mksh.nodaemon.ok new file mode 100644 index 00000000..d152cde7 --- /dev/null +++ b/tests/test_shells/mksh.nodaemon.ok @@ -0,0 +1,31 @@ + +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1 +[1] + Terminated bash -c ... +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.hostname.display=false" + USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.segment_data.user.display=false" +  BRANCH  ⋯  tests  shell  3rd  echo -n +  BRANCH  ⋯  tests  shell  3rd  echo ' +   abc +   def +   ' + +abc +def + +  BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  BRANCH  ⋯  shell  3rd  `echo`  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default_leftonly.dividers.left.hard=\$ABC" +  BRANCH $ABC⋯  shell  3rd  `echo` $ABCfalse diff --git a/tests/test_shells/postproc.py b/tests/test_shells/postproc.py new file mode 100755 index 00000000..71e79f9e --- /dev/null +++ b/tests/test_shells/postproc.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import socket +import sys +import codecs + + +test_type = sys.argv[1] +test_client = sys.argv[2] +shell = sys.argv[3] +fname = os.path.join('tests', 'shell', '.'.join((shell, test_type, test_client, 'full.log'))) +new_fname = os.path.join('tests', 'shell', '.'.join((shell, test_type, test_client, 'log'))) +pid_fname = os.path.join('tests', 'shell', '3rd', 'pid') + + +try: + with open(pid_fname, 'r') as P: + pid = P.read().strip() +except IOError: + pid = None +hostname = socket.gethostname() +user = os.environ['USER'] + +with codecs.open(fname, 'r', encoding='utf-8') as R: + with codecs.open(new_fname, 'w', encoding='utf-8') as W: + found_cd = False + for line in (R if shell != 'fish' else R.read().split('\n')): + if not found_cd: + found_cd = ('cd tests/shell/3rd' in line) + continue + if 'true is the last line' in line: + break + line = line.translate({ + ord('\r'): None + }) + line = line.replace(hostname, 'HOSTNAME') + line = line.replace(user, 'USER') + if pid is not None: + line = line.replace(pid, 'PID') + if shell == 'fish': + res = '' + try: + while line.index('\033[0;'): + start = line.index('\033[0;') + end = line.index('\033[0m', start) + res += line[start:end + 4] + '\n' + line = line[end + 4:] + except ValueError: + pass + line = res + elif shell == 'tcsh': + try: + start = line.index('\033[0;') + end = line.index(' ', start) + line = line[start:end] + '\033[0m\n' + except ValueError: + line = '' + elif shell == 'mksh': + # Output is different in travis: on my machine I see full + # command, in travis it is truncated just after `true`. + if line.startswith('[1] + Terminated'): + line = '[1] + Terminated bash -c ...\n' + elif shell == 'dash': + # Position of this line is not stable: it may go both before and + # after the next line + if line.startswith('[1] + Terminated'): + continue + W.write(line) diff --git a/tests/test_shells/screenrc b/tests/test_shells/screenrc new file mode 100644 index 00000000..3ba7ba7f --- /dev/null +++ b/tests/test_shells/screenrc @@ -0,0 +1,3 @@ +width 1024 +height 1 +logfile "tests/shell/${SH}.${TEST_TYPE}.${TEST_CLIENT}.full.log" diff --git a/tests/test_shells/tcsh.ok b/tests/test_shells/tcsh.ok new file mode 100644 index 00000000..cf87b8f9 --- /dev/null +++ b/tests/test_shells/tcsh.ok @@ -0,0 +1,16 @@ +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1   +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  ^[[32m   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  ^H   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  \[\]   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  %%   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  #[bold]   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  (echo)   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  $(echo)   +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  `echo`   diff --git a/tests/test_shells/test.sh b/tests/test_shells/test.sh new file mode 100755 index 00000000..216ed4fc --- /dev/null +++ b/tests/test_shells/test.sh @@ -0,0 +1,362 @@ +#!/bin/sh +: ${PYTHON:=python} +FAILED=0 +if test "x$1" = "x--fast" ; then + FAST=1 + shift +fi +ONLY_SHELL="$1" +ONLY_TEST_TYPE="$2" +ONLY_TEST_CLIENT="$3" + +if ! test -z "$ONLY_SHELL$ONLY_TEST_TYPE$ONLY_TEST_CLIENT" ; then + FAST= +fi + +export PYTHON + +if test "x$ONLY_SHELL" = "x--help" ; then +cat << EOF +Usage: + $0 [[[ONLY_SHELL | ""] (ONLY_TEST_TYPE | "")] (ONLY_TEST_CLIENT | "")] + +ONLY_SHELL: execute only tests for given shell +ONLY_TEST_TYPE: execute only "daemon" or "nodaemon" tests +ONLY_TEST_CLIENT: use only given test client (one of C, python, render, shell) +EOF +exit 0 +fi + +check_screen_log() { + TEST_TYPE="$1" + TEST_CLIENT="$2" + SH="$3" + if test -e tests/test_shells/${SH}.${TEST_TYPE}.ok ; then + diff -a -u tests/test_shells/${SH}.${TEST_TYPE}.ok tests/shell/${SH}.${TEST_TYPE}.${TEST_CLIENT}.log + return $? + elif test -e tests/test_shells/${SH}.ok ; then + diff -a -u tests/test_shells/${SH}.ok tests/shell/${SH}.${TEST_TYPE}.${TEST_CLIENT}.log + return $? + else + cat tests/shell/${SH}.${TEST_TYPE}.${TEST_CLIENT}.log + return 1 + fi +} + +run() { + TEST_TYPE="$1" + shift + TEST_CLIENT="$1" + shift + SH="$1" + shift + local local_path="$PWD/tests/shell/path:$PWD/scripts" + if test "x$SH" = "xfish" ; then + local_path="${local_path}:/usr/bin:/bin" + fi + if test $TEST_TYPE = daemon ; then + local additional_prompts=1 + else + local additional_prompts= + fi + env -i \ + LANG=en_US.UTF-8 \ + PATH="$local_path" \ + TERM="${TERM}" \ + COLUMNS="${COLUMNS}" \ + LINES="${LINES}" \ + TEST_TYPE="${TEST_TYPE}" \ + TEST_CLIENT="${TEST_CLIENT}" \ + SH="${SH}" \ + DIR1="${DIR1}" \ + DIR2="${DIR2}" \ + XDG_CONFIG_HOME="$PWD/tests/shell/fish_home" \ + IPYTHONDIR="$PWD/tests/shell/ipython_home" \ + POWERLINE_SHELL_CONTINUATION=$additional_prompts \ + POWERLINE_SHELL_SELECT=$additional_prompts \ + POWERLINE_COMMAND="${POWERLINE_COMMAND} -p $PWD/powerline/config_files" \ + "$@" +} + +run_test() { + TEST_TYPE="$1" + shift + TEST_CLIENT="$1" + shift + SH="$1" + SESNAME="powerline-shell-test-${SH}-$$" + + run "${TEST_TYPE}" "${TEST_CLIENT}" "${SH}" \ + screen -L -c tests/test_shells/screenrc -d -m -S "$SESNAME" \ + "$@" + while ! screen -S "$SESNAME" -X readreg a tests/test_shells/input.$SH ; do + sleep 0.1s + done + # Wait for screen to initialize + sleep 1 + while ! screen -S "$SESNAME" -p 0 -X width 300 1 ; do + sleep 0.1s + done + if test "x${SH}" = "xdash" ; then + # If I do not use this hack for dash then output will look like + # + # command1 + # command2 + # … + # prompt1> prompt2> … + while read -r line ; do + screen -S "$SESNAME" -p 0 -X stuff "$line"$(printf '\r') + sleep 1 + done < tests/test_shells/input.$SH + else + screen -S "$SESNAME" -p 0 -X paste a + fi + # Wait for screen to exit (sending command to non-existing screen session + # fails; when launched instance exits corresponding session is deleted) + while screen -S "$SESNAME" -X blankerprg "" > /dev/null ; do + sleep 0.1s + done + ./tests/test_shells/postproc.py ${TEST_TYPE} ${TEST_CLIENT} ${SH} + rm -f tests/shell/3rd/pid + if ! check_screen_log ${TEST_TYPE} ${TEST_CLIENT} ${SH} ; then + echo '____________________________________________________________' + if test "x$POWERLINE_TEST_NO_CAT_V" != "x1" ; then + # Repeat the diff to make it better viewable in travis output + echo "Diff (cat -v):" + echo '============================================================' + check_screen_log ${TEST_TYPE} ${TEST_CLIENT} ${SH} | cat -v + echo '____________________________________________________________' + fi + echo "Failed ${SH}. Full output:" + echo '============================================================' + cat tests/shell/${SH}.${TEST_TYPE}.${TEST_CLIENT}.full.log + echo '____________________________________________________________' + if test "x$POWERLINE_TEST_NO_CAT_V" != "x1" ; then + echo "Full output (cat -v):" + echo '============================================================' + cat -v tests/shell/${SH}.${TEST_TYPE}.${TEST_CLIENT}.full.log + echo '____________________________________________________________' + fi + case ${SH} in + *ksh) + ${SH} -c 'echo ${KSH_VERSION}' + ;; + dash) + # ? + ;; + busybox) + busybox --help + ;; + *) + ${SH} --version + ;; + esac + if which dpkg >/dev/null ; then + dpkg -s ${SH} + fi + return 1 + fi + return 0 +} + +test -d tests/shell && rm -r tests/shell +mkdir tests/shell +git init tests/shell/3rd +git --git-dir=tests/shell/3rd/.git checkout -b BRANCH +export DIR1="" +export DIR2="" +mkdir tests/shell/3rd/"$DIR1" +mkdir tests/shell/3rd/"$DIR2" +mkdir tests/shell/3rd/'\[\]' +mkdir tests/shell/3rd/'%%' +mkdir tests/shell/3rd/'#[bold]' +mkdir tests/shell/3rd/'(echo)' +mkdir tests/shell/3rd/'$(echo)' +mkdir tests/shell/3rd/'`echo`' + +mkdir tests/shell/fish_home +mkdir tests/shell/fish_home/fish +mkdir tests/shell/fish_home/fish/generated_completions +cp -r tests/test_shells/ipython_home tests/shell + +mkdir tests/shell/path +ln -s "$(which "${PYTHON}")" tests/shell/path/python +ln -s "$(which screen)" tests/shell/path +ln -s "$(which env)" tests/shell/path +ln -s "$(which sleep)" tests/shell/path +ln -s "$(which cat)" tests/shell/path +ln -s "$(which false)" tests/shell/path +ln -s "$(which true)" tests/shell/path +ln -s "$(which kill)" tests/shell/path +ln -s "$(which echo)" tests/shell/path +ln -s "$(which which)" tests/shell/path +ln -s "$(which dirname)" tests/shell/path +ln -s "$(which wc)" tests/shell/path +ln -s "$(which stty)" tests/shell/path +ln -s "$(which cut)" tests/shell/path +ln -s "$(which bc)" tests/shell/path +ln -s "$(which expr)" tests/shell/path +ln -s "$(which mktemp)" tests/shell/path +ln -s "$(which grep)" tests/shell/path +ln -s "$(which sed)" tests/shell/path +ln -s "$(which rm)" tests/shell/path +ln -s ../../test_shells/bgscript.sh tests/shell/path +ln -s ../../test_shells/waitpid.sh tests/shell/path +if which socat ; then + ln -s "$(which socat)" tests/shell/path +fi +for pexe in powerline powerline-config ; do + if test -e scripts/$pexe ; then + ln -s "$PWD/scripts/$pexe" tests/shell/path + elif which $pexe ; then + ln -s "$(which $pexe)" tests/shell/path + else + echo "Executable $pexe was not found" + exit 1 + fi +done + +for exe in bash zsh busybox fish tcsh mksh dash ipython ; do + if which $exe >/dev/null ; then + ln -s "$(which $exe)" tests/shell/path + fi +done + +unset ENV + +export ADDRESS="powerline-ipc-test-$$" +export PYTHON +echo "Powerline address: $ADDRESS" + +if test -z "${ONLY_SHELL}" || test "x${ONLY_SHELL%sh}" != "x${ONLY_SHELL}" || test "x${ONLY_SHELL}" = xbusybox ; then + scripts/powerline-config shell command + + for TEST_TYPE in "daemon" "nodaemon" ; do + if test "x$ONLY_TEST_TYPE" != "x" && test "x$ONLY_TEST_TYPE" != "x$TEST_TYPE" ; then + continue + fi + if test x$FAST = x1 ; then + if test $TEST_TYPE = daemon ; then + VARIANTS=3 + else + VARIANTS=4 + fi + EXETEST="$(( ${RANDOM:-`date +%N | sed s/^0*//`} % $VARIANTS ))" + echo "Execute tests: $EXETEST" + fi + + if test $TEST_TYPE = daemon ; then + sh -c ' + echo $$ > tests/shell/daemon_pid + $PYTHON ./scripts/powerline-daemon -s$ADDRESS -f >tests/shell/daemon_log 2>&1 + ' & + fi + echo "> Testing $TEST_TYPE" + I=-1 + for POWERLINE_COMMAND in \ + $PWD/scripts/powerline \ + $PWD/scripts/powerline-render \ + $PWD/client/powerline.py \ + $PWD/client/powerline.sh + do + case "$POWERLINE_COMMAND" in + *powerline) TEST_CLIENT=C ;; + *powerline-render) TEST_CLIENT=render ;; + *powerline.py) TEST_CLIENT=python ;; + *powerline.sh) TEST_CLIENT=shell ;; + esac + if test "$TEST_CLIENT" = render && test "$TEST_TYPE" = daemon ; then + continue + fi + I="$(( I + 1 ))" + if test "$TEST_CLIENT" = "C" && ! test -x scripts/powerline ; then + if which powerline >/dev/null ; then + POWERLINE_COMMAND=powerline + else + continue + fi + fi + if test "$TEST_CLIENT" = "shell" && ! which socat >/dev/null ; then + continue + fi + if test "x$ONLY_TEST_CLIENT" != "x" && test "x$TEST_CLIENT" != "x$ONLY_TEST_CLIENT" ; then + continue + fi + POWERLINE_COMMAND="$POWERLINE_COMMAND --socket $ADDRESS" + export POWERLINE_COMMAND + echo ">> powerline command is ${POWERLINE_COMMAND:-empty}" + J=-1 + for TEST_COMMAND in \ + "bash --norc --noprofile -i" \ + "zsh -f -i" \ + "fish -i" \ + "tcsh -f -i" \ + "busybox ash -i" \ + "mksh -i" \ + "dash -i" + do + J="$(( J + 1 ))" + if test x$FAST = x1 ; then + if test $(( (I + J) % $VARIANTS )) -ne $EXETEST ; then + continue + fi + fi + SH="${TEST_COMMAND%% *}" + # dash tests are not stable, see #931 + if test x$FAST$SH = x1dash ; then + continue + fi + if test "x$ONLY_SHELL" != "x" && test "x$ONLY_SHELL" != "x$SH" ; then + continue + fi + if ! which $SH >/dev/null ; then + continue + fi + echo ">>> $(which $SH)" + if ! run_test $TEST_TYPE $TEST_CLIENT $TEST_COMMAND ; then + FAILED=1 + fi + done + done + if test $TEST_TYPE = daemon ; then + $PYTHON ./scripts/powerline-daemon -s$ADDRESS -k + wait $(cat tests/shell/daemon_pid) + if ! test -z "$(cat tests/shell/daemon_log)" ; then + echo '____________________________________________________________' + echo "Daemon log:" + echo '============================================================' + cat tests/shell/daemon_log + FAILED=1 + fi + fi + done +fi + +if ! $PYTHON scripts/powerline-daemon -s$ADDRESS > tests/shell/daemon_log_2 2>&1 ; then + echo "Daemon exited with status $?" + FAILED=1 +else + sleep 1 + $PYTHON scripts/powerline-daemon -s$ADDRESS -k +fi + +if ! test -z "$(cat tests/shell/daemon_log_2)" ; then + FAILED=1 + echo '____________________________________________________________' + echo "Daemon log (2nd):" + echo '============================================================' + cat tests/shell/daemon_log_2 + FAILED=1 +fi + +if test "x${ONLY_SHELL}" = "x" || test "x${ONLY_SHELL}" = "xipython" ; then + if which ipython >/dev/null ; then + echo "> $(which ipython)" + if ! run_test ipython ipython ipython ; then + FAILED=1 + fi + fi +fi + +test $FAILED -eq 0 && rm -r tests/shell +exit $FAILED diff --git a/tests/test_shells/waitpid.sh b/tests/test_shells/waitpid.sh new file mode 100755 index 00000000..8d98e216 --- /dev/null +++ b/tests/test_shells/waitpid.sh @@ -0,0 +1,4 @@ +#!/bin/sh +while ! test -e pid ; do + sleep 0.1s +done diff --git a/tests/test_shells/zsh.daemon.ok b/tests/test_shells/zsh.daemon.ok new file mode 100644 index 00000000..d0259c4a --- /dev/null +++ b/tests/test_shells/zsh.daemon.ok @@ -0,0 +1,38 @@ + +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1] + terminated bgscript.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  `echo`  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="${POWERLINE_COMMAND//_leftonly}" ; bindkey -v + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd   COMMND   HOSTNAME  USER  ⋯  tests  shell  3rd   + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd   + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd  echo abc +abc + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd  false + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.segment_data.hostname.display=false" + INSERT  USER  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.segment_data.user.display=false" + INSERT  ⋯  tests  shell  3rd  select abc in def ghi jkl + select                            do + select                             echo $abc + select                             break + select                            done +1) def 2) ghi 3) jkl +                   Select variant  1 +def + INSERT  ⋯  tests  shell  3rd  hash -d foo=$PWD:h ; cd . + INSERT  ~foo  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.dividers.left.hard=\$ABC" + INSERT $ABC~foo  3rd $ABCtrue diff --git a/tests/test_shells/zsh.nodaemon.ok b/tests/test_shells/zsh.nodaemon.ok new file mode 100644 index 00000000..f3edbcdc --- /dev/null +++ b/tests/test_shells/zsh.nodaemon.ok @@ -0,0 +1,38 @@ + +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  ⓔ  some-virtual-environment   BRANCH  ⋯  tests  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1] + terminated bgscript.sh +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  cd "$DIR1" +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  ^[[32m  cd ../"$DIR2" +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  ^H  cd ../'\[\]' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  \[\]  cd ../'%%' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  %%  cd ../'#[bold]' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  #[bold]  cd ../'(echo)' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  (echo)  cd ../'$(echo)' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  $(echo)  cd ../'`echo`' +  HOSTNAME  USER   BRANCH  ⋯  shell  3rd  `echo`  cd .. +  HOSTNAME  USER   BRANCH  ⋯  tests  shell  3rd  POWERLINE_COMMAND="${POWERLINE_COMMAND//_leftonly}" ; bindkey -v + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd   COMMND   HOSTNAME  USER  ⋯  tests  shell  3rd   + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd   + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd  echo abc +abc + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd  false + INSERT   HOSTNAME  USER  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.segment_data.hostname.display=false" + INSERT  USER  ⋯  tests  shell  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.segment_data.user.display=false" + INSERT  ⋯  tests  shell  3rd  select abc in def ghi jkl + select  do + select   echo $abc + select   break + select  done +1) def 2) ghi 3) jkl + Select variant  1 +def + INSERT  ⋯  tests  shell  3rd  hash -d foo=$PWD:h ; cd . + INSERT  ~foo  3rd  POWERLINE_COMMAND="$POWERLINE_COMMAND -t default.dividers.left.hard=\$ABC" + INSERT $ABC~foo  3rd $ABCtrue diff --git a/tests/test_tabline.vim b/tests/test_tabline.vim new file mode 100755 index 00000000..a47fafaf --- /dev/null +++ b/tests/test_tabline.vim @@ -0,0 +1,56 @@ +#!/usr/bin/vim -S +set encoding=utf-8 +let g:powerline_config_paths = [expand(':p:h:h') . '/powerline/config_files'] +source powerline/bindings/vim/plugin/powerline.vim +edit abc +tabedit def +tabedit ghi + +redir => g:messages + +try + let &columns = 80 + let result = eval(&tabline[2:]) +catch + call writefile(['Exception while evaluating &tabline', v:exception], 'message.fail') + cquit +endtry + +if result isnot# '%#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# Tabs ' + call writefile(['Unexpected tabline', result], 'message.fail') + cquit +endif + +tabonly! + +try + let result = eval(&tabline[2:]) +catch + call writefile(['Exception while evaluating &tabline (2)', v:exception], 'message.fail') + cquit +endtry + +if result isnot# '%#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 ' + call writefile(['Unexpected tabline (2)', result], 'message.fail') + cquit +endif + +try + vsplit + let result = eval(&tabline[2:]) +catch + call writefile(['Exception while evaluating &tabline (3)', v:exception], 'message.fail') +endtry + +if result isnot# '%#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 ' + call writefile(['Unexpected tabline (3)', result], 'message.fail') + cquit +endif + +redir END +if g:messages =~ '\S' + call writefile(['Non-empty messages:', g:messages], 'message.fail') + cquit +endif + +qall! diff --git a/tests/test_watcher.py b/tests/test_watcher.py new file mode 100644 index 00000000..956faa7a --- /dev/null +++ b/tests/test_watcher.py @@ -0,0 +1,196 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import shutil +import os + +from time import sleep +from functools import partial + +from powerline.lib.watcher import create_file_watcher, create_tree_watcher, INotifyError +from powerline.lib.watcher.uv import UvNotFound +from powerline import get_fallback_logger +from powerline.lib.monotonic import monotonic + +from tests import TestCase, SkipTest + + +INOTIFY_DIR = 'inotify' + os.environ.get('PYTHON', '') + + +def clear_dir(dir): + for root, dirs, files in list(os.walk(dir, topdown=False)): + for f in files: + os.remove(os.path.join(root, f)) + for d in dirs: + os.rmdir(os.path.join(root, d)) + + +class TestFilesystemWatchers(TestCase): + def do_test_for_change(self, watcher, path): + st = monotonic() + while monotonic() - st < 1: + if watcher(path): + return + sleep(0.1) + self.fail('The change to {0} was not detected'.format(path)) + + def test_file_watcher(self): + try: + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='inotify') + except INotifyError: + raise SkipTest('This test is not suitable for a stat based file watcher') + return self.do_test_file_watcher(w) + + def do_test_file_watcher(self, w): + try: + f1, f2, f3 = map(lambda x: os.path.join(INOTIFY_DIR, 'file%d' % x), (1, 2, 3)) + with open(f1, 'wb'): + with open(f2, 'wb'): + with open(f3, 'wb'): + pass + ne = os.path.join(INOTIFY_DIR, 'notexists') + self.assertRaises(OSError, w, ne) + self.assertTrue(w(f1)) + self.assertTrue(w(f2)) + os.utime(f1, None), os.utime(f2, None) + self.do_test_for_change(w, f1) + self.do_test_for_change(w, f2) + # Repeat once + os.utime(f1, None), os.utime(f2, None) + self.do_test_for_change(w, f1) + self.do_test_for_change(w, f2) + # Check that no false changes are reported + self.assertFalse(w(f1), 'Spurious change detected') + self.assertFalse(w(f2), 'Spurious change detected') + # Check that open the file with 'w' triggers a change + with open(f1, 'wb'): + with open(f2, 'wb'): + pass + self.do_test_for_change(w, f1) + self.do_test_for_change(w, f2) + # Check that writing to a file with 'a' triggers a change + with open(f1, 'ab') as f: + f.write(b'1') + self.do_test_for_change(w, f1) + # Check that deleting a file registers as a change + os.unlink(f1) + self.do_test_for_change(w, f1) + # Test that changing the inode of a file does not cause it to stop + # being watched + os.rename(f3, f2) + self.do_test_for_change(w, f2) + self.assertFalse(w(f2), 'Spurious change detected') + os.utime(f2, None) + self.do_test_for_change(w, f2) + finally: + clear_dir(INOTIFY_DIR) + + def test_uv_file_watcher(self): + raise SkipTest('Uv watcher tests are not stable') + try: + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='uv') + except UvNotFound: + raise SkipTest('Pyuv is not available') + return self.do_test_file_watcher(w) + + def test_tree_watcher(self): + tw = create_tree_watcher(get_fallback_logger()) + return self.do_test_tree_watcher(tw) + + def do_test_tree_watcher(self, tw): + try: + subdir = os.path.join(INOTIFY_DIR, 'subdir') + os.mkdir(subdir) + try: + if tw.watch(INOTIFY_DIR).is_dummy: + raise SkipTest('No tree watcher available') + except UvNotFound: + raise SkipTest('Pyuv is not available') + self.assertTrue(tw(INOTIFY_DIR)) + self.assertFalse(tw(INOTIFY_DIR)) + changed = partial(self.do_test_for_change, tw, INOTIFY_DIR) + open(os.path.join(INOTIFY_DIR, 'tree1'), 'w').close() + changed() + open(os.path.join(subdir, 'tree1'), 'w').close() + changed() + os.unlink(os.path.join(subdir, 'tree1')) + changed() + os.rmdir(subdir) + changed() + os.mkdir(subdir) + changed() + os.rename(subdir, subdir + '1') + changed() + shutil.rmtree(subdir + '1') + changed() + os.mkdir(subdir) + f = os.path.join(subdir, 'f') + open(f, 'w').close() + changed() + with open(f, 'a') as s: + s.write(' ') + changed() + os.rename(f, f + '1') + changed() + finally: + clear_dir(INOTIFY_DIR) + + def test_uv_tree_watcher(self): + raise SkipTest('Uv watcher tests are not stable') + tw = create_tree_watcher(get_fallback_logger(), 'uv') + return self.do_test_tree_watcher(tw) + + def test_inotify_file_watcher_is_watching(self): + try: + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='inotify') + except INotifyError: + raise SkipTest('INotify is not available') + return self.do_test_file_watcher_is_watching(w) + + def test_stat_file_watcher_is_watching(self): + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='stat') + return self.do_test_file_watcher_is_watching(w) + + def test_uv_file_watcher_is_watching(self): + try: + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='uv') + except UvNotFound: + raise SkipTest('Pyuv is not available') + return self.do_test_file_watcher_is_watching(w) + + def do_test_file_watcher_is_watching(self, w): + try: + f1, f2, f3 = map(lambda x: os.path.join(INOTIFY_DIR, 'file%d' % x), (1, 2, 3)) + with open(f1, 'wb'): + with open(f2, 'wb'): + with open(f3, 'wb'): + pass + ne = os.path.join(INOTIFY_DIR, 'notexists') + self.assertRaises(OSError, w, ne) + self.assertTrue(w(f1)) + self.assertFalse(w.is_watching(ne)) + self.assertTrue(w.is_watching(f1)) + self.assertFalse(w.is_watching(f2)) + finally: + clear_dir(INOTIFY_DIR) + + +old_cwd = None + + +def setUpModule(): + global old_cwd + old_cwd = os.getcwd() + os.chdir(os.path.dirname(__file__)) + os.mkdir(INOTIFY_DIR) + + +def tearDownModule(): + shutil.rmtree(INOTIFY_DIR) + os.chdir(old_cwd) + + +if __name__ == '__main__': + from tests import main + main() diff --git a/tests/vim.py b/tests/vim.py new file mode 100644 index 00000000..d8ac109f --- /dev/null +++ b/tests/vim.py @@ -0,0 +1,916 @@ +# vim:fileencoding=utf-8:noet +_log = [] +vars = {} +vvars = {'version': 703} +_tabpage = 0 +_mode = 'n' +_buf_purge_events = set() +options = { + 'paste': 0, + 'ambiwidth': 'single', + 'columns': 80, + 'encoding': 'utf-8', +} +_last_bufnr = 0 +_highlights = {} +from collections import defaultdict as _defaultdict +_environ = _defaultdict(lambda: '') +del _defaultdict + + +_thread_id = None + + +def _set_thread_id(): + global _thread_id + from threading import current_thread + _thread_id = current_thread().ident + + +# Assuming import is done from the main thread +_set_thread_id() + + +def _print_log(): + for item in _log: + print (item) + _log[:] = () + + +def _vim(func): + from functools import wraps + from threading import current_thread + + @wraps(func) + def f(*args, **kwargs): + global _thread_id + if _thread_id != current_thread().ident: + raise RuntimeError('Accessing vim from separate threads is not allowed') + _log.append((func.__name__, args)) + return func(*args, **kwargs) + + return f + + +def _unicode(func): + from functools import wraps + import sys + + if sys.version_info < (3,): + return func + + @wraps(func) + def f(*args, **kwargs): + from powerline.lib.unicode import u + ret = func(*args, **kwargs) + if isinstance(ret, bytes): + ret = u(ret) + return ret + + return f + + +class _Buffers(object): + @_vim + def __init__(self): + self.d = {} + + @_vim + def __len__(self): + return len(self.d) + + @_vim + def __getitem__(self, item): + return self.d[item] + + @_vim + def __setitem__(self, item, value): + self.d[item] = value + + @_vim + def __iter__(self): + return iter(self.d.values()) + + @_vim + def __contains__(self, item): + return item in self.d + + @_vim + def _keys(self): + return self.d.keys() + + @_vim + def _pop(self, *args, **kwargs): + return self.d.pop(*args, **kwargs) + + +buffers = _Buffers() + + +class _ObjList(object): + @_vim + def __init__(self, objtype): + self.l = [] + self.objtype = objtype + + @_vim + def __getitem__(self, item): + return self.l[item - int(item > 0)] + + @_vim + def __len__(self): + return len(self.l) + + @_vim + def __iter__(self): + return iter(self.l) + + @_vim + def _pop(self, idx): + obj = self.l.pop(idx - 1) + for moved_obj in self.l[idx - 1:]: + moved_obj.number -= 1 + return obj + + @_vim + def _append(self, *args, **kwargs): + return self.l.append(*args, **kwargs) + + @_vim + def _new(self, *args, **kwargs): + number = len(self) + 1 + new_obj = self.objtype(number, *args, **kwargs) + self._append(new_obj) + return new_obj + + +def _construct_result(r): + import sys + if sys.version_info < (3,): + return r + else: + if isinstance(r, str): + return r.encode('utf-8') + elif isinstance(r, list): + return [_construct_result(i) for i in r] + elif isinstance(r, dict): + return dict(( + (_construct_result(k), _construct_result(v)) + for k, v in r.items() + )) + return r + + +def _str_func(func): + from functools import wraps + + @wraps(func) + def f(*args, **kwargs): + return _construct_result(func(*args, **kwargs)) + return f + + +def _log_print(): + import sys + for entry in _log: + sys.stdout.write(repr(entry) + '\n') + + +_current_group = None +_on_wipeout = [] + + +@_vim +def command(cmd): + global _current_group + cmd = cmd.lstrip() + if cmd.startswith('let g:'): + import re + varname, value = re.compile(r'^let g:(\w+)\s*=\s*(.*)').match(cmd).groups() + vars[varname] = value + elif cmd.startswith('hi '): + sp = cmd.split() + _highlights[sp[1]] = sp[2:] + elif cmd.startswith('function! Powerline_plugin_ctrlp'): + # Ignore CtrlP updating functions + pass + elif cmd.startswith('augroup'): + augroup = cmd.partition(' ')[2] + if augroup.upper() == 'END': + _current_group = None + else: + _current_group = augroup + elif cmd.startswith('autocmd'): + rest = cmd.partition(' ')[2] + auevent, rest = rest.partition(' ')[::2] + pattern, aucmd = rest.partition(' ')[::2] + if auevent != 'BufWipeout' or pattern != '*': + raise NotImplementedError + import sys + if sys.version_info < (3,): + if not aucmd.startswith(':python '): + raise NotImplementedError + else: + if not aucmd.startswith(':python3 '): + raise NotImplementedError + _on_wipeout.append(aucmd.partition(' ')[2]) + elif cmd.startswith('set '): + if cmd.startswith('set statusline='): + options['statusline'] = cmd[len('set statusline='):] + elif cmd.startswith('set tabline='): + options['tabline'] = cmd[len('set tabline='):] + else: + raise NotImplementedError(cmd) + else: + raise NotImplementedError(cmd) + + +@_vim +@_unicode +def eval(expr): + if expr.startswith('g:'): + return vars[expr[2:]] + elif expr.startswith('v:'): + return vvars[expr[2:]] + elif expr.startswith('&'): + return options[expr[1:]] + elif expr.startswith('$'): + return _environ[expr[1:]] + elif expr.startswith('PowerlineRegisterCachePurgerEvent'): + _buf_purge_events.add(expr[expr.find('"') + 1:expr.rfind('"') - 1]) + return '0' + elif expr.startswith('exists('): + return '0' + elif expr.startswith('getwinvar('): + import re + match = re.match(r'^getwinvar\((\d+), "(\w+)"\)$', expr) + if not match: + raise NotImplementedError(expr) + winnr = int(match.group(1)) + varname = match.group(2) + return _emul_getwinvar(winnr, varname) + elif expr.startswith('has_key('): + import re + match = re.match(r'^has_key\(getwinvar\((\d+), ""\), "(\w+)"\)$', expr) + if match: + winnr = int(match.group(1)) + varname = match.group(2) + return 0 + (varname in current.tabpage.windows[winnr].vars) + else: + match = re.match(r'^has_key\(gettabwinvar\((\d+), (\d+), ""\), "(\w+)"\)$', expr) + if not match: + raise NotImplementedError(expr) + tabnr = int(match.group(1)) + winnr = int(match.group(2)) + varname = match.group(3) + return 0 + (varname in tabpages[tabnr].windows[winnr].vars) + elif expr == 'getbufvar("%", "NERDTreeRoot").path.str()': + import os + assert os.path.basename(current.buffer.name).startswith('NERD_tree_') + return '/usr/include' + elif expr == 'tabpagenr()': + return current.tabpage.number + elif expr == 'tabpagenr("$")': + return len(tabpages) + elif expr.startswith('tabpagewinnr('): + tabnr = int(expr[len('tabpagewinnr('):-1]) + return tabpages[tabnr].window.number + elif expr.startswith('tabpagebuflist('): + import re + match = re.match(r'tabpagebuflist\((\d+)\)\[(\d+)\]', expr) + tabnr = int(match.group(1)) + winnr = int(match.group(2)) + 1 + return tabpages[tabnr].windows[winnr].buffer.number + elif expr.startswith('gettabwinvar('): + import re + match = re.match(r'gettabwinvar\((\d+), (\d+), "(\w+)"\)', expr) + tabnr = int(match.group(1)) + winnr = int(match.group(2)) + varname = match.group(3) + return tabpages[tabnr].windows[winnr].vars[varname] + raise NotImplementedError(expr) + + +@_vim +def bindeval(expr): + if expr == 'g:': + return vars + elif expr == '{}': + return {} + elif expr == '[]': + return [] + import re + match = re.compile(r'^function\("([^"\\]+)"\)$').match(expr) + if match: + return globals()['_emul_' + match.group(1)] + else: + raise NotImplementedError + + +@_vim +@_str_func +def _emul_mode(*args): + if args and args[0]: + return _mode + else: + return _mode[0] + + +@_vim +@_str_func +def _emul_getbufvar(bufnr, varname): + import re + if varname[0] == '&': + if bufnr == '%': + bufnr = current.buffer.number + if bufnr not in buffers: + return '' + try: + return buffers[bufnr].options[varname[1:]] + except KeyError: + try: + return options[varname[1:]] + except KeyError: + return '' + elif re.match('^[a-zA-Z_]+$', varname): + if bufnr == '%': + bufnr = current.buffer.number + if bufnr not in buffers: + return '' + return buffers[bufnr].vars[varname] + raise NotImplementedError + + +@_vim +@_str_func +def _emul_getwinvar(winnr, varname): + return current.tabpage.windows[winnr].vars.get(varname, '') + + +@_vim +def _emul_setwinvar(winnr, varname, value): + current.tabpage.windows[winnr].vars[varname] = value + + +@_vim +def _emul_virtcol(expr): + if expr == '.': + return current.window.cursor[1] + 1 + if isinstance(expr, list) and len(expr) == 3: + return expr[-2] + expr[-1] + raise NotImplementedError + + +_v_pos = None + + +@_vim +def _emul_getpos(expr): + if expr == '.': + return [0, current.window.cursor[0] + 1, current.window.cursor[1] + 1, 0] + if expr == 'v': + return _v_pos or [0, current.window.cursor[0] + 1, current.window.cursor[1] + 1, 0] + raise NotImplementedError + + +@_vim +@_str_func +def _emul_fnamemodify(path, modstring): + import os + _modifiers = { + '~': lambda path: path.replace(os.environ['HOME'].encode('utf-8'), b'~') if path.startswith(os.environ['HOME'].encode('utf-8')) else path, + '.': lambda path: (lambda tpath: path if tpath[:3] == b'..' + os.sep.encode() else tpath)(os.path.relpath(path)), + 't': lambda path: os.path.basename(path), + 'h': lambda path: os.path.dirname(path), + } + + for mods in modstring.split(':')[1:]: + path = _modifiers[mods](path) + return path + + +@_vim +@_str_func +def _emul_expand(expr): + global _abuf + if expr == '': + return _abuf or current.buffer.number + raise NotImplementedError + + +@_vim +def _emul_bufnr(expr): + if expr == '$': + return _last_bufnr + raise NotImplementedError + + +@_vim +def _emul_exists(varname): + if varname.startswith('g:'): + return varname[2:] in vars + raise NotImplementedError + + +@_vim +def _emul_line2byte(line): + buflines = current.buffer._buf_lines + if line == len(buflines) + 1: + return sum((len(s) for s in buflines)) + 1 + raise NotImplementedError + + +@_vim +def _emul_line(expr): + cursorline = current.window.cursor[0] + 1 + numlines = len(current.buffer._buf_lines) + if expr == 'w0': + return max(cursorline - 5, 1) + if expr == 'w$': + return min(cursorline + 5, numlines) + raise NotImplementedError + + +@_vim +@_str_func +def _emul_strtrans(s): + # FIXME Do more replaces + return s.replace(b'\xFF', b'') + + +@_vim +@_str_func +def _emul_bufname(bufnr): + try: + return buffers[bufnr]._name or b'' + except KeyError: + return b'' + + +_window_id = 0 + + +class _Window(object): + def __init__(self, number, buffer=None, cursor=(1, 0), width=80): + global _window_id + self.cursor = cursor + self.width = width + self.number = number + if buffer: + if type(buffer) is _Buffer: + self.buffer = buffer + else: + self.buffer = _Buffer(**buffer) + else: + self.buffer = _Buffer() + _window_id += 1 + self._window_id = _window_id + self.options = {} + self.vars = { + 'powerline_window_id': self._window_id, + } + + def __repr__(self): + return '' + + +class _Tabpage(object): + def __init__(self, number): + self.windows = _ObjList(_Window) + self.number = number + + def _new_window(self, **kwargs): + self.window = self.windows._new(**kwargs) + return self.window + + def _close_window(self, winnr, open_window=True): + curwinnr = self.window.number + win = self.windows._pop(winnr) + if self.windows and winnr == curwinnr: + self.window = self.windows[-1] + elif open_window: + current.tabpage._new_window() + return win + + def _close(self): + global _tabpage + while self.windows: + self._close_window(1, False) + tabpages._pop(self.number) + _tabpage = len(tabpages) + + +tabpages = _ObjList(_Tabpage) + + +_abuf = None + + +class _Buffer(object): + def __init__(self, name=None): + global _last_bufnr + _last_bufnr += 1 + bufnr = _last_bufnr + self.number = bufnr + # FIXME Use unicode() for python-3 + self.name = name + self.vars = {'changedtick': 1} + self.options = { + 'modified': 0, + 'readonly': 0, + 'fileformat': 'unix', + 'filetype': '', + 'buftype': '', + 'fileencoding': 'utf-8', + 'textwidth': 80, + } + self._buf_lines = [''] + self._undostate = [self._buf_lines[:]] + self._undo_written = len(self._undostate) + buffers[bufnr] = self + + @property + def name(self): + import sys + if sys.version_info < (3,): + return self._name + else: + return str(self._name, 'utf-8') if self._name else None + + @name.setter + def name(self, name): + if name is None: + self._name = None + else: + import os + if type(name) is not bytes: + name = name.encode('utf-8') + if b':/' in name: + self._name = name + else: + self._name = os.path.abspath(name) + + def __getitem__(self, line): + return self._buf_lines[line] + + def __setitem__(self, line, value): + self.options['modified'] = 1 + self.vars['changedtick'] += 1 + self._buf_lines[line] = value + from copy import copy + self._undostate.append(copy(self._buf_lines)) + + def __setslice__(self, *args): + self.options['modified'] = 1 + self.vars['changedtick'] += 1 + self._buf_lines.__setslice__(*args) + from copy import copy + self._undostate.append(copy(self._buf_lines)) + + def __getslice__(self, *args): + return self._buf_lines.__getslice__(*args) + + def __len__(self): + return len(self._buf_lines) + + def __repr__(self): + return '' + + def __del__(self): + global _abuf + bufnr = self.number + try: + import __main__ + except ImportError: + pass + except RuntimeError: + # Module may have already been garbage-collected + pass + else: + if _on_wipeout: + _abuf = bufnr + try: + for event in _on_wipeout: + exec(event, __main__.__dict__) + finally: + _abuf = None + + +class _Current(object): + @property + def buffer(self): + return self.window.buffer + + @property + def window(self): + return self.tabpage.window + + @property + def tabpage(self): + return tabpages[_tabpage - 1] + + +current = _Current() + + +_dict = None + + +@_vim +def _init(): + global _dict + + if _dict: + return _dict + + _dict = {} + for varname, value in globals().items(): + if varname[0] != '_': + _dict[varname] = value + _tabnew() + return _dict + + +@_vim +def _get_segment_info(): + mode_translations = { + chr(ord('V') - 0x40): '^V', + chr(ord('S') - 0x40): '^S', + } + mode = _mode + mode = mode_translations.get(mode, mode) + window = current.window + buffer = current.buffer + tabpage = current.tabpage + return { + 'window': window, + 'winnr': window.number, + 'buffer': buffer, + 'bufnr': buffer.number, + 'tabpage': tabpage, + 'tabnr': tabpage.number, + 'window_id': window._window_id, + 'mode': mode, + } + + +@_vim +def _launch_event(event): + pass + + +@_vim +def _start_mode(mode): + global _mode + if mode == 'i': + _launch_event('InsertEnter') + elif _mode == 'i': + _launch_event('InsertLeave') + _mode = mode + + +@_vim +def _undo(): + if len(current.buffer._undostate) == 1: + return + buffer = current.buffer + buffer._undostate.pop(-1) + buffer._buf_lines = buffer._undostate[-1] + if buffer._undo_written == len(buffer._undostate): + buffer.options['modified'] = 0 + + +@_vim +def _edit(name=None): + if current.buffer.name is None: + buffer = current.buffer + buffer.name = name + else: + buffer = _Buffer(name) + current.window.buffer = buffer + + +@_vim +def _tabnew(name=None): + global windows + global _tabpage + tabpage = tabpages._new() + windows = tabpage.windows + _tabpage = len(tabpages) + _new(name) + return tabpage + + +@_vim +def _new(name=None): + current.tabpage._new_window(buffer={'name': name}) + + +@_vim +def _split(): + current.tabpage._new_window(buffer=current.buffer) + + +@_vim +def _close(winnr, wipe=True): + win = current.tabpage._close_window(winnr) + if wipe: + for w in current.tabpage.windows: + if w.buffer.number == win.buffer.number: + break + else: + _bw(win.buffer.number) + + +@_vim +def _bw(bufnr=None): + bufnr = bufnr or current.buffer.number + winnr = 1 + for win in current.tabpage.windows: + if win.buffer.number == bufnr: + _close(winnr, wipe=False) + winnr += 1 + buffers._pop(bufnr) + if not buffers: + _Buffer() + _b(max(buffers._keys())) + + +@_vim +def _b(bufnr): + current.window.buffer = buffers[bufnr] + + +@_vim +def _set_cursor(line, col): + current.window.cursor = (line, col) + if _mode == 'n': + _launch_event('CursorMoved') + elif _mode == 'i': + _launch_event('CursorMovedI') + + +@_vim +def _get_buffer(): + return current.buffer + + +@_vim +def _set_bufoption(option, value, bufnr=None): + buffers[bufnr or current.buffer.number].options[option] = value + if option == 'filetype': + _launch_event('FileType') + + +class _WithNewBuffer(object): + def __init__(self, func, *args, **kwargs): + self.call = lambda: func(*args, **kwargs) + + def __enter__(self): + self.call() + self.bufnr = current.buffer.number + return _get_segment_info() + + def __exit__(self, *args): + _bw(self.bufnr) + + +@_vim +def _set_dict(d, new, setfunc=None): + if not setfunc: + def setfunc(k, v): + d[k] = v + + old = {} + na = [] + for k, v in new.items(): + try: + old[k] = d[k] + except KeyError: + na.append(k) + setfunc(k, v) + return old, na + + +class _WithBufOption(object): + def __init__(self, **new): + self.new = new + + def __enter__(self): + self.buffer = current.buffer + self.old = _set_dict(self.buffer.options, self.new, _set_bufoption)[0] + + def __exit__(self, *args): + self.buffer.options.update(self.old) + + +class _WithMode(object): + def __init__(self, new): + self.new = new + + def __enter__(self): + self.old = _mode + _start_mode(self.new) + return _get_segment_info() + + def __exit__(self, *args): + _start_mode(self.old) + + +class _WithDict(object): + def __init__(self, d, **new): + self.new = new + self.d = d + + def __enter__(self): + self.old, self.na = _set_dict(self.d, self.new) + + def __exit__(self, *args): + self.d.update(self.old) + for k in self.na: + self.d.pop(k) + + +class _WithSplit(object): + def __enter__(self): + _split() + + def __exit__(self, *args): + _close(2, wipe=False) + + +class _WithBufName(object): + def __init__(self, new): + self.new = new + + def __enter__(self): + import os + buffer = current.buffer + self.buffer = buffer + self.old = buffer.name + buffer.name = self.new + if buffer.name and os.path.basename(buffer.name) == 'ControlP': + buffer.vars['powerline_ctrlp_type'] = 'main' + buffer.vars['powerline_ctrlp_args'] = ['focus', 'byfname', '0', 'prev', 'item', 'next', 'marked'] + + def __exit__(self, *args): + self.buffer.name = self.old + + +class _WithNewTabPage(object): + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + + def __enter__(self): + self.tab = _tabnew(*self.args, **self.kwargs) + + def __exit__(self, *args): + self.tab._close() + + +class _WithGlobal(object): + def __init__(self, **kwargs): + self.kwargs = kwargs + + def __enter__(self): + self.empty = object() + self.old = dict(((key, globals().get(key, self.empty)) for key in self.kwargs)) + globals().update(self.kwargs) + + def __exit__(self, *args): + for k, v in self.old.items(): + if v is self.empty: + globals().pop(k, None) + else: + globals()[k] = v + + +@_vim +def _with(key, *args, **kwargs): + if key == 'buffer': + return _WithNewBuffer(_edit, *args, **kwargs) + elif key == 'bufname': + return _WithBufName(*args, **kwargs) + elif key == 'mode': + return _WithMode(*args, **kwargs) + elif key == 'bufoptions': + return _WithBufOption(**kwargs) + elif key == 'options': + return _WithDict(options, **kwargs) + elif key == 'globals': + return _WithDict(vars, **kwargs) + elif key == 'wvars': + return _WithDict(current.window.vars, **kwargs) + elif key == 'environ': + return _WithDict(_environ, **kwargs) + elif key == 'split': + return _WithSplit() + elif key == 'tabpage': + return _WithNewTabPage(*args, **kwargs) + elif key == 'vpos': + return _WithGlobal(_v_pos=[0, kwargs['line'], kwargs['col'], kwargs['off']]) + + +class error(Exception): + pass diff --git a/tools/colors.map b/tools/colors.map new file mode 100644 index 00000000..61d8c4a7 --- /dev/null +++ b/tools/colors.map @@ -0,0 +1,646 @@ +Grey 545454 +Grey, Silver C0C0C0 +grey BEBEBE +LightGray D3D3D3 +LightSlateGrey 778899 +SlateGray 708090 +SlateGray1 C6E2FF +SlateGray2 B9D3EE +SlateGray3 9FB6CD +SlateGray4 6C7B8B +black 000000 +grey0 000000 +grey1 030303 +grey2 050505 +grey3 080808 +grey4 0A0A0A +grey5 0D0D0D +grey6 0F0F0F +grey7 121212 +grey8 141414 +grey9 171717 +grey10 1A1A1A +grey11 1C1C1C +grey12 1F1F1F +grey13 212121 +grey14 242424 +grey15 262626 +grey16 292929 +grey17 2B2B2B +grey18 2E2E2E +grey19 303030 +grey20 333333 +grey21 363636 +grey22 383838 +grey23 3B3B3B +grey24 3D3D3D +grey25 404040 +grey26 424242 +grey27 454545 +grey28 474747 +grey29 4A4A4A +grey30 4D4D4D +grey31 4F4F4F +grey32 525252 +grey33 545454 +grey34 575757 +grey35 595959 +grey36 5C5C5C +grey37 5E5E5E +grey38 616161 +grey39 636363 +grey40 666666 +grey41, DimGrey 696969 +grey42 6B6B6B +grey43 6E6E6E +grey44 707070 +grey45 737373 +grey46 757575 +grey47 787878 +grey48 7A7A7A +grey49 7D7D7D +grey50 7F7F7F +grey51 828282 +grey52 858585 +grey53 878787 +grey54 8A8A8A +grey55 8C8C8C +grey56 8F8F8F +grey57 919191 +grey58 949494 +grey59 969696 +grey60 999999 +grey61 9C9C9C +grey62 9E9E9E +grey63 A1A1A1 +grey64 A3A3A3 +grey65 A6A6A6 +grey66 A8A8A8 +grey67 ABABAB +grey68 ADADAD +grey69 B0B0B0 +grey70 B3B3B3 +grey71 B5B5B5 +grey72 B8B8B8 +grey73 BABABA +grey74 BDBDBD +grey75 BFBFBF +grey76 C2C2C2 +grey77 C4C4C4 +grey78 C7C7C7 +grey79 C9C9C9 +grey80 CCCCCC +grey81 CFCFCF +grey82 D1D1D1 +grey83 D4D4D4 +grey84 D6D6D6 +grey85 D9D9D9 +grey86 DBDBDB +grey87 DEDEDE +grey88 E0E0E0 +grey89 E3E3E3 +grey90 E5E5E5 +grey91 E8E8E8 +grey92 EBEBEB +grey93 EDEDED +grey94 F0F0F0 +grey95 F2F2F2 +grey96 F5F5F5 +grey97 F7F7F7 +grey98 FAFAFA +grey99 FCFCFC +grey100, White FFFFFF +Dark Slate Grey 2F4F4F +Dim Grey 545454 +Very Light Grey CDCDCD +Free Speech Grey 635688 +AliceBlue F0F8FF +BlueViolet 8A2BE2 +Cadet Blue 5F9F9F +CadetBlue 5F9EA0 +CadetBlue 5F9EA0 +CadetBlue1 98F5FF +CadetBlue2 8EE5EE +CadetBlue3 7AC5CD +CadetBlue4 53868B +Corn Flower Blue 42426F +CornflowerBlue 6495ED +DarkSlateBlue 483D8B +DarkTurquoise 00CED1 +DeepSkyBlue 00BFFF +DeepSkyBlue1 00BFFF +DeepSkyBlue2 00B2EE +DeepSkyBlue3 009ACD +DeepSkyBlue4 00688B +DodgerBlue 1E90FF +DodgerBlue1 1E90FF +DodgerBlue2 1C86EE +DodgerBlue3 1874CD +DodgerBlue4 104E8B +LightBlue ADD8E6 +LightBlue1 BFEFFF +LightBlue2 B2DFEE +LightBlue3 9AC0CD +LightBlue4 68838B +LightCyan E0FFFF +LightCyan1 E0FFFF +LightCyan2 D1EEEE +LightCyan3 B4CDCD +LightCyan4 7A8B8B +LightSkyBlue 87CEFA +LightSkyBlue1 B0E2FF +LightSkyBlue2 A4D3EE +LightSkyBlue3 8DB6CD +LightSkyBlue4 607B8B +LightSlateBlue 8470FF +LightSteelBlue B0C4DE +LightSteelBlue1 CAE1FF +LightSteelBlue2 BCD2EE +LightSteelBlue3 A2B5CD +LightSteelBlue4 6E7B8B +Aquamarine 70DB93 +MediumBlue 0000CD +MediumSlateBlue 7B68EE +MediumTurquoise 48D1CC +MidnightBlue 191970 +NavyBlue 000080 +PaleTurquoise AFEEEE +PaleTurquoise1 BBFFFF +PaleTurquoise2 AEEEEE +PaleTurquoise3 96CDCD +PaleTurquoise4 668B8B +PowderBlue B0E0E6 +RoyalBlue 4169E1 +RoyalBlue1 4876FF +RoyalBlue2 436EEE +RoyalBlue3 3A5FCD +RoyalBlue4 27408B +RoyalBlue5 002266 +SkyBlue 87CEEB +SkyBlue1 87CEFF +SkyBlue2 7EC0EE +SkyBlue3 6CA6CD +SkyBlue4 4A708B +SlateBlue 6A5ACD +SlateBlue1 836FFF +SlateBlue2 7A67EE +SlateBlue3 6959CD +SlateBlue4 473C8B +SteelBlue 4682B4 +SteelBlue1 63B8FF +SteelBlue2 5CACEE +SteelBlue3 4F94CD +SteelBlue4 36648B +aquamarine 7FFFD4 +aquamarine1 7FFFD4 +aquamarine2 76EEC6 +aquamarine3, MediumAquamarine 66CDAA +aquamarine4 458B74 +azure F0FFFF +azure1 F0FFFF +azure2 E0EEEE +azure3 C1CDCD +azure4 838B8B +blue 0000FF +blue1 0000FF +blue2 0000EE +blue3 0000CD +blue4 00008B +aqua 00FFFF +True Iris Blue 03B4CC +cyan 00FFFF +cyan1 00FFFF +cyan2 00EEEE +cyan3 00CDCD +cyan4 008B8B +navy 000080 +teal 008080 +turquoise 40E0D0 +turquoise1 00F5FF +turquoise2 00E5EE +turquoise3 00C5CD +turquoise4 00868B +DarkSlateGray 2F4F4F +DarkSlateGray1 97FFFF +DarkSlateGray2 8DEEEE +DarkSlateGray3 79CDCD +DarkSlateGray4 528B8B +Dark Slate Blue 241882 +Dark Turquoise 7093DB +Medium Slate Blue 7F00FF +Medium Turquoise 70DBDB +Midnight Blue 2F2F4F +Navy Blue 23238E +Neon Blue 4D4DFF +New Midnight Blue 00009C +Rich Blue 5959AB +Sky Blue 3299CC +Slate Blue 007FFF +Summer Sky 38B0DE +Iris Blue 03B4C8 +Free Speech Blue 4156C5 +RosyBrown BC8F8F +RosyBrown1 FFC1C1 +RosyBrown2 EEB4B4 +RosyBrown3 CD9B9B +RosyBrown4 8B6969 +SaddleBrown 8B4513 +SandyBrown F4A460 +beige F5F5DC +brown A52A2A +brown A62A2A +brown1 FF4040 +brown2 EE3B3B +brown3 CD3333 +brown4 8B2323 +dark brown 5C4033 +burlywood DEB887 +burlywood1 FFD39B +burlywood2 EEC591 +burlywood3 CDAA7D +burlywood4 8B7355 +baker's chocolate 5C3317 +chocolate D2691E +chocolate1 FF7F24 +chocolate2 EE7621 +chocolate3 CD661D +chocolate4 8B4513 +peru CD853F +tan D2B48C +tan1 FFA54F +tan2 EE9A49 +tan3 CD853F +tan4 8B5A2B +Dark Tan 97694F +Dark Wood 855E42 +Light Wood 856363 +Medium Wood A68064 +New Tan EBC79E +Semi-Sweet Chocolate 6B4226 +Sienna 8E6B23 +Tan DB9370 +Very Dark Brown 5C4033 +Dark Green 2F4F2F +DarkGreen 006400 +dark green copper 4A766E +DarkKhaki BDB76B +DarkOliveGreen 556B2F +DarkOliveGreen1 CAFF70 +DarkOliveGreen2 BCEE68 +DarkOliveGreen3 A2CD5A +DarkOliveGreen4 6E8B3D +olive 808000 +DarkSeaGreen 8FBC8F +DarkSeaGreen1 C1FFC1 +DarkSeaGreen2 B4EEB4 +DarkSeaGreen3 9BCD9B +DarkSeaGreen4 698B69 +ForestGreen 228B22 +GreenYellow ADFF2F +LawnGreen 7CFC00 +LightSeaGreen 20B2AA +LimeGreen 32CD32 +MediumSeaGreen 3CB371 +MediumSpringGreen 00FA9A +MintCream F5FFFA +OliveDrab 6B8E23 +OliveDrab1 C0FF3E +OliveDrab2 B3EE3A +OliveDrab3 9ACD32 +OliveDrab4 698B22 +PaleGreen 98FB98 +PaleGreen1 9AFF9A +PaleGreen2 90EE90 +PaleGreen3 7CCD7C +PaleGreen4 548B54 +SeaGreen, SeaGreen4 2E8B57 +SeaGreen1 54FF9F +SeaGreen2 4EEE94 +SeaGreen3 43CD80 +SpringGreen 00FF7F +SpringGreen1 00FF7F +SpringGreen2 00EE76 +SpringGreen3 00CD66 +SpringGreen4 008B45 +YellowGreen 9ACD32 +chartreuse 7FFF00 +chartreuse1 7FFF00 +chartreuse2 76EE00 +chartreuse3 66CD00 +chartreuse4 458B00 +green 00FF00 +green 008000 +lime 00FF00 +green1 00FF00 +green2 00EE00 +green3 00CD00 +green4 008B00 +khaki F0E68C +khaki1 FFF68F +khaki2 EEE685 +khaki3 CDC673 +khaki4 8B864E +Dark Olive Green 4F4F2F +Green Yellow [sic] D19275 +Hunter Green [sic] 8E2323 +Forest Green, Khaki, Medium Aquamarine 238E23 +Medium Forest Green DBDB70 +Medium Sea Green 426F42 +Medium Spring Green 7FFF00 +Pale Green 8FBC8F +Sea Green 238E68 +Spring Green 00FF7F +Free Speech Green 09F911 +Free Speech Aquamarine 029D74 +DarkOrange FF8C00 +DarkOrange1 FF7F00 +DarkOrange2 EE7600 +DarkOrange3 CD6600 +DarkOrange4 8B4500 +DarkSalmon E9967A +LightCoral F08080 +LightSalmon FFA07A +LightSalmon1 FFA07A +LightSalmon2 EE9572 +LightSalmon3 CD8162 +LightSalmon4 8B5742 +PeachPuff FFDAB9 +PeachPuff1 FFDAB9 +PeachPuff2 EECBAD +PeachPuff3 CDAF95 +PeachPuff4 8B7765 +bisque FFE4C4 +bisque1 FFE4C4 +bisque2 EED5B7 +bisque3 CDB79E +bisque4 8B7D6B +coral FF7F00 +coral FF7F50 +coral1 FF7256 +coral2 EE6A50 +coral3 CD5B45 +coral4 8B3E2F +honeydew F0FFF0 +honeydew1 F0FFF0 +honeydew2 E0EEE0 +honeydew3 C1CDC1 +honeydew4 838B83 +orange FFA500 +orange1 FFA500 +orange2 EE9A00 +orange3 CD8500 +orange4 8B5A00 +salmon FA8072 +salmon1 FF8C69 +salmon2 EE8262 +salmon3 CD7054 +salmon4 8B4C39 +sienna A0522D +sienna1 FF8247 +sienna2 EE7942 +sienna3 CD6839 +sienna4 8B4726 +Mandarian Orange 8E2323 +Orange FF7F00 +Orange Red FF2400 +DeepPink FF1493 +DeepPink1 FF1493 +DeepPink2 EE1289 +DeepPink3 CD1076 +DeepPink4 8B0A50 +HotPink FF69B4 +HotPink1 FF6EB4 +HotPink2 EE6AA7 +HotPink3 CD6090 +HotPink4 8B3A62 +IndianRed CD5C5C +IndianRed1 FF6A6A +IndianRed2 EE6363 +IndianRed3 CD5555 +IndianRed4 8B3A3A +LightPink FFB6C1 +LightPink1 FFAEB9 +LightPink2 EEA2AD +LightPink3 CD8C95 +LightPink4 8B5F65 +MediumVioletRed C71585 +MistyRose FFE4E1 +MistyRose1 FFE4E1 +MistyRose2 EED5D2 +MistyRose3 CDB7B5 +MistyRose4 8B7D7B +OrangeRed FF4500 +OrangeRed1 FF4500 +OrangeRed2 EE4000 +OrangeRed3 CD3700 +OrangeRed4 8B2500 +PaleVioletRed DB7093 +PaleVioletRed1 FF82AB +PaleVioletRed2 EE799F +PaleVioletRed3 CD6889 +PaleVioletRed4 8B475D +VioletRed D02090 +VioletRed1 FF3E96 +VioletRed2 EE3A8C +VioletRed3 CD3278 +VioletRed4 8B2252 +firebrick B22222 +firebrick1 FF3030 +firebrick2 EE2C2C +firebrick3 CD2626 +firebrick4 8B1A1A +pink FFC0CB +pink1 FFB5C5 +pink2 EEA9B8 +pink3 CD919E +pink4 8B636C +Flesh F5CCB0 +Feldspar D19275 +red FF0000 +red1 FF0000 +red2 EE0000 +red3 CD0000 +red4 8B0000 +tomato FF6347 +tomato1 FF6347 +tomato2 EE5C42 +tomato3 CD4F39 +tomato4 8B3626 +Dusty Rose 856363 +Firebrick 8E2323 +Indian Red F5CCB0 +Pink BC8F8F +Salmon 6F4242 +Scarlet 8C1717 +Spicy Pink FF1CAE +Free Speech Magenta E35BD8 +Free Speech Red C00000 +DarkOrchid 9932CC +DarkOrchid1 BF3EFF +DarkOrchid2 B23AEE +DarkOrchid3 9A32CD +DarkOrchid4 68228B +DarkViolet 9400D3 +LavenderBlush FFF0F5 +LavenderBlush1 FFF0F5 +LavenderBlush2 EEE0E5 +LavenderBlush3 CDC1C5 +LavenderBlush4 8B8386 +MediumOrchid BA55D3 +MediumOrchid1 E066FF +MediumOrchid2 D15FEE +MediumOrchid3 B452CD +MediumOrchid4 7A378B +MediumPurple 9370DB +Medium Orchid 9370DB +MediumPurple1 AB82FF +Dark Orchid 9932CD +MediumPurple2 9F79EE +MediumPurple3 8968CD +MediumPurple4 5D478B +lavender E6E6FA +magenta FF00FF +fuchsia FF00FF +magenta1 FF00FF +magenta2 EE00EE +magenta3 CD00CD +magenta4 8B008B +maroon B03060 +maroon1 FF34B3 +maroon2 EE30A7 +maroon3 CD2990 +maroon4 8B1C62 +orchid DA70D6 +Orchid DB70DB +orchid1 FF83FA +orchid2 EE7AE9 +orchid3 CD69C9 +orchid4 8B4789 +plum DDA0DD +plum1 FFBBFF +plum2 EEAEEE +plum3 CD96CD +plum4 8B668B +purple A020F0 +purple 800080 +purple1 9B30FF +purple2 912CEE +purple3 7D26CD +purple4 551A8B +thistle D8BFD8 +thistle1 FFE1FF +thistle2 EED2EE +thistle3 CDB5CD +thistle4 8B7B8B +violet EE82EE +violet blue 9F5F9F +Dark Purple 871F78 +Maroon 800000 +Medium Violet Red DB7093 +Neon Pink FF6EC7 +Plum EAADEA +Thistle D8BFD8 +Turquoise ADEAEA +Violet 4F2F4F +Violet Red CC3299 +AntiqueWhite FAEBD7 +AntiqueWhite1 FFEFDB +AntiqueWhite2 EEDFCC +AntiqueWhite3 CDC0B0 +AntiqueWhite4 8B8378 +FloralWhite FFFAF0 +GhostWhite F8F8FF +NavajoWhite FFDEAD +NavajoWhite1 FFDEAD +NavajoWhite2 EECFA1 +NavajoWhite3 CDB38B +NavajoWhite4 8B795E +OldLace FDF5E6 +WhiteSmoke F5F5F5 +gainsboro DCDCDC +ivory FFFFF0 +ivory1 FFFFF0 +ivory2 EEEEE0 +ivory3 CDCDC1 +ivory4 8B8B83 +linen FAF0E6 +seashell FFF5EE +seashell1 FFF5EE +seashell2 EEE5DE +seashell3 CDC5BF +seashell4 8B8682 +snow FFFAFA +snow1 FFFAFA +snow2 EEE9E9 +snow3 CDC9C9 +snow4 8B8989 +wheat F5DEB3 +wheat1 FFE7BA +wheat2 EED8AE +wheat3 CDBA96 +wheat4 8B7E66 +white FFFFFF +Quartz D9D9F3 +Wheat D8D8BF +BlanchedAlmond FFEBCD +DarkGoldenrod B8860B +DarkGoldenrod1 FFB90F +DarkGoldenrod2 EEAD0E +DarkGoldenrod3 CD950C +DarkGoldenrod4 8B6508 +LemonChiffon FFFACD +LemonChiffon1 FFFACD +LemonChiffon2 EEE9BF +LemonChiffon3 CDC9A5 +LemonChiffon4 8B8970 +LightGoldenrod EEDD82 +LightGoldenrod1 FFEC8B +LightGoldenrod2 EEDC82 +LightGoldenrod3 CDBE70 +LightGoldenrod4 8B814C +LightGoldenrodYellow FAFAD2 +LightYellow FFFFE0 +LightYellow1 FFFFE0 +LightYellow2 EEEED1 +LightYellow3 CDCDB4 +LightYellow4 8B8B7A +PaleGoldenrod EEE8AA +PapayaWhip FFEFD5 +cornsilk FFF8DC +cornsilk1 FFF8DC +cornsilk2 EEE8CD +cornsilk3 CDC8B1 +cornsilk4 8B8878 +goldenrod DAA520 +goldenrod1 FFC125 +goldenrod2 EEB422 +goldenrod3 CD9B1D +goldenrod4 8B6914 +moccasin FFE4B5 +yellow FFFF00 +yellow1 FFFF00 +yellow2 EEEE00 +yellow3 CDCD00 +yellow4 8B8B00 +gold FFD700 +gold1 FFD700 +gold2 EEC900 +gold3 CDAD00 +gold4 8B7500 +Goldenrod DBDB70 +Medium Goldenrod EAEAAE +Yellow Green 99CC32 +copper B87333 +cool copper D98719 +Green Copper 856363 +brass B5A642 +bronze 8C7853 +bronze II A67D3D +bright gold D9D919 +Old Gold CFB53B +CSS Gold CC9900 +gold CD7F32 +silver E6E8FA +Silver, Grey C0C0C0 +Light Steel Blue 545454 +Steel Blue 236B8E diff --git a/tools/colors_find.py b/tools/colors_find.py new file mode 100755 index 00000000..cf66ef91 --- /dev/null +++ b/tools/colors_find.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os + +from colormath.color_objects import sRGBColor, LabColor +from colormath.color_conversions import convert_color +from colormath.color_diff import delta_e_cie2000 + + +def get_lab(name, rgb): + rgb = sRGBColor( + int(rgb[:2], 16), int(rgb[2:4], 16), int(rgb[4:6], 16), + is_upscaled=True + ) + lab = convert_color(rgb, LabColor) + return name, lab + + +with open(os.path.join(os.path.dirname(__file__), 'colors.map'), 'r') as f: + colors = [get_lab(*line.split('\t')) for line in f] + + +ulab = get_lab(None, sys.argv[1])[1] + + +def find_color(urgb, colors): + cur_distance = 3 * (255 ** 2 + 1) + cur_color = None + for color, clab in colors: + dist = delta_e_cie2000(ulab, clab) + if dist < cur_distance: + cur_distance = dist + cur_color = (color, clab) + return cur_color + + +cur_color = find_color(ulab, colors) + + +def lab_to_csi(lab): + rgb = convert_color(lab, sRGBColor) + colstr = ';2;' + ';'.join((str(i) for i in get_upscaled_values(rgb))) + return colstr + 'm' + + +def get_upscaled_values(rgb): + return [min(max(0, i), 255) for i in rgb.get_upscaled_value_tuple()] + + +def get_rgb(lab): + rgb = convert_color(lab, sRGBColor) + rgb = sRGBColor(*get_upscaled_values(rgb), is_upscaled=True) + return rgb.get_rgb_hex()[1:] + +print(get_rgb(ulab), ':', cur_color[0], ':', get_rgb(cur_color[1])) + +col_1 = lab_to_csi(ulab) +col_2 = lab_to_csi(cur_color[1]) +sys.stdout.write('\033[48' + col_1 + '\033[38' + col_2 + 'abc\033[0m <-- bg:urgb, fg:crgb\n') +sys.stdout.write('\033[48' + col_2 + '\033[38' + col_1 + 'abc\033[0m <-- bg:crgb, fg:urgb\n') diff --git a/tools/generate_gradients.py b/tools/generate_gradients.py new file mode 100755 index 00000000..290e75e4 --- /dev/null +++ b/tools/generate_gradients.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet + +'''Gradients generator +''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import json +import argparse + +from itertools import groupby + +from colormath.color_objects import sRGBColor, LabColor +from colormath.color_conversions import convert_color +from colormath.color_diff import delta_e_cie2000 + +from powerline.colorscheme import cterm_to_hex + + +def num2(s): + try: + return (True, [int(v) for v in s.partition(' ')[::2]]) + except TypeError: + return (False, [float(v) for v in s.partition(' ')[::2]]) + + +def rgbint_to_lab(rgbint): + rgb = sRGBColor( + (rgbint >> 16) & 0xFF, (rgbint >> 8) & 0xFF, rgbint & 0xFF, + is_upscaled=True + ) + return convert_color(rgb, LabColor) + + +cterm_to_lab = tuple((rgbint_to_lab(v) for v in cterm_to_hex)) + + +def color(s): + if len(s) <= 3: + return cterm_to_lab[int(s)] + else: + return rgbint_to_lab(int(s, 16)) + + +def nums(s): + return [int(i) for i in s.split()] + + +def linear_gradient(start_value, stop_value, start_offset, stop_offset, offset): + return start_value + ((offset - start_offset) * (stop_value - start_value) / (stop_offset - start_offset)) + + +def lab_gradient(slab, elab, soff, eoff, off): + svals = slab.get_value_tuple() + evals = elab.get_value_tuple() + return LabColor(*[ + linear_gradient(start_value, end_value, soff, eoff, off) + for start_value, end_value in zip(svals, evals) + ]) + + +def generate_gradient_function(DATA): + def gradient_function(y): + initial_offset = 0 + for offset, start, end in DATA: + if y <= offset: + return lab_gradient(start, end, initial_offset, offset, y) + initial_offset = offset + return gradient_function + + +def get_upscaled_values(rgb): + return [min(max(0, i), 255) for i in rgb.get_upscaled_value_tuple()] + + +def get_rgb(lab): + rgb = convert_color(lab, sRGBColor) + rgb = sRGBColor(*get_upscaled_values(rgb), is_upscaled=True) + return rgb.get_rgb_hex()[1:] + + +def find_color(ulab, colors, ctrans): + cur_distance = float('inf') + cur_color = None + i = 0 + for clab in colors: + dist = delta_e_cie2000(ulab, clab) + if dist < cur_distance: + cur_distance = dist + cur_color = (ctrans(i), clab) + i += 1 + return cur_color + + +def print_color(color): + if type(color) is int: + colstr = '5;' + str(color) + else: + rgb = convert_color(color, sRGBColor) + colstr = '2;' + ';'.join((str(i) for i in get_upscaled_values(rgb))) + sys.stdout.write('\033[48;' + colstr + 'm ') + + +def print_colors(colors, num): + for i in range(num): + color = colors[int(round(i * (len(colors) - 1) / num))] + print_color(color) + sys.stdout.write('\033[0m\n') + + +def dec_scale_generator(num): + j = 0 + r = '' + while num: + r += '\033[{0}m'.format(j % 2) + for i in range(10): + r += str(i) + num -= 1 + if not num: + break + j += 1 + r += '\033[0m\n' + return r + + +def compute_steps(gradient, weights): + maxweight = len(gradient) - 1 + if weights: + weight_sum = sum(weights) + norm_weights = [100.0 * weight / weight_sum for weight in weights] + steps = [0] + for weight in norm_weights: + steps.append(steps[-1] + weight) + steps.pop(0) + steps.pop(0) + else: + step = m / maxweight + steps = [i * step for i in range(1, maxweight + 1)] + return steps + + +palettes = { + '16': (cterm_to_lab[:16], lambda c: c), + '256': (cterm_to_lab, lambda c: c), + None: (cterm_to_lab[16:], lambda c: c + 16), +} + + +def show_scale(rng, num_output): + if not rng and num_output >= 32 and (num_output - 1) // 10 >= 4 and (num_output - 1) % 10 == 0: + sys.stdout.write('0') + sys.stdout.write(''.join(('%*u' % (num_output // 10, i) for i in range(10, 101, 10)))) + sys.stdout.write('\n') + else: + if rng: + vmin, vmax = rng[1] + isint = rng[0] + else: + isint = True + vmin = 0 + vmax = 100 + s = '' + lasts = ' ' + str(vmax) + while len(s) + len(lasts) < num_output: + curpc = len(s) + 1 if s else 0 + curval = vmin + curpc * (vmax - vmin) / num_output + if isint: + curval = int(round(curval)) + s += str(curval) + ' ' + sys.stdout.write(s[:-1] + lasts + '\n') + sys.stdout.write(dec_scale_generator(num_output) + '\n') + + +if __name__ == '__main__': + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('gradient', nargs='*', metavar='COLOR', type=color, help='List of colors (either indexes from 8-bit palette or 24-bit RGB in hexadecimal notation)') + p.add_argument('-n', '--num_items', metavar='INT', type=int, help='Number of items in resulting list', default=101) + p.add_argument('-N', '--num_output', metavar='INT', type=int, help='Number of characters in sample', default=101) + p.add_argument('-r', '--range', metavar='V1 V2', type=num2, help='Use this range when outputting scale') + p.add_argument('-s', '--show', action='store_true', help='If present output gradient sample') + p.add_argument('-p', '--palette', choices=('16', '256'), help='Use this palette. Defaults to 240-color palette (256 colors without first 16)') + p.add_argument('-w', '--weights', metavar='INT INT ...', type=nums, help='Adjust weights of colors. Number of weights must be equal to number of colors') + p.add_argument('-C', '--omit-terminal', action='store_true', help='If present do not compute values for terminal') + + args = p.parse_args() + + m = args.num_items + + steps = compute_steps(args.gradient, args.weights) + + data = [ + (weight, args.gradient[i - 1], args.gradient[i]) + for weight, i in zip(steps, range(1, len(args.gradient))) + ] + gr_func = generate_gradient_function(data) + gradient = [gr_func(y) for y in range(0, m)] + + r = [get_rgb(lab) for lab in gradient] + if not args.omit_terminal: + r2 = [find_color(lab, *palettes[args.palette])[0] for lab in gradient] + r3 = [i[0] for i in groupby(r2)] + + if not args.omit_terminal: + print(json.dumps(r3) + ',') + print(json.dumps(r2) + ',') + print(json.dumps(r)) + + if args.show: + print_colors(args.gradient, args.num_output) + if not args.omit_terminal: + print_colors(r3, args.num_output) + print_colors(r2, args.num_output) + print_colors(gradient, args.num_output) + + show_scale(args.range, args.num_output)