From 3f25952c13d5847d510c0cae22a8ba876638d570 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 18:40:16 +0200 Subject: Adding upstream version 2.8.3. Signed-off-by: Daniel Baumann --- .editorconfig | 20 + .gitattributes | 1 + .github/workflows/main.yaml | 46 + .gitignore | 16 + .local.vimrc | 11 + .travis.yml | 32 + CONTRIBUTING.rst | 138 ++ LICENSE | 21 + MANIFEST.in | 8 + README.rst | 120 ++ client/powerline.c | 164 ++ client/powerline.py | 104 ++ client/powerline.sh | 53 + docs/.gitignore | 1 + docs/Makefile | 39 + docs/source/_static/css/theme_overrides.css | 6 + docs/source/_static/img/icons/cross.png | Bin 0 -> 473 bytes docs/source/_static/img/icons/error.png | Bin 0 -> 543 bytes docs/source/_static/img/icons/tick.png | Bin 0 -> 451 bytes docs/source/_static/img/pl-mode-insert.png | Bin 0 -> 6894 bytes docs/source/_static/img/pl-mode-normal.png | Bin 0 -> 6879 bytes docs/source/_static/img/pl-mode-replace.png | Bin 0 -> 6949 bytes docs/source/_static/img/pl-mode-visual.png | Bin 0 -> 6868 bytes docs/source/_static/img/pl-truncate1.png | Bin 0 -> 6882 bytes docs/source/_static/img/pl-truncate2.png | Bin 0 -> 5541 bytes docs/source/_static/img/pl-truncate3.png | Bin 0 -> 3093 bytes docs/source/commands.rst | 9 + docs/source/commands/config.rst | 12 + docs/source/commands/daemon.rst | 12 + docs/source/commands/lint.rst | 14 + docs/source/commands/main.rst | 12 + docs/source/conf.py | 70 + docs/source/configuration.rst | 151 ++ docs/source/configuration/listers.rst | 35 + docs/source/configuration/local.rst | 260 +++ docs/source/configuration/reference.rst | 602 +++++++ docs/source/configuration/segments.rst | 28 + docs/source/configuration/segments/common.rst | 57 + docs/source/configuration/segments/i3wm.rst | 6 + docs/source/configuration/segments/pdb.rst | 7 + docs/source/configuration/segments/shell.rst | 6 + docs/source/configuration/segments/tmux.rst | 6 + docs/source/configuration/segments/vim.rst | 46 + docs/source/configuration/selectors.rst | 17 + docs/source/configuration/selectors/vim.rst | 6 + docs/source/develop.rst | 13 + docs/source/develop/extensions.rst | 47 + docs/source/develop/listers.rst | 49 + docs/source/develop/local-themes.rst | 59 + docs/source/develop/segments.rst | 547 ++++++ docs/source/develop/tips-and-tricks.rst | 21 + docs/source/index.rst | 28 + docs/source/installation.rst | 131 ++ docs/source/installation/linux.rst | 110 ++ docs/source/installation/osx.rst | 71 + docs/source/license-and-credits.rst | 31 + docs/source/overview.rst | 67 + docs/source/powerline_autodoc.py | 64 + docs/source/powerline_automan.py | 408 +++++ docs/source/tips-and-tricks.rst | 110 ++ docs/source/troubleshooting.rst | 333 ++++ docs/source/troubleshooting/linux.rst | 78 + docs/source/troubleshooting/osx.rst | 71 + docs/source/usage.rst | 88 + docs/source/usage/other.rst | 224 +++ docs/source/usage/shell-prompts.rst | 161 ++ docs/source/usage/wm-widgets.rst | 101 ++ font/10-powerline-symbols.conf | 105 ++ font/PowerlineSymbols.otf | Bin 0 -> 2264 bytes powerline/__init__.py | 991 +++++++++++ powerline/bindings/__init__.py | 0 powerline/bindings/awesome/powerline-awesome.py | 20 + powerline/bindings/awesome/powerline.lua | 15 + powerline/bindings/bar/powerline-bar.py | 60 + powerline/bindings/bash/powerline.sh | 153 ++ powerline/bindings/config.py | 286 +++ powerline/bindings/fish/powerline-setup.fish | 100 ++ powerline/bindings/i3/powerline-i3.py | 52 + powerline/bindings/ipython/__init__.py | 0 powerline/bindings/ipython/post_0_11.py | 126 ++ powerline/bindings/ipython/pre_0_11.py | 146 ++ powerline/bindings/ipython/since_5.py | 81 + powerline/bindings/ipython/since_7.py | 78 + powerline/bindings/lemonbar/powerline-lemonbar.py | 61 + powerline/bindings/pdb/__init__.py | 183 ++ powerline/bindings/pdb/__main__.py | 9 + powerline/bindings/qtile/__init__.py | 0 powerline/bindings/qtile/widget.py | 61 + powerline/bindings/rc/powerline.rc | 92 + powerline/bindings/shell/powerline.sh | 239 +++ powerline/bindings/tcsh/powerline.tcsh | 60 + powerline/bindings/tmux/__init__.py | 85 + powerline/bindings/tmux/powerline-base.conf | 11 + powerline/bindings/tmux/powerline.conf | 2 + .../bindings/tmux/powerline_tmux_1.7_plus.conf | 3 + powerline/bindings/tmux/powerline_tmux_1.8.conf | 5 + .../bindings/tmux/powerline_tmux_1.8_minus.conf | 11 + .../bindings/tmux/powerline_tmux_1.8_plus.conf | 5 + .../bindings/tmux/powerline_tmux_1.9_plus.conf | 9 + .../bindings/tmux/powerline_tmux_2.1_plus.conf | 3 + powerline/bindings/vim/__init__.py | 482 +++++ .../bindings/vim/autoload/powerline/debug.vim | 20 + powerline/bindings/vim/plugin/powerline.vim | 169 ++ powerline/bindings/wm/__init__.py | 72 + powerline/bindings/wm/awesome.py | 59 + powerline/bindings/zsh/__init__.py | 225 +++ powerline/bindings/zsh/powerline.zsh | 216 +++ powerline/colorscheme.py | 147 ++ powerline/commands/__init__.py | 0 powerline/commands/config.py | 109 ++ powerline/commands/daemon.py | 24 + powerline/commands/lemonbar.py | 35 + powerline/commands/lint.py | 21 + powerline/commands/main.py | 190 ++ powerline/config.py | 10 + powerline/config_files/colors.json | 124 ++ powerline/config_files/colorschemes/default.json | 57 + .../colorschemes/ipython/__main__.json | 6 + .../config_files/colorschemes/pdb/__main__.json | 8 + .../config_files/colorschemes/pdb/default.json | 5 + .../config_files/colorschemes/pdb/solarized.json | 5 + .../config_files/colorschemes/shell/__main__.json | 10 + .../config_files/colorschemes/shell/default.json | 16 + .../config_files/colorschemes/shell/solarized.json | 13 + powerline/config_files/colorschemes/solarized.json | 40 + .../config_files/colorschemes/tmux/default.json | 14 + .../config_files/colorschemes/tmux/solarized.json | 14 + .../config_files/colorschemes/vim/__main__.json | 51 + .../config_files/colorschemes/vim/default.json | 154 ++ .../config_files/colorschemes/vim/solarized.json | 121 ++ .../colorschemes/vim/solarizedlight.json | 122 ++ powerline/config_files/config.json | 53 + powerline/config_files/themes/ascii.json | 153 ++ powerline/config_files/themes/ipython/in.json | 25 + powerline/config_files/themes/ipython/in2.json | 12 + powerline/config_files/themes/ipython/out.json | 24 + powerline/config_files/themes/ipython/rewrite.json | 23 + powerline/config_files/themes/pdb/default.json | 27 + powerline/config_files/themes/powerline.json | 151 ++ .../config_files/themes/powerline_terminus.json | 151 ++ .../config_files/themes/powerline_unicode7.json | 165 ++ powerline/config_files/themes/shell/__main__.json | 14 + .../config_files/themes/shell/continuation.json | 12 + powerline/config_files/themes/shell/default.json | 43 + .../themes/shell/default_leftonly.json | 34 + powerline/config_files/themes/shell/select.json | 13 + powerline/config_files/themes/tmux/default.json | 28 + powerline/config_files/themes/unicode.json | 151 ++ .../config_files/themes/unicode_terminus.json | 151 ++ .../themes/unicode_terminus_condensed.json | 151 ++ powerline/config_files/themes/vim/__main__.json | 10 + powerline/config_files/themes/vim/cmdwin.json | 18 + powerline/config_files/themes/vim/default.json | 128 ++ powerline/config_files/themes/vim/help.json | 36 + .../config_files/themes/vim/plugin_commandt.json | 26 + .../themes/vim/plugin_gundo-preview.json | 18 + .../config_files/themes/vim/plugin_gundo.json | 18 + .../config_files/themes/vim/plugin_nerdtree.json | 17 + powerline/config_files/themes/vim/quickfix.json | 40 + powerline/config_files/themes/vim/tabline.json | 93 + powerline/config_files/themes/wm/default.json | 17 + powerline/dist/systemd/powerline-daemon.service | 10 + powerline/ipython.py | 68 + powerline/lemonbar.py | 21 + powerline/lib/__init__.py | 28 + powerline/lib/config.py | 218 +++ powerline/lib/debug.py | 97 + powerline/lib/dict.py | 88 + powerline/lib/encoding.py | 125 ++ powerline/lib/humanize_bytes.py | 25 + powerline/lib/inotify.py | 184 ++ powerline/lib/memoize.py | 42 + powerline/lib/monotonic.py | 100 ++ powerline/lib/overrides.py | 80 + powerline/lib/path.py | 18 + powerline/lib/shell.py | 133 ++ powerline/lib/threaded.py | 262 +++ powerline/lib/unicode.py | 283 +++ powerline/lib/url.py | 17 + powerline/lib/vcs/__init__.py | 276 +++ powerline/lib/vcs/bzr.py | 108 ++ powerline/lib/vcs/git.py | 208 +++ powerline/lib/vcs/mercurial.py | 88 + powerline/lib/watcher/__init__.py | 76 + powerline/lib/watcher/inotify.py | 268 +++ powerline/lib/watcher/stat.py | 44 + powerline/lib/watcher/tree.py | 90 + powerline/lib/watcher/uv.py | 207 +++ powerline/lint/__init__.py | 625 +++++++ powerline/lint/checks.py | 866 +++++++++ powerline/lint/context.py | 68 + powerline/lint/imp.py | 56 + powerline/lint/inspect.py | 63 + powerline/lint/markedjson/__init__.py | 19 + powerline/lint/markedjson/composer.py | 119 ++ powerline/lint/markedjson/constructor.py | 285 +++ powerline/lint/markedjson/error.py | 241 +++ powerline/lint/markedjson/events.py | 97 + powerline/lint/markedjson/loader.py | 25 + powerline/lint/markedjson/markedvalue.py | 151 ++ powerline/lint/markedjson/nodes.py | 55 + powerline/lint/markedjson/parser.py | 255 +++ powerline/lint/markedjson/reader.py | 141 ++ powerline/lint/markedjson/resolver.py | 131 ++ powerline/lint/markedjson/scanner.py | 499 ++++++ powerline/lint/markedjson/tokens.py | 72 + powerline/lint/selfcheck.py | 16 + powerline/lint/spec.py | 759 ++++++++ powerline/listers/__init__.py | 0 powerline/listers/i3wm.py | 63 + powerline/listers/pdb.py | 37 + powerline/listers/vim.py | 123 ++ powerline/matchers/__init__.py | 6 + powerline/matchers/vim/__init__.py | 19 + powerline/matchers/vim/plugin/__init__.py | 6 + powerline/matchers/vim/plugin/commandt.py | 14 + powerline/matchers/vim/plugin/gundo.py | 16 + powerline/matchers/vim/plugin/nerdtree.py | 15 + powerline/pdb.py | 48 + powerline/renderer.py | 606 +++++++ powerline/renderers/__init__.py | 0 powerline/renderers/i3bar.py | 36 + powerline/renderers/ipython/__init__.py | 34 + powerline/renderers/ipython/pre_5.py | 56 + powerline/renderers/ipython/since_5.py | 130 ++ powerline/renderers/ipython/since_7.py | 91 + powerline/renderers/lemonbar.py | 61 + powerline/renderers/pango_markup.py | 39 + powerline/renderers/pdb.py | 50 + powerline/renderers/shell/__init__.py | 182 ++ powerline/renderers/shell/bash.py | 96 + powerline/renderers/shell/ksh.py | 19 + powerline/renderers/shell/rcsh.py | 7 + powerline/renderers/shell/readline.py | 14 + powerline/renderers/shell/tcsh.py | 31 + powerline/renderers/shell/zsh.py | 16 + powerline/renderers/tmux.py | 81 + powerline/renderers/vim.py | 188 ++ powerline/segment.py | 450 +++++ powerline/segments/__init__.py | 63 + powerline/segments/common/__init__.py | 0 powerline/segments/common/bat.py | 302 ++++ powerline/segments/common/env.py | 201 +++ powerline/segments/common/mail.py | 78 + powerline/segments/common/net.py | 315 ++++ powerline/segments/common/players.py | 636 +++++++ powerline/segments/common/sys.py | 184 ++ powerline/segments/common/time.py | 123 ++ powerline/segments/common/vcs.py | 89 + powerline/segments/common/wthr.py | 234 +++ powerline/segments/i3wm.py | 309 ++++ powerline/segments/ipython.py | 9 + powerline/segments/pdb.py | 61 + powerline/segments/shell.py | 196 +++ powerline/segments/tmux.py | 22 + powerline/segments/vim/__init__.py | 805 +++++++++ powerline/segments/vim/plugin/__init__.py | 6 + powerline/segments/vim/plugin/ale.py | 52 + powerline/segments/vim/plugin/capslock.py | 30 + powerline/segments/vim/plugin/coc.py | 51 + powerline/segments/vim/plugin/commandt.py | 97 + powerline/segments/vim/plugin/nerdtree.py | 25 + powerline/segments/vim/plugin/syntastic.py | 43 + powerline/segments/vim/plugin/tagbar.py | 51 + powerline/selectors/__init__.py | 0 powerline/selectors/vim.py | 10 + powerline/shell.py | 38 + powerline/theme.py | 182 ++ powerline/version.py | 7 + powerline/vim.py | 359 ++++ scripts/.gitignore | 1 + scripts/powerline-config | 22 + scripts/powerline-daemon | 495 ++++++ scripts/powerline-lint | 13 + scripts/powerline-release.py | 242 +++ scripts/powerline-render | 31 + setup.py | 122 ++ tests/__init__.py | 0 tests/install.sh | 81 + tests/modules/__init__.py | 95 + tests/modules/lib/__init__.py | 191 ++ tests/modules/lib/config_mock.py | 230 +++ tests/modules/lib/fsconfig.py | 83 + tests/modules/lib/terminal.py | 307 ++++ tests/modules/lib/vterm.py | 193 ++ tests/modules/matchers.py | 6 + tests/modules/vim.py | 927 ++++++++++ tests/shlib/common.sh | 150 ++ tests/shlib/vim.sh | 33 + tests/shlib/vterm.sh | 17 + tests/terminfo/s/screen | Bin 0 -> 1564 bytes tests/terminfo/s/st-256color | Bin 0 -> 2324 bytes tests/test.sh | 42 + tests/test_awesome/path/awesome-client | 3 + tests/test_awesome/powerline/config.json | 7 + .../test_awesome/powerline/themes/wm/default.json | 18 + tests/test_awesome/powerline/themes/wm/dvi.json | 18 + tests/test_awesome/test.sh | 188 ++ tests/test_bar/path/lemonbar | 11 + tests/test_bar/path/xrandr | 31 + tests/test_bar/powerline/config.json | 9 + tests/test_bar/powerline/themes/wm/default.json | 18 + tests/test_bar/powerline/themes/wm/dvi.json | 18 + tests/test_bar/test.sh | 195 +++ tests/test_daemon/test.sh | 41 + tests/test_in_vterm/shell/inits/dash | 16 + tests/test_in_vterm/test.sh | 14 + tests/test_in_vterm/test_shells.py | 162 ++ tests/test_in_vterm/test_shells.sh | 119 ++ tests/test_in_vterm/test_tmux.py | 251 +++ tests/test_in_vterm/test_tmux.sh | 44 + tests/test_in_vterm/test_vim.py | 73 + tests/test_in_vterm/test_vim.sh | 39 + tests/test_lint/test.sh | 10 + tests/test_python/empty | 0 tests/test_python/test.sh | 13 + tests/test_python/test_cmdline.py | 149 ++ tests/test_python/test_config_merging.py | 270 +++ tests/test_python/test_config_reload.py | 319 ++++ tests/test_python/test_configuration.py | 877 ++++++++++ tests/test_python/test_lib.py | 733 ++++++++ tests/test_python/test_lib_config.py | 52 + tests/test_python/test_listers.py | 159 ++ tests/test_python/test_logging.py | 467 +++++ tests/test_python/test_provided_config_files.py | 201 +++ tests/test_python/test_segments.py | 1846 ++++++++++++++++++++ tests/test_python/test_selectors.py | 36 + tests/test_python/test_watcher.py | 245 +++ tests/test_shells/bgscript.sh | 5 + tests/test_shells/inputs/bash | 73 + tests/test_shells/inputs/busybox | 37 + tests/test_shells/inputs/dash | 37 + tests/test_shells/inputs/fish | 69 + tests/test_shells/inputs/ipython | 7 + tests/test_shells/inputs/mksh | 38 + tests/test_shells/inputs/pdb | 89 + tests/test_shells/inputs/rc | 33 + tests/test_shells/inputs/tcsh | 24 + tests/test_shells/inputs/zsh | 90 + .../ipython_home/profile_default/ipython_config.py | 22 + tests/test_shells/outputs/bash.daemon.ok | 45 + tests/test_shells/outputs/bash.nodaemon.ok | 45 + tests/test_shells/outputs/busybox.daemon.ok | 29 + tests/test_shells/outputs/busybox.nodaemon.ok | 29 + tests/test_shells/outputs/dash.daemon.ok | 28 + tests/test_shells/outputs/dash.nodaemon.ok | 28 + tests/test_shells/outputs/fish.ok | 52 + tests/test_shells/outputs/ipython.ok | 14 + tests/test_shells/outputs/mksh.daemon.ok | 32 + tests/test_shells/outputs/mksh.nodaemon.ok | 32 + tests/test_shells/outputs/pdb.module.ok | 222 +++ tests/test_shells/outputs/pdb.subclass.ok | 217 +++ tests/test_shells/outputs/rc.daemon.ok | 24 + tests/test_shells/outputs/rc.nodaemon.ok | 24 + tests/test_shells/outputs/tcsh.ok | 17 + tests/test_shells/outputs/zsh.daemon.ok | 52 + tests/test_shells/outputs/zsh.nodaemon.ok | 52 + tests/test_shells/outputs/zsh.zpython.ok | 52 + tests/test_shells/pdb-main.py | 24 + tests/test_shells/pdb-script.py | 38 + tests/test_shells/postproc.py | 130 ++ tests/test_shells/run_script.py | 125 ++ tests/test_shells/test.sh | 491 ++++++ tests/test_shells/waitpid.sh | 4 + tests/test_shells/zsh_test_script.zsh | 11 + tests/test_vim/pyfiles/setup_statusline_catcher.py | 18 + tests/test_vim/test.sh | 60 + tests/test_vim/tests/commandt_plugin.vim | 17 + tests/test_vim/tests/empty_encoding.old.vim | 32 + tests/test_vim/tests/foreign_stl_override.vim | 22 + tests/test_vim/tests/invalid_unicode.vim | 19 + tests/test_vim/tests/local_overrides.vim | 48 + tests/test_vim/tests/nerdtree_plugin.vim | 11 + tests/test_vim/tests/plugin_file.vim | 22 + tests/test_vim/tests/tabline.vim | 56 + tests/test_vim/vim_utils.vim | 88 + tests/vim_sys_path/vim.py | 7 + tools/colors.map | 646 +++++++ tools/colors_find.py | 63 + tools/generate_gradients.py | 217 +++ tools/purge-PRs.py | 27 + 381 files changed, 40787 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/main.yaml create mode 100644 .gitignore create mode 100644 .local.vimrc create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 client/powerline.c create mode 100755 client/powerline.py create mode 100755 client/powerline.sh create mode 100644 docs/.gitignore create mode 100644 docs/Makefile create mode 100644 docs/source/_static/css/theme_overrides.css create mode 100644 docs/source/_static/img/icons/cross.png create mode 100644 docs/source/_static/img/icons/error.png create mode 100644 docs/source/_static/img/icons/tick.png create mode 100644 docs/source/_static/img/pl-mode-insert.png create mode 100644 docs/source/_static/img/pl-mode-normal.png create mode 100644 docs/source/_static/img/pl-mode-replace.png create mode 100644 docs/source/_static/img/pl-mode-visual.png create mode 100644 docs/source/_static/img/pl-truncate1.png create mode 100644 docs/source/_static/img/pl-truncate2.png create mode 100644 docs/source/_static/img/pl-truncate3.png create mode 100644 docs/source/commands.rst create mode 100644 docs/source/commands/config.rst create mode 100644 docs/source/commands/daemon.rst create mode 100644 docs/source/commands/lint.rst create mode 100644 docs/source/commands/main.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/configuration.rst create mode 100644 docs/source/configuration/listers.rst create mode 100644 docs/source/configuration/local.rst create mode 100644 docs/source/configuration/reference.rst create mode 100644 docs/source/configuration/segments.rst create mode 100644 docs/source/configuration/segments/common.rst create mode 100644 docs/source/configuration/segments/i3wm.rst create mode 100644 docs/source/configuration/segments/pdb.rst create mode 100644 docs/source/configuration/segments/shell.rst create mode 100644 docs/source/configuration/segments/tmux.rst create mode 100644 docs/source/configuration/segments/vim.rst create mode 100644 docs/source/configuration/selectors.rst create mode 100644 docs/source/configuration/selectors/vim.rst create mode 100644 docs/source/develop.rst create mode 100644 docs/source/develop/extensions.rst create mode 100644 docs/source/develop/listers.rst create mode 100644 docs/source/develop/local-themes.rst create mode 100644 docs/source/develop/segments.rst create mode 100644 docs/source/develop/tips-and-tricks.rst create mode 100644 docs/source/index.rst create mode 100644 docs/source/installation.rst create mode 100644 docs/source/installation/linux.rst create mode 100644 docs/source/installation/osx.rst create mode 100644 docs/source/license-and-credits.rst create mode 100644 docs/source/overview.rst create mode 100644 docs/source/powerline_autodoc.py create mode 100644 docs/source/powerline_automan.py create mode 100644 docs/source/tips-and-tricks.rst create mode 100644 docs/source/troubleshooting.rst create mode 100644 docs/source/troubleshooting/linux.rst create mode 100644 docs/source/troubleshooting/osx.rst create mode 100644 docs/source/usage.rst create mode 100644 docs/source/usage/other.rst create mode 100644 docs/source/usage/shell-prompts.rst create mode 100644 docs/source/usage/wm-widgets.rst create mode 100644 font/10-powerline-symbols.conf create mode 100644 font/PowerlineSymbols.otf create mode 100644 powerline/__init__.py create mode 100644 powerline/bindings/__init__.py create mode 100755 powerline/bindings/awesome/powerline-awesome.py create mode 100644 powerline/bindings/awesome/powerline.lua create mode 100755 powerline/bindings/bar/powerline-bar.py create mode 100644 powerline/bindings/bash/powerline.sh create mode 100644 powerline/bindings/config.py create mode 100644 powerline/bindings/fish/powerline-setup.fish create mode 100755 powerline/bindings/i3/powerline-i3.py create mode 100644 powerline/bindings/ipython/__init__.py create mode 100644 powerline/bindings/ipython/post_0_11.py create mode 100644 powerline/bindings/ipython/pre_0_11.py create mode 100644 powerline/bindings/ipython/since_5.py create mode 100644 powerline/bindings/ipython/since_7.py create mode 100755 powerline/bindings/lemonbar/powerline-lemonbar.py create mode 100644 powerline/bindings/pdb/__init__.py create mode 100755 powerline/bindings/pdb/__main__.py create mode 100644 powerline/bindings/qtile/__init__.py create mode 100644 powerline/bindings/qtile/widget.py create mode 100644 powerline/bindings/rc/powerline.rc create mode 100644 powerline/bindings/shell/powerline.sh create mode 100644 powerline/bindings/tcsh/powerline.tcsh create mode 100644 powerline/bindings/tmux/__init__.py create mode 100644 powerline/bindings/tmux/powerline-base.conf create mode 100644 powerline/bindings/tmux/powerline.conf create mode 100644 powerline/bindings/tmux/powerline_tmux_1.7_plus.conf create mode 100644 powerline/bindings/tmux/powerline_tmux_1.8.conf create mode 100644 powerline/bindings/tmux/powerline_tmux_1.8_minus.conf create mode 100644 powerline/bindings/tmux/powerline_tmux_1.8_plus.conf create mode 100644 powerline/bindings/tmux/powerline_tmux_1.9_plus.conf create mode 100644 powerline/bindings/tmux/powerline_tmux_2.1_plus.conf create mode 100644 powerline/bindings/vim/__init__.py create mode 100644 powerline/bindings/vim/autoload/powerline/debug.vim create mode 100644 powerline/bindings/vim/plugin/powerline.vim create mode 100644 powerline/bindings/wm/__init__.py create mode 100644 powerline/bindings/wm/awesome.py create mode 100644 powerline/bindings/zsh/__init__.py create mode 100644 powerline/bindings/zsh/powerline.zsh create mode 100644 powerline/colorscheme.py create mode 100644 powerline/commands/__init__.py create mode 100644 powerline/commands/config.py create mode 100644 powerline/commands/daemon.py create mode 100644 powerline/commands/lemonbar.py create mode 100755 powerline/commands/lint.py create mode 100644 powerline/commands/main.py create mode 100644 powerline/config.py create mode 100644 powerline/config_files/colors.json create mode 100644 powerline/config_files/colorschemes/default.json create mode 100644 powerline/config_files/colorschemes/ipython/__main__.json create mode 100644 powerline/config_files/colorschemes/pdb/__main__.json create mode 100644 powerline/config_files/colorschemes/pdb/default.json create mode 100644 powerline/config_files/colorschemes/pdb/solarized.json create mode 100644 powerline/config_files/colorschemes/shell/__main__.json create mode 100644 powerline/config_files/colorschemes/shell/default.json create mode 100644 powerline/config_files/colorschemes/shell/solarized.json create mode 100644 powerline/config_files/colorschemes/solarized.json create mode 100644 powerline/config_files/colorschemes/tmux/default.json create mode 100644 powerline/config_files/colorschemes/tmux/solarized.json create mode 100644 powerline/config_files/colorschemes/vim/__main__.json create mode 100644 powerline/config_files/colorschemes/vim/default.json create mode 100644 powerline/config_files/colorschemes/vim/solarized.json create mode 100644 powerline/config_files/colorschemes/vim/solarizedlight.json create mode 100644 powerline/config_files/config.json create mode 100644 powerline/config_files/themes/ascii.json create mode 100644 powerline/config_files/themes/ipython/in.json create mode 100644 powerline/config_files/themes/ipython/in2.json create mode 100644 powerline/config_files/themes/ipython/out.json create mode 100644 powerline/config_files/themes/ipython/rewrite.json create mode 100644 powerline/config_files/themes/pdb/default.json create mode 100644 powerline/config_files/themes/powerline.json create mode 100644 powerline/config_files/themes/powerline_terminus.json create mode 100644 powerline/config_files/themes/powerline_unicode7.json create mode 100644 powerline/config_files/themes/shell/__main__.json create mode 100644 powerline/config_files/themes/shell/continuation.json create mode 100644 powerline/config_files/themes/shell/default.json create mode 100644 powerline/config_files/themes/shell/default_leftonly.json create mode 100644 powerline/config_files/themes/shell/select.json create mode 100644 powerline/config_files/themes/tmux/default.json create mode 100644 powerline/config_files/themes/unicode.json create mode 100644 powerline/config_files/themes/unicode_terminus.json create mode 100644 powerline/config_files/themes/unicode_terminus_condensed.json create mode 100644 powerline/config_files/themes/vim/__main__.json create mode 100644 powerline/config_files/themes/vim/cmdwin.json create mode 100644 powerline/config_files/themes/vim/default.json create mode 100644 powerline/config_files/themes/vim/help.json create mode 100644 powerline/config_files/themes/vim/plugin_commandt.json create mode 100644 powerline/config_files/themes/vim/plugin_gundo-preview.json create mode 100644 powerline/config_files/themes/vim/plugin_gundo.json create mode 100644 powerline/config_files/themes/vim/plugin_nerdtree.json create mode 100644 powerline/config_files/themes/vim/quickfix.json create mode 100644 powerline/config_files/themes/vim/tabline.json create mode 100644 powerline/config_files/themes/wm/default.json create mode 100644 powerline/dist/systemd/powerline-daemon.service create mode 100644 powerline/ipython.py create mode 100644 powerline/lemonbar.py create mode 100644 powerline/lib/__init__.py create mode 100644 powerline/lib/config.py create mode 100755 powerline/lib/debug.py create mode 100644 powerline/lib/dict.py create mode 100644 powerline/lib/encoding.py create mode 100644 powerline/lib/humanize_bytes.py create mode 100644 powerline/lib/inotify.py create mode 100644 powerline/lib/memoize.py create mode 100644 powerline/lib/monotonic.py create mode 100644 powerline/lib/overrides.py create mode 100644 powerline/lib/path.py create mode 100644 powerline/lib/shell.py create mode 100644 powerline/lib/threaded.py create mode 100644 powerline/lib/unicode.py create mode 100644 powerline/lib/url.py create mode 100644 powerline/lib/vcs/__init__.py create mode 100644 powerline/lib/vcs/bzr.py create mode 100644 powerline/lib/vcs/git.py create mode 100644 powerline/lib/vcs/mercurial.py create mode 100644 powerline/lib/watcher/__init__.py create mode 100644 powerline/lib/watcher/inotify.py create mode 100644 powerline/lib/watcher/stat.py create mode 100644 powerline/lib/watcher/tree.py create mode 100644 powerline/lib/watcher/uv.py create mode 100644 powerline/lint/__init__.py create mode 100644 powerline/lint/checks.py create mode 100644 powerline/lint/context.py create mode 100644 powerline/lint/imp.py create mode 100644 powerline/lint/inspect.py create mode 100644 powerline/lint/markedjson/__init__.py create mode 100644 powerline/lint/markedjson/composer.py create mode 100644 powerline/lint/markedjson/constructor.py create mode 100644 powerline/lint/markedjson/error.py create mode 100644 powerline/lint/markedjson/events.py create mode 100644 powerline/lint/markedjson/loader.py create mode 100644 powerline/lint/markedjson/markedvalue.py create mode 100644 powerline/lint/markedjson/nodes.py create mode 100644 powerline/lint/markedjson/parser.py create mode 100644 powerline/lint/markedjson/reader.py create mode 100644 powerline/lint/markedjson/resolver.py create mode 100644 powerline/lint/markedjson/scanner.py create mode 100644 powerline/lint/markedjson/tokens.py create mode 100644 powerline/lint/selfcheck.py create mode 100644 powerline/lint/spec.py create mode 100644 powerline/listers/__init__.py create mode 100644 powerline/listers/i3wm.py create mode 100644 powerline/listers/pdb.py create mode 100644 powerline/listers/vim.py create mode 100644 powerline/matchers/__init__.py create mode 100644 powerline/matchers/vim/__init__.py create mode 100644 powerline/matchers/vim/plugin/__init__.py create mode 100644 powerline/matchers/vim/plugin/commandt.py create mode 100644 powerline/matchers/vim/plugin/gundo.py create mode 100644 powerline/matchers/vim/plugin/nerdtree.py create mode 100644 powerline/pdb.py create mode 100644 powerline/renderer.py create mode 100644 powerline/renderers/__init__.py create mode 100644 powerline/renderers/i3bar.py create mode 100644 powerline/renderers/ipython/__init__.py create mode 100644 powerline/renderers/ipython/pre_5.py create mode 100644 powerline/renderers/ipython/since_5.py create mode 100644 powerline/renderers/ipython/since_7.py create mode 100644 powerline/renderers/lemonbar.py create mode 100644 powerline/renderers/pango_markup.py create mode 100644 powerline/renderers/pdb.py create mode 100644 powerline/renderers/shell/__init__.py create mode 100644 powerline/renderers/shell/bash.py create mode 100644 powerline/renderers/shell/ksh.py create mode 100644 powerline/renderers/shell/rcsh.py create mode 100644 powerline/renderers/shell/readline.py create mode 100644 powerline/renderers/shell/tcsh.py create mode 100644 powerline/renderers/shell/zsh.py create mode 100644 powerline/renderers/tmux.py create mode 100644 powerline/renderers/vim.py create mode 100644 powerline/segment.py create mode 100644 powerline/segments/__init__.py create mode 100644 powerline/segments/common/__init__.py create mode 100644 powerline/segments/common/bat.py create mode 100644 powerline/segments/common/env.py create mode 100644 powerline/segments/common/mail.py create mode 100644 powerline/segments/common/net.py create mode 100644 powerline/segments/common/players.py create mode 100644 powerline/segments/common/sys.py create mode 100644 powerline/segments/common/time.py create mode 100644 powerline/segments/common/vcs.py create mode 100644 powerline/segments/common/wthr.py create mode 100644 powerline/segments/i3wm.py create mode 100644 powerline/segments/ipython.py create mode 100644 powerline/segments/pdb.py create mode 100644 powerline/segments/shell.py create mode 100644 powerline/segments/tmux.py create mode 100644 powerline/segments/vim/__init__.py create mode 100644 powerline/segments/vim/plugin/__init__.py create mode 100644 powerline/segments/vim/plugin/ale.py create mode 100644 powerline/segments/vim/plugin/capslock.py create mode 100644 powerline/segments/vim/plugin/coc.py create mode 100644 powerline/segments/vim/plugin/commandt.py create mode 100644 powerline/segments/vim/plugin/nerdtree.py create mode 100644 powerline/segments/vim/plugin/syntastic.py create mode 100644 powerline/segments/vim/plugin/tagbar.py create mode 100644 powerline/selectors/__init__.py create mode 100644 powerline/selectors/vim.py create mode 100644 powerline/shell.py create mode 100644 powerline/theme.py create mode 100644 powerline/version.py create mode 100644 powerline/vim.py create mode 100644 scripts/.gitignore create mode 100755 scripts/powerline-config create mode 100755 scripts/powerline-daemon create mode 100755 scripts/powerline-lint create mode 100755 scripts/powerline-release.py create mode 100755 scripts/powerline-render create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100755 tests/install.sh create mode 100644 tests/modules/__init__.py create mode 100644 tests/modules/lib/__init__.py create mode 100644 tests/modules/lib/config_mock.py create mode 100644 tests/modules/lib/fsconfig.py create mode 100644 tests/modules/lib/terminal.py create mode 100644 tests/modules/lib/vterm.py create mode 100644 tests/modules/matchers.py create mode 100644 tests/modules/vim.py create mode 100644 tests/shlib/common.sh create mode 100644 tests/shlib/vim.sh create mode 100644 tests/shlib/vterm.sh create mode 100644 tests/terminfo/s/screen create mode 100644 tests/terminfo/s/st-256color create mode 100755 tests/test.sh create mode 100755 tests/test_awesome/path/awesome-client create mode 100644 tests/test_awesome/powerline/config.json create mode 100644 tests/test_awesome/powerline/themes/wm/default.json create mode 100644 tests/test_awesome/powerline/themes/wm/dvi.json create mode 100755 tests/test_awesome/test.sh create mode 100755 tests/test_bar/path/lemonbar create mode 100755 tests/test_bar/path/xrandr create mode 100644 tests/test_bar/powerline/config.json create mode 100644 tests/test_bar/powerline/themes/wm/default.json create mode 100644 tests/test_bar/powerline/themes/wm/dvi.json create mode 100755 tests/test_bar/test.sh create mode 100755 tests/test_daemon/test.sh create mode 100644 tests/test_in_vterm/shell/inits/dash create mode 100755 tests/test_in_vterm/test.sh create mode 100755 tests/test_in_vterm/test_shells.py create mode 100755 tests/test_in_vterm/test_shells.sh create mode 100755 tests/test_in_vterm/test_tmux.py create mode 100755 tests/test_in_vterm/test_tmux.sh create mode 100755 tests/test_in_vterm/test_vim.py create mode 100755 tests/test_in_vterm/test_vim.sh create mode 100755 tests/test_lint/test.sh create mode 100644 tests/test_python/empty create mode 100755 tests/test_python/test.sh create mode 100644 tests/test_python/test_cmdline.py create mode 100644 tests/test_python/test_config_merging.py create mode 100644 tests/test_python/test_config_reload.py create mode 100644 tests/test_python/test_configuration.py create mode 100644 tests/test_python/test_lib.py create mode 100644 tests/test_python/test_lib_config.py create mode 100644 tests/test_python/test_listers.py create mode 100644 tests/test_python/test_logging.py create mode 100644 tests/test_python/test_provided_config_files.py create mode 100644 tests/test_python/test_segments.py create mode 100644 tests/test_python/test_selectors.py create mode 100644 tests/test_python/test_watcher.py create mode 100755 tests/test_shells/bgscript.sh create mode 100644 tests/test_shells/inputs/bash create mode 100644 tests/test_shells/inputs/busybox create mode 100644 tests/test_shells/inputs/dash create mode 100644 tests/test_shells/inputs/fish create mode 100644 tests/test_shells/inputs/ipython create mode 100644 tests/test_shells/inputs/mksh create mode 100644 tests/test_shells/inputs/pdb create mode 100644 tests/test_shells/inputs/rc create mode 100644 tests/test_shells/inputs/tcsh create mode 100644 tests/test_shells/inputs/zsh create mode 100644 tests/test_shells/ipython_home/profile_default/ipython_config.py create mode 100644 tests/test_shells/outputs/bash.daemon.ok create mode 100644 tests/test_shells/outputs/bash.nodaemon.ok create mode 100644 tests/test_shells/outputs/busybox.daemon.ok create mode 100644 tests/test_shells/outputs/busybox.nodaemon.ok create mode 100644 tests/test_shells/outputs/dash.daemon.ok create mode 100644 tests/test_shells/outputs/dash.nodaemon.ok create mode 100644 tests/test_shells/outputs/fish.ok create mode 100644 tests/test_shells/outputs/ipython.ok create mode 100644 tests/test_shells/outputs/mksh.daemon.ok create mode 100644 tests/test_shells/outputs/mksh.nodaemon.ok create mode 100644 tests/test_shells/outputs/pdb.module.ok create mode 100644 tests/test_shells/outputs/pdb.subclass.ok create mode 100644 tests/test_shells/outputs/rc.daemon.ok create mode 100644 tests/test_shells/outputs/rc.nodaemon.ok create mode 100644 tests/test_shells/outputs/tcsh.ok create mode 100644 tests/test_shells/outputs/zsh.daemon.ok create mode 100644 tests/test_shells/outputs/zsh.nodaemon.ok create mode 100644 tests/test_shells/outputs/zsh.zpython.ok create mode 100644 tests/test_shells/pdb-main.py create mode 100644 tests/test_shells/pdb-script.py create mode 100755 tests/test_shells/postproc.py create mode 100755 tests/test_shells/run_script.py create mode 100755 tests/test_shells/test.sh create mode 100755 tests/test_shells/waitpid.sh create mode 100644 tests/test_shells/zsh_test_script.zsh create mode 100644 tests/test_vim/pyfiles/setup_statusline_catcher.py create mode 100755 tests/test_vim/test.sh create mode 100755 tests/test_vim/tests/commandt_plugin.vim create mode 100755 tests/test_vim/tests/empty_encoding.old.vim create mode 100644 tests/test_vim/tests/foreign_stl_override.vim create mode 100644 tests/test_vim/tests/invalid_unicode.vim create mode 100755 tests/test_vim/tests/local_overrides.vim create mode 100755 tests/test_vim/tests/nerdtree_plugin.vim create mode 100755 tests/test_vim/tests/plugin_file.vim create mode 100755 tests/test_vim/tests/tabline.vim create mode 100644 tests/test_vim/vim_utils.vim create mode 100644 tests/vim_sys_path/vim.py create mode 100644 tools/colors.map create mode 100755 tools/colors_find.py create mode 100755 tools/generate_gradients.py create mode 100755 tools/purge-PRs.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..94eab89 --- /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 0000000..e307485 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.rst whitespace=-blank-at-eol diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..ea7f7f9 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,46 @@ +name: Build and Publish to PyPI + +on: + push: + branches: + - master + - develop + - feature/actions + pull_request: + branches: + - develop + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Build + run: | + python setup.py sdist bdist_wheel + + - name: Publish + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + packages_dir: dist/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6491a77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +tags + +*.py[co] +__pycache__ + +*.egg +*.egg-info +dist +build + +message.fail + +/client/powerline + +/tests/tmp +/tests/status diff --git a/.local.vimrc b/.local.vimrc new file mode 100644 index 0000000..c8e1ef3 --- /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 0000000..57ef8e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +sudo: false +dist: trusty +cache: + directories: + - $HOME/.cache/pip + - tests/bot-ci +addons: + apt: + packages: + - libssl1.0.0 + - zsh + - tcsh + - mksh + - busybox + # - rc + - socat + - bc +language: python +install: tests/install.sh +script: tests/test.sh +jobs: + include: + - stage: PyPy + python: "pypy" + - python: "pypy3" + - stage: Latest Python + python: "3.6" + - stage: Intermediate versions + python: "3.5" + - python: "3.4" + +# vim: et diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..3792b70 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,138 @@ +***************** +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''' + + from __future__ import (unicode_literals, division, absolute_import, print_function) + + 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 `powerline repository + `_. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..23b2ab2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Copyright 2013 Kim Silkebækken and other contributors +https://github.com/powerline/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 0000000..1ebcf9c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +recursive-include powerline *.json *.vim +recursive-include powerline/bindings *.* +recursive-exclude powerline/bindings *.pyc *.pyo +recursive-include powerline/dist *.* +recursive-include client *.* +recursive-include docs/source *.rst *.py +include docs/Makefile +include LICENSE diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..0c37ac2 --- /dev/null +++ b/README.rst @@ -0,0 +1,120 @@ +Powerline +========= + +.. image:: https://api.travis-ci.org/powerline/powerline.svg?branch=develop + :target: `travis-build-status`_ + :alt: Build status +.. _travis-build-status: https://travis-ci.org/powerline/powerline + +**Powerline is a statusline plugin for vim, and provides statuslines and +prompts for several other applications, including zsh, bash, fish, tmux, +IPython, Awesome, i3 and Qtile.** + ++---------+---------------------------------------------------+ +| Author | Kim Silkebækken (kim.silkebaekken+vim@gmail.com) | ++---------+---------------------------------------------------+ +| Source | https://github.com/powerline/powerline | ++---------+---------------------------------------------------+ +| Version | beta | ++---------+---------------------------------------------------+ + +**Powerline does not support python2 anymore and powerline will stop working with python2 in the near future.** + +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 3.6+. +* **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 the `vim-airline +`_ project. + +Configuration +------------- + +Basic powerline configuration is done via `JSON` files located at `.config/powerline/`. It is a good idea to start by copying the default configuration located at `powerline_root/powerline/config_files/` to `.config/powerline/`. +If you installed the powerline from the AUR or via pip, `powerline_root` should be `/usr/lib/python3.6/site-packages/` or something similar, depending on your python version. + +If you installed powerline via apt-get 'powerline_root' should be '/usr/share/powerline/'. + +This should yield you the following directory structure: + + :: + + .config/powerline/ + ├── colorschemes + │   ├── ... + │   └── wm + |      └── default.json // Your configuration goes here + ├── colors.json + ├── config.json + └── themes + ├── ... + └── wm + └── default.json // Your configuration goes here + + + +The files in the subdirectories of `themes` are used to specify which segments shall be shown; the files in subdirectories of `colorschemes` are used to specify which colors (as defined in `colors.json`) shall be used to display a segment. + +Note that your local configuration only overrides the global configuration, it does not replace it, i.e. if you don't configure something locally, the global default will be used instead. + +* Consult the `documentation `_ for more details. See also the `segment reference `_ for available segments and their configuration. +* 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/powerline/powerline/develop/docs/source/_static/img/pl-mode-normal.png + :alt: Normal mode +* .. image:: https://raw.github.com/powerline/powerline/develop/docs/source/_static/img/pl-mode-insert.png + :alt: Insert mode +* .. image:: https://raw.github.com/powerline/powerline/develop/docs/source/_static/img/pl-mode-visual.png + :alt: Visual mode +* .. image:: https://raw.github.com/powerline/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/powerline/powerline/develop/docs/source/_static/img/pl-truncate1.png + :alt: Truncation illustration +* .. image:: https://raw.github.com/powerline/powerline/develop/docs/source/_static/img/pl-truncate2.png + :alt: Truncation illustration +* .. image:: https://raw.github.com/powerline/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/shop/fonts/pragmatapro diff --git a/client/powerline.c b/client/powerline.c new file mode 100644 index 0000000..ff107ec --- /dev/null +++ b/client/powerline.c @@ -0,0 +1,164 @@ +/* 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; + } +} + +static inline size_t true_sun_len(const struct sockaddr_un *ptr) { +#ifdef __linux__ + /* Because SUN_LEN uses strlen and abstract namespace paths begin + * with a null byte, SUN_LEN is broken for these. Passing the full + * struct size also fails on Linux, so compute manually. The + * abstract namespace is Linux-only. */ + if (ptr->sun_path[0] == '\0') { + return sizeof(ptr->sun_family) + strlen(ptr->sun_path + 1) + 1; + } +#endif +#ifdef SUN_LEN + /* If the vendor provided SUN_LEN, we may as well use it. */ + return SUN_LEN(ptr); +#else + /* SUN_LEN is not POSIX, so if it was not provided, use the struct + * size as a fallback. */ + return sizeof(struct sockaddr_un); +#endif +} + +#ifdef __linux__ +# define ADDRESS_TEMPLATE "powerline-ipc-%d" +# define A +1 +#else +# define ADDRESS_TEMPLATE "/tmp/powerline-ipc-%d" +# define A +#endif + +#define ADDRESS_SIZE sizeof(ADDRESS_TEMPLATE) + (sizeof(uid_t) * 4) +#define NUM_ARGS_SIZE (sizeof(int) * 2 + 1) +#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; + int len; + + 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, true_sun_len(&server)) < 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); + } + + len = snprintf(num_args, NUM_ARGS_SIZE, "%x", argc - 1); + do_write(sd, num_args, len); + 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 0000000..28492c1 --- /dev/null +++ b/client/powerline.py @@ -0,0 +1,104 @@ +#!/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 + +try: + from posix import environ +except ImportError: + from os import environ + +# XXX Hack for importing powerline modules to work. +sys.path.pop(0) + +try: + from powerline.lib.encoding import get_preferred_output_encoding +except ImportError: + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__))))) + from powerline.lib.encoding import get_preferred_output_encoding + + +if len(sys.argv) < 2: + print('Must provide at least one argument.', file=sys.stderr) + raise SystemExit(1) + +use_filesystem = not sys.platform.lower().startswith('linux') + +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 = get_preferred_output_encoding() + + +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 0000000..ad278c2 --- /dev/null +++ b/client/powerline.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +use_filesystem=1 +darwin= +if test -n "$OSTYPE" ; then + # OSTYPE variable is a shell feature. supported by bash and zsh, but not + # dash, busybox or (m)ksh. + if test "${OSTYPE#linux}" '!=' "${OSTYPE}" ; then + use_filesystem= + elif test "${OSTYPE#darwin}" ; then + darwin=1 + fi +elif command -v uname >/dev/null ; then + if uname -o | grep -iqF linux ; then + use_filesystem= + elif uname -o | grep -iqF darwin ; then + darwin=1 + fi +fi + +if test "$1" = "--socket" ; then + shift + ADDRESS="$1" + shift +else + ADDRESS="powerline-ipc-${UID:-`id -u`}" + test -n "$use_filesystem" && ADDRESS="/tmp/$ADDRESS" +fi + +if test -n "$darwin" ; then + ENV=genv +else + ENV=env +fi + +if test -z "$use_filesystem" ; then + 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 +) 2>/dev/null | 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 0000000..e35d885 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +_build diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..3b41221 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,39 @@ +# Makefile for Sphinx documentation +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -T -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +GH_PAGES_SOURCES = source Makefile +GH_SOURCE_BRANCH = develop + +.PHONY: html clean html latexpdf + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + +clean: + -rm -rf $(BUILDDIR)/* + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." diff --git a/docs/source/_static/css/theme_overrides.css b/docs/source/_static/css/theme_overrides.css new file mode 100644 index 0000000..7e39e34 --- /dev/null +++ b/docs/source/_static/css/theme_overrides.css @@ -0,0 +1,6 @@ +.wy-table-responsive > table > tbody > tr > td { + white-space: unset; +} +.wy-table-responsive > table > tbody > tr > td:first-child { + vertical-align: top; +} diff --git a/docs/source/_static/img/icons/cross.png b/docs/source/_static/img/icons/cross.png new file mode 100644 index 0000000..33a3837 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 0000000..dbfda22 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 0000000..c277e6b 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 0000000..9b09e18 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 0000000..29d3716 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 0000000..d7c89a4 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 0000000..d654763 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 0000000..c687502 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 0000000..1630f1d 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 0000000..83e5b21 Binary files /dev/null and b/docs/source/_static/img/pl-truncate3.png differ diff --git a/docs/source/commands.rst b/docs/source/commands.rst new file mode 100644 index 0000000..a35d05f --- /dev/null +++ b/docs/source/commands.rst @@ -0,0 +1,9 @@ +************************************** +Powerline shell commands’ manual pages +************************************** + +.. toctree:: + :maxdepth: 1 + :glob: + + commands/* diff --git a/docs/source/commands/config.rst b/docs/source/commands/config.rst new file mode 100644 index 0000000..3fc25aa --- /dev/null +++ b/docs/source/commands/config.rst @@ -0,0 +1,12 @@ +:orphan: + +powerline-config manual page +============================ + +.. automan:: powerline.commands.config + :prog: powerline-config + +See also +-------- + +:manpage:`powerline(1)` diff --git a/docs/source/commands/daemon.rst b/docs/source/commands/daemon.rst new file mode 100644 index 0000000..d899c73 --- /dev/null +++ b/docs/source/commands/daemon.rst @@ -0,0 +1,12 @@ +:orphan: + +powerline-daemon manual page +============================ + +.. automan:: powerline.commands.daemon + :prog: powerline-daemon + +See also +-------- + +:manpage:`powerline(1)` diff --git a/docs/source/commands/lint.rst b/docs/source/commands/lint.rst new file mode 100644 index 0000000..92d676d --- /dev/null +++ b/docs/source/commands/lint.rst @@ -0,0 +1,14 @@ +:orphan: + +.. _command-powerline-lint: + +powerline-lint manual page +========================== + +.. automan:: powerline.commands.lint + :prog: powerline-lint + +See also +-------- + +:manpage:`powerline(1)`, :manpage:`powerline-config(1)` diff --git a/docs/source/commands/main.rst b/docs/source/commands/main.rst new file mode 100644 index 0000000..178bcb9 --- /dev/null +++ b/docs/source/commands/main.rst @@ -0,0 +1,12 @@ +:orphan: + +powerline manual page +===================== + +.. automan:: powerline.commands.main + :prog: powerline + +See also +-------- + +:manpage:`powerline-daemon(1)`, :manpage:`powerline-config(1)` diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..5577ef6 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,70 @@ +# 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', 'powerline_automan', + 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', +] +source_suffix = '.rst' +master_doc = 'index' +project = 'Powerline' +version = 'beta' +release = 'beta' +exclude_patterns = ['_build'] +pygments_style = 'sphinx' + +html_theme = 'default' +html_static_path = ['_static'] +html_show_copyright = False + +latex_show_urls = 'footnote' +latex_elements = { + 'preamble': ''' + \\DeclareUnicodeCharacter{22EF}{$\\cdots$} % Dots + \\DeclareUnicodeCharacter{2665}{\\ding{170}} % Heart + \\DeclareUnicodeCharacter{2746}{\\ding{105}} % Snow + \\usepackage{pifont} + ''', +} + +man_pages = [] +for doc in os.listdir(os.path.join(os.path.dirname(__file__), 'commands')): + if doc.endswith('.rst'): + name = doc[:-4] + module = 'powerline.commands.{0}'.format(name) + get_argparser = __import__(str(module), fromlist=[str('get_argparser')]).get_argparser + parser = get_argparser() + description = parser.description + man_pages.append([ + 'commands/' + name, + 'powerline' if name == 'main' else 'powerline-' + name, + description, + '', + 1 + ]) + +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 + +if on_rtd or html_theme == 'sphinx_rtd_theme': + html_context = { + 'css_files': [ + 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', + 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', + '_static/css/theme_overrides.css', + ], + } diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst new file mode 100644 index 0000000..a2be2d5 --- /dev/null +++ b/docs/source/configuration.rst @@ -0,0 +1,151 @@ +******************************* +Configuration and customization +******************************* + +.. note:: + **Forking the main GitHub repo is not needed to personalize 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}/colorschemes/{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` + +Here `{powerline}` is one of the following: + +#. The default configuration directory located in the main package: + :file:`{powerline_root}/powerline/config_files`. May be absent in some + packages (e.g. when installing via Gentoo ebuilds). +#. If variable ``$XDG_CONFIG_DIRS`` is set and non-empty then to any + :file:`{directory}/powerline` where `{directory}` is a directory listed in + a colon-separated ``$XDG_CONFIG_DIRS`` list. Directories are checked in + reverse order. +#. User configuration directory located in :file:`$XDG_CONFIG_HOME/powerline`. + This usually corresponds to :file:`~/.config/powerline` on all platforms. + +If per-instance configuration is needed please refer to :ref:`Local +configuration overrides `. + +.. _configuration-merging: + +.. note:: + Existing multiple configuration files that have the same name, but are placed + in different directories, will be merged. Merging happens in the order given + in the above list of possible `{powerline}` meanings. + + 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. + +Look at configuration in :file:`{powerline_root}/powerline/config_files`. If you +want to modify some file you can create :file:`~/.config/powerline` directory +and put modifications there: all configuration files are :ref:`merged +` with each other. + +Each extension (vim, tmux, etc.) has its own theme, and they are located in +:file:`{config directory}/themes/{extension}/default.json`. Best way to modify +it is to copy this theme as a whole, remove ``segment_data`` key with +corresponding value if present (unless you need to modify it, in which case only +modifications must be left) and do necessary modifications in the list of +segments (lists are not subject to merging: this is why you need a copy). + +If you want to move, remove or customize any of the provided segments in the +copy, 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. + +.. note:: + If your modifications appear not to work, run :ref:`powerline-lint script + `. This script should show you the location of the + error. + +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. + + It is using OpenWeatherMap as a provider, which can be configured with a + personal API key. These can be generated `here + `_ + + 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 or + api key arguments: + + .. code-block:: javascript + + { + "name": "weather", + "priority": 50, + "args": { + "unit": "F", + "location_query": "oslo, norway", + "weather_api_key": "your_api_key" + } + }, + +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 0000000..7aaaabc --- /dev/null +++ b/docs/source/configuration/listers.rst @@ -0,0 +1,35 @@ +.. _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. + +Vim listers +----------- + +.. automodule:: powerline.listers.vim + :members: + +Pdb listers +----------- + +.. automodule:: powerline.listers.pdb + :members: + +i3wm listers +------------ + +.. automodule:: powerline.listers.i3wm + :members: diff --git a/docs/source/configuration/local.rst b/docs/source/configuration/local.rst new file mode 100644 index 0000000..0f3d110 --- /dev/null +++ b/docs/source/configuration/local.rst @@ -0,0 +1,260 @@ +.. _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: + +.. _local-configuration-overrides-vim-config: + +``g:powerline_config_overrides`` + Dictionary, recursively merged with contents of + :file:`powerline/config.json`. + +``g:powerline_theme_overrides`` + Dictionary mapping theme names to theme overrides, recursively merged with + contents of :file:`powerline/themes/vim/{key}.json`. Note that this way some + value (e.g. segment) in a list cannot be redefined, 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. + +``g:powerline_use_var_handler`` + This variable may be set to either 0 or 1. If it is set to 1 then Vim will + save log in ``g:powerline_log_messages`` variable in addition to whatever + was configured in :ref:`log_* options `. Level is always + :ref:`log_level `, same for format. + + .. warning:: + This variable is deprecated. Use :ref:`log_file option + ` in conjunction with + :py:class:`powerline.vim.VimVarHandler` class and :ref:`Vim config + overrides variable `. Using + this is also the only variant to make saving into the environment + variable the *only* place where log is saved or save into different + variable. + + .. autoclass:: powerline.vim.VimVarHandler + +.. _local-configuration-overrides-script: + +Powerline script overrides +========================== + +Powerline script has a number of options controlling powerline behavior. Here +``VALUE`` always means “some JSON object”. + +``-c KEY.NESTED_KEY=VALUE`` or ``--config-override=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-override=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``. + +.. warning:: + Such overrides are suggested for testing purposes only. Use + :ref:`Environment variables overrides ` + for other purposes. + +.. _local-configuration-overrides-env: + +Environment variables overrides +=============================== + +All bindings that use ``POWERLINE_COMMAND`` environment variable support taking +overrides from environment variables. In this case overrides should look like +the following:: + + OVERRIDE='key1.key2.key3=value;key4.key5={"value":1};key6=true;key1.key7=10' + +. This will be parsed into + +.. code-block:: Python + + { + "key1": { + "key2": { + "key3": "value" + }, + "key7": 10, + }, + "key4": { + "key5": { + "value": 1, + }, + }, + "key6": True, + } + +. Rules: + +#. Environment variable must form a semicolon-separated list of key-value pairs: + ``key=value;key2=value2``. +#. Keys are always dot-separated strings that must not contain equals sign (as + well as semicolon) or start with an underscore. They are interpreted + literally and create a nested set of dictionaries: ``k1.k2.k3`` creates + ``{"k1":{"k2":{}}}`` and inside the innermost dictionary last key (``k3`` in + the example) is contained with its value. +#. Value may be empty in which case they are interpreted as an order to remove + some value: ``k1.k2=`` will form ``{"k1":{"k2":REMOVE_THIS_KEY}}`` nested + dictionary where ``k2`` value is a special value that tells + dictionary-merging function to remove ``k2`` rather then replace it with + something. +#. Value may be a JSON strings like ``{"a":1}`` (JSON dictionary), ``["a",1]`` + (JSON list), ``1`` or ``-1`` (JSON number), ``"abc"`` (JSON string) or + ``true``, ``false`` and ``null`` (JSON boolean objects and ``Null`` object + from JSON). General rule is that anything starting with a digit (U+0030 till + U+0039, inclusive), a hyphenminus (U+002D), a quotation mark (U+0022), a left + curly bracket (U+007B) or a left square bracket (U+005B) is considered to be + some JSON object, same for *exact* values ``true``, ``false`` and ``null``. +#. Any other value is considered to be literal string: ``k1=foo:bar`` parses to + ``{"k1": "foo:bar"}``. + +The following environment variables may be used for overrides according to the +above rules: + +``POWERLINE_CONFIG_OVERRIDES`` + Overrides values from :file:`powerline/config.json`. + +``POWERLINE_THEME_OVERRIDES`` + Overrides values from :file:`powerline/themes/{ext}/{key}.json`. Top-level + key is treated as a name of the theme for which overrides are used: e.g. to + disable cwd segment defined in :file:`powerline/themes/shell/default.json` + one needs to use:: + + POWERLINE_THEME_OVERRIDES=default.segment_data.cwd.display=false + +Additionally one environment variable is a usual *colon*-separated list of +directories: ``POWERLINE_CONFIG_PATHS``. This one defines paths which will be +searched for configuration. Empty paths in ``POWERLINE_CONFIG_PATHS`` are +ignored. + +.. note:: + Overrides from environment variables have lower priority then + :ref:`Powerline script overrides `. + Latter are suggested for tests only. + +Zsh/zpython overrides +===================== + +Here overrides are controlled by similarly to the powerline script, but values +are taken from zsh variables. :ref:`Environment variable overrides +` are also supported: if variable is a string +this variant is used. + +``POWERLINE_CONFIG_OVERRIDES`` + 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_OVERRIDES`` + 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_OVERRIDES``, 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 variable without is set without quotes: ``POWERLINE_CONFIG_PATHS=( + ~/example )``. In addition to arrays usual colon-separated “array” string + can be used: ``POWERLINE_CONFIG_PATHS=$HOME/path1:$HOME/path2``. + +Ipython overrides +================= + +Ipython overrides depend on ipython version. Before ipython-0.11 additional +keyword arguments should be passed to setup() function. After ipython-0.11 +``c.Powerline.KEY`` should be used. 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. + +``config_paths`` + Sets directories where configuration should be read from. If present, no + default locations are searched for configuration. No expansions are + performed thus paths starting with ``~/`` cannot be used: use + :py:func:`os.path.expanduser`. + +Prompt command +============== + +In addition to the above configuration options ``$POWERLINE_COMMAND`` +environment variable can be used to tell shell or tmux to use specific powerline +implementation and ``$POWERLINE_CONFIG_COMMAND`` to tell zsh or tmux where +``powerline-config`` script is located. This is mostly useful for putting +powerline into different directory. + +.. note:: + + ``$POWERLINE_COMMAND`` is always treated as one path in shell bindings, so + path with spaces in it may be used. To specify additional arguments one may + use ``$POWERLINE_COMMAND_ARGS``, but note that this variable exists for + testing purposes only and may be removed. One should use :ref:`Environment + variable overrides ` instead. + +To disable prompt in shell, but still have tmux support or to disable tmux +support environment variables ``$POWERLINE_NO_{SHELL}_PROMPT`` and +``$POWERLINE_NO_{SHELL}_TMUX_SUPPORT`` can be used (substitute ``{SHELL}`` with +the name of the shell (all-caps) that should be affected (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. + +In order to keep shell prompt, but avoid launching Python twice to get unused +:ref:`above ` lines in tcsh ``$POWERLINE_NO_TCSH_ABOVE`` or +``$POWERLINE_NO_SHELL_ABOVE`` variable should be set. + +In order to remove additional space from the end of the right prompt in fish +that was added in order to support multiline prompt ``$POWERLINE_NO_FISH_ABOVE`` +or ``$POWERLINE_NO_SHELL_ABOVE`` variable should be set. + +PDB overrides +============= + +Like shell bindings :ref:`PDB bindings ` take overrides from +:ref:`environment variables `. diff --git a/docs/source/configuration/reference.rst b/docs/source/configuration/reference.rst new file mode 100644 index 0000000..d4fd4ab --- /dev/null +++ b/docs/source/configuration/reference.rst @@ -0,0 +1,602 @@ +*********************** +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 used terminal emulator supports 24-bit colors. + + This variable is forced to be ``false`` if :ref:`term_escape_style + ` option is set to ``"fbterm"`` or if it is + set to ``"auto"`` and powerline detected fbterm. + +.. _config-common-term_escape_style: + +``term_escape_style`` + Defines what escapes sequences should be used. Accepts three variants: + + ======= =================================================================== + Variant Description + ======= =================================================================== + auto ``xterm`` or ``fbterm`` depending on ``$TERM`` variable value: + ``TERM=fbterm`` implies ``fbterm`` escaping style, all other values + select ``xterm`` escaping. + xterm Uses ``\e[{fb};5;{color}m`` for colors (``{fb}`` is either ``38`` + (foreground) or ``48`` (background)). Should be used for most + terminals. + fbterm Uses ``\e[{fb};{color}}`` for colors (``{fb}`` is either ``1`` + (foreground) or ``2`` (background)). Should be used for fbterm: + framebuffer terminal. + ======= =================================================================== + +.. _config-common-ambiwidth: + +``ambiwidth`` + Tells powerline what to do with characters with East Asian Width Class + Ambiguous (such as Euro, Registered Sign, Copyright Sign, Greek + letters, Cyrillic letters). Valid values: any positive integer; it is + suggested that this option is 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. + 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 how logs will be handled. There are three variants here: + + #. Absent. In this case logging will be done to stderr: equivalent to + ``[["logging.StreamHandler", []]]`` or ``[null]``. + #. Plain string. In this case logging will be done to the given file: + ``"/file/name"`` is equivalent to ``[["logging.FileHandler", + [["/file/name"]]]]`` or ``["/file/name"]``. Leading ``~/`` is expanded in + the file name, so using ``"~/.log/foo"`` is permitted. If directory + pointed by the option is absent, it will be created, but not its parent. + #. List of handler definitions. Handler definition may either be ``null``, + a string or a list with two or three elements: + + #. Logging class name and module. If module name is absent, it is + equivalent to ``logging.handlers``. + #. Class constructor arguments in a form ``[[args[, kwargs]]]``: accepted + variants are ``[]`` (no arguments), ``[args]`` (e.g. + ``[["/file/name"]]``: only positional arguments) or ``[args, kwargs]`` + (e.g. ``[[], {"host": "localhost", "port": 6666}]``: positional and + keyword arguments, but no positional arguments in the example). + #. Optional logging level. Overrides :ref:`log_level key + ` and has the same format. + #. Optional format string. Partially overrides :ref:`log_format key + ` and has the same format. “Partially” here + means that it may only specify more critical level. + +.. _config-common-log_level: + +``log_level`` + String, determines logging level. Defaults to ``WARNING``. + +.. _config-common-log_format: + +``log_format`` + String, determines format of the log messages. Defaults to + ``'%(asctime)s:%(level)s:%(message)s'``. + +``interval`` + Number, determines time (in seconds) between checks for changed + configuration. Checks are done in a separate 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_terminus`` 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. + + For wm (:ref:`lemonbar ` only) it is a dictionary + ``{output : theme_name}`` that maps the ``xrandr`` output names to the + local themes to use on that output. + +.. _config-ext-components: + +``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-ext-update_interval: + +``update_interval`` + Determines how often WM status bars need to be updated, in seconds. Only + valid for WM extensions which use ``powerline-daemon``. Defaults to + 2 seconds. + +.. _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 indices. + * A list of hex color strings. + + It is expected that gradients are defined from least alert color to most + alert or non-alert colors are used. + +.. _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 + `. + + ``attrs`` + 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 no + attributes are needed this list should be left 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 +powerline_unicode7 Theme with powerline dividers and unicode-7 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 +powerline_terminus Like unicode_terminus, but with powerline symbols +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. + +``outer_padding`` + Defines number of spaces at the end of output (on the right side) or at + the start of output (on the left side). Defaults to ``1``. + + +``dividers`` + Defines the dividers used in all Powerline extensions. + + 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 amount 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 + segment_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 + ``segment_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_groups option + `. + + ``segment_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 ``segment_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_groups: + + ``highlight_groups`` + 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 (i.e. with a greater priority number) 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 0000000..63b4975 --- /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 0000000..5d52d69 --- /dev/null +++ b/docs/source/configuration/segments/common.rst @@ -0,0 +1,57 @@ +*************** +Common segments +*************** + +VCS submodule +============= + +.. automodule:: powerline.segments.common.vcs + :members: + +System properties +================= + +.. automodule:: powerline.segments.common.sys + :members: + +Network +======= + +.. automodule:: powerline.segments.common.net + :members: + +Current environment +=================== + +.. automodule:: powerline.segments.common.env + :members: + +Battery +======= + +.. automodule:: powerline.segments.common.bat + :members: + +Weather +======= + +.. automodule:: powerline.segments.common.wthr + :members: + +Date and time +============= + +.. automodule:: powerline.segments.common.time + :members: + +Mail +==== + +.. automodule:: powerline.segments.common.mail + :members: + +Media players +============= + +.. automodule:: powerline.segments.common.players + :members: diff --git a/docs/source/configuration/segments/i3wm.rst b/docs/source/configuration/segments/i3wm.rst new file mode 100644 index 0000000..d103374 --- /dev/null +++ b/docs/source/configuration/segments/i3wm.rst @@ -0,0 +1,6 @@ +************* +i3wm segments +************* + +.. automodule:: powerline.segments.i3wm + :members: diff --git a/docs/source/configuration/segments/pdb.rst b/docs/source/configuration/segments/pdb.rst new file mode 100644 index 0000000..b9a104b --- /dev/null +++ b/docs/source/configuration/segments/pdb.rst @@ -0,0 +1,7 @@ +************ +PDB segments +************ + +.. automodule:: powerline.segments.pdb + :members: + diff --git a/docs/source/configuration/segments/shell.rst b/docs/source/configuration/segments/shell.rst new file mode 100644 index 0000000..fb3c804 --- /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 0000000..1a4a78f --- /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 0000000..5df70d6 --- /dev/null +++ b/docs/source/configuration/segments/vim.rst @@ -0,0 +1,46 @@ +************ +Vim segments +************ + +.. automodule:: powerline.segments.vim + :members: + + +Plugin-specific segments +======================== + +Asynchronous Linter Engine (ALE) segments +----------------------------------------- + +.. automodule:: powerline.segments.vim.plugin.ale + :members: + +Syntastic segments +------------------ + +.. automodule:: powerline.segments.vim.plugin.syntastic + :members: + +Command-T segments +------------------ + +.. automodule:: powerline.segments.vim.plugin.commandt + :members: + +Tagbar segments +--------------- + +.. automodule:: powerline.segments.vim.plugin.tagbar + :members: + +NERDTree segments +----------------- + +.. automodule:: powerline.segments.vim.plugin.nerdtree + :members: + +Capslock segments +----------------- + +.. automodule:: powerline.segments.vim.plugin.capslock + :members: diff --git a/docs/source/configuration/selectors.rst b/docs/source/configuration/selectors.rst new file mode 100644 index 0000000..f9367b2 --- /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 0000000..5097320 --- /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 0000000..bf45498 --- /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 0000000..2ddf223 --- /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 (for powerline extensions developed 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 0000000..e779704 --- /dev/null +++ b/docs/source/develop/listers.rst @@ -0,0 +1,49 @@ +.. _dev-listers: + +*************** +Writing listers +*************** + +Listers provide a way 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 0000000..959e1c4 --- /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 +the 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 0000000..59549f6 --- /dev/null +++ b/docs/source/develop/segments.rst @@ -0,0 +1,547 @@ +.. _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 + something needs to be logged. + +``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). + +.. note:: + For powerline-lint to work properly the following things may be needed: + + #. If segment is a :py:class:`powerline.segments.Segment` instance and used + arguments are scattered over multiple methods + :py:meth:`powerline.segments.Segment.argspecobjs` should be overridden in + subclass to tell powerline-lint which objects should be inspected for + arguments. + #. If segment takes some arguments that are never listed, but accessed via + ``kwargs.get()`` or previous function cannot be used for whatever reason + :py:meth:`powerline.segments.Segment.additional_args` should be + overridden in subclass. + #. If user is expected to use one :ref:`name ` for + multiple segments which cannot be linked to the segment function + automatically by powerline-lint (e.g. because there are no instances of + the segments in question in the default configuration) + :py:func:`powerline.lint.checks.register_common_name` function should be + used. + +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 this 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_groups': [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: + +.. _dev-segments-contents: + +``contents`` + Text displayed by segment. Should be a ``unicode`` (Python2) or ``str`` + (Python3) instance. + +``literal_contents`` + Text that needs to be output literally (i.e. without passing through + :py:meth:`powerline.renderer.strwidth` to determine length, through + :py:meth:`powerline.renderer.escape` to escape special characters and + through :py:meth:`powerline.renderer.hl` to highlight it). Should be a tuple + ``(contents_length, contents)`` where ``contents_length`` is an integer and + ``contents`` is a ``unicode`` (Python2) or ``str`` (Python3) instance. + + If this key is present and its second value is true then other contents keys + (:ref:`contents `, :ref:`after + `, :ref:`before `) will + be ignored. + + .. note:: + If target is inclusion of the segment in powerline upstream all segment + functions that output *only* subsegments with ``literal_contents`` key + must contain the following string in documentation:: + + No highlight groups are used (literal segment). + + String must be present on the separate line. + +.. _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_groups: + +``highlight_groups`` + Determines segment highlighting. Refer to :ref:`themes documentation + ` for more details. + + Defaults to the name of the segment. + + .. note:: + If target is inclusion of the segment in powerline upstream all used + highlighting groups must be specified 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 the 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. + + All groups must be specified 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 target is inclusion of the segment in powerline upstream used divider + highlight group must be specified in the segment documentation in the + form:: + + Divider highlight group used: ``group``. + + This text must not wrap and all divider highlight group names are + supposed to end 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_groups + ` 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_groups``, ``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:: + ``os.environ`` must not ever be used: + + * If segment is run in the daemon this way it will get daemon’s + environment which is not correct. + * If segment is run in Vim or in zsh with libzpython ``os.environ`` will + contain Vim or zsh environ *at the moment Python interpreter was + loaded*. + +``getcwd`` + Function that returns current working directory being called with no + arguments. ``os.getcwd`` must not be used for the same reasons the use of + ``os.environ`` is forbidden, 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. ``vim.current.window`` or ``vim.windows[number - 1]`` + may be used to obtain such object. May be a false object, in which case any + of this object’s properties must not be used. + +``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. It is + safe to assume that this ID is hashable and supports equality comparison, + but no other assumptions about it should be used. Currently uses integer + numbers incremented each time window is created. + +``buffer`` + ``vim.Buffer`` object. One may be obtained 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 iteration over ``vim.buffers`` and checking their ``number`` + attributes should be performed. + +``bufnr`` + Buffer number. + +``tabpage`` + ``vim.Tabpage`` object. One may be obtained using ``vim.current.tabpage`` or + ``vim.tabpages[number - 1]``. May be a false object, in which case no + object’s properties can be used. + +``tabnr`` + Tabpage number. + +``mode`` + Current mode. + +``encoding`` + Value of ``&encoding`` from the time when powerline was initialized. It + should be used to convert return values. + +.. note:: + 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 ``window_cached`` decorator is used, “current + tabpage” restriction may be safely ignored if segment is not supposed to be + used in tabline. + +.. warning:: + Powerline is being tested with vim-7.0.112 (some minor sanity check) and + latest Vim. This means that most of the functionality like + ``vim.Window.number``, ``vim.*.vars``, ``vim.*.options`` or even ``dir(vim + object)`` should be avoided in segments that want to be included in the + upstream. + +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. Is either one integer, + ``sig{name}`` or ``sig{name}+core`` (latter two are only seen in ``rc`` + shell). + + ``last_pipe_status`` + List of exit codes returned by last programs in the pipe or some false + object. Only available in ``zsh`` and ``rc``. Is a list of either + integers, ``sig{name}`` or ``sig{name}+core`` (latter two are only seen + in ``rc`` shell). + + ``jobnum`` + Number of background jobs. + + ``renderer_arg`` + Dictionary containing some keys that are additional arguments used by + shell bindings. *This attribute must not be used 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. In tmux this is the same as :ref:`pane_id + `. + + 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. + + .. _dev-seginfo-shell-renarg-pane_id: + + ``pane_id`` + Identifier unique to each tmux pane. Is always an integer, optional. + Obtained by using ``tmux display -p '#D'``, then all leading spaces + and per cent signs are stripped and the result is converted into an + integer. + + 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``). + +Pdb +--- + +``pdb`` + Currently active :py:class:`pdb.Pdb` instance. + +``curframe`` + Frame which will be run next. Note: due to the existence of + :py:func:`powerline.listers.pdb.frame_lister` one must not use + ``segment_info['pdb'].curframe``. + +``initial_stack_length`` + Equal to the length of :py:attr:`pdb.Pdb.stack` at the first invocation of + the prompt decremented by one. + +i3wm +---- + +``mode`` + Currently active i3 mode (as a string). + +``output`` + ``xrandr`` output name currently drawing to. Currently only available + in lemonbar bindings. + +``workspace`` + the `i3-ipc` workspace object corresponding to this workspace. + Contains string attributes ``name`` and ``output``, as well as boolean + attributes for ``visible``, ``urgent`` and ``focused``. Currently only + provided by the :py:func:`powerline.listers.i3wm.workspace_lister` lister. + +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 0000000..c850659 --- /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 0000000..9a4d84b --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,28 @@ +********* +Powerline +********* + +.. toctree:: + :maxdepth: 3 + :glob: + + overview + installation + usage + configuration + develop + troubleshooting + tips-and-tricks + license-and-credits + +.. toctree:: + :maxdepth: 2 + + commands + +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 0000000..8dc4236 --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,131 @@ +************ +Installation +************ + +Generic requirements +==================== + +* Python 2.6 or later, 3.2 or later, PyPy 2.0 or later, PyPy3 2.3 or later. It + is the only non-optional requirement. + + .. warning: + It is highly advised to use UCS-4 version of Python because UCS-2 version + uses significantly slower text processing (length determination and + non-printable character replacement) functions due to the need of + supporting unicode characters above U+FFFF which are represented as + surrogate pairs. This price will be paid even if configuration has no such + characters. + +* 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. +* ``hglib`` python package *and* mercurial 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. +* ``i3ipc`` python package. Required for i3wm bindings and segments. +* ``xrandr`` program. Required for the multi-monitor lemonbar binding and the + :py:func:`powerline.listers.i3wm.output_lister`. + +.. note:: + Until bazaar supports Python-3 or PyPy powerline will not support + repository information when running in these interpreters. + +.. _repository-root: + +.. note:: + When using ``pip``, the ``{repository_root}`` directory referenced in + documentation may be found using ``pip show powerline-status``. In the output + of ``pip show`` there is a line like ``Location: {path}``, that ``{path}`` is + ``{repository_root}``. Unless it is ``--editable`` installation this is only + applicable for ``{repository_root}/powerline/…`` paths: something like + ``{repository_root}/scripts/powerline-render`` is not present. + + When using other packages referenced paths may not exist, in this case refer + to package documentation. + +Pip installation +================ + +Due to a naming conflict with an unrelated project, powerline is available on +PyPI under the ``powerline-status`` name: + +.. code-block:: sh + + pip install powerline-status + +is the preferred method because this will get the latest release. To get current +development version + +.. code-block:: sh + + pip install --user git+https://github.com/powerline/powerline + +may be used. If powerline was already checked out into some directory + +.. code-block:: sh + + pip install --user --editable={path_to_powerline} + +is useful, but note that in this case ``pip`` will not install ``powerline`` +executable and something like + +.. code-block:: sh + + ln -s {path_to_powerline}/scripts/powerline ~/.local/bin + +will have to be done (:file:`~/.local/bin` should be replaced with some path +present in ``$PATH``). + +.. note:: + We can use either ``https``(``git+ssh://git@github.com/powerline/powerline``) + or ``https``(``git+https://github.com/powerline/powerline``) protocols. + ``git`` protocol is deprecated by Github. + +Fonts installation +================== + +Powerline uses several special glyphs to get the arrow effect and some custom +symbols for developers. This requires having either a symbol font or a patched +font installed in the system. The used application (e.g. terminal emulator) must +also either be configured to use patched fonts (in some cases even support it +because custom glyphs live in private use area which some applications reserve +for themselves) or support fontconfig for powerline to work properly with +powerline-specific glyphs. + +:ref:`24-bit color support ` may be enabled if +used 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. + +Download the font from `powerline-fonts`_. If preferred font can’t be found in +the `powerline-fonts`_ repo, then patching the preferred font is needed instead. + +.. _powerline-fonts: https://github.com/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 0000000..e134707 --- /dev/null +++ b/docs/source/installation/linux.rst @@ -0,0 +1,110 @@ +********************* +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. + +* `Arch Linux (AUR), Python 2 version `_ +* `Arch Linux (AUR), Python 3 version `_ +* Gentoo Live ebuild in `raiagent `_ overlay +* Powerline package is available for Debian starting from Wheezy (via `backports + `_). Use `search + `_ + to get more information. + +If used distribution does not have an official package installation guide below +should be followed: + +1. Install Python 3.2+, Python 2.6+ or PyPy and ``pip`` with ``setuptools``. + This step is distribution-specific, so no commands provided. +2. Install Powerline using one of the following commands: + + .. code-block:: sh + + pip install --user powerline-status + + will get the latest release version and + + .. code-block:: sh + + pip install --user git+https://github.com/powerline/powerline + + will get the latest development version. + + .. note:: Due to the naming conflict with an unrelated project powerline is + named ``powerline-status`` in PyPI. + + .. note:: + Powerline developers should be aware that``pip install --editable`` does + not currently fully work. Installation performed this way are missing + ``powerline`` executable that needs to be symlinked. It will be located in + ``scripts/powerline``. + +Fonts installation +================== + +Fontconfig +---------- + +This method only works on Linux. It’s the second recommended method if terminal +emulator supports it as patching fonts is not needed, and it generally works +with any coding font. + +#. Download the latest version of the symbol font and fontconfig file:: + + wget https://github.com/powerline/powerline/raw/develop/font/PowerlineSymbols.otf + wget https://github.com/powerline/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 ~/.local/share/fonts/ + +#. Update font cache for the path the font was moved to (root privileges may be + needed to update cache for the system-wide paths):: + + fc-cache -vf ~/.local/share/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 custom symbols still cannot be seen then try closing all instances of the +terminal emulator. Restarting X may be needed for the changes to take effect. + +If custom symbols *still* can’t be seen, double-check that the font have been +installed to a valid X font path, and that the fontconfig file was installed to +a valid fontconfig path. Alternatively try to install a :ref:`patched font +`. + +Patched font installation +------------------------- + +This is the preferred method, but it is not always available because not all +fonts were patched and not all fonts *can* be patched due to licensing issues. + +After downloading font the following should be done: + +#. Move the patched font to a valid X font path. Valid font paths can be + listed with ``xset q``:: + + mv 'SomeFont for Powerline.otf' ~/.local/share/fonts/ + +#. Update font cache for the path the font was moved to (root privileges may be + needed for updating font cache for some paths):: + + fc-cache -vf ~/.local/share/fonts/ + +After installing patched font terminal emulator, GVim or whatever application +powerline should work with must be configured to use the patched font. The +correct font usually ends with *for Powerline*. + +If custom symbols cannot be seen then try closing all instances of the terminal +emulator. X server may need to be restarted for the changes to take effect. + +If custom symbols *still* can’t be seen then double-check that the font have +been installed 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 0000000..1d6fafc --- /dev/null +++ b/docs/source/installation/osx.rst @@ -0,0 +1,71 @@ +******************** +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 + + Homebrew may be used here:: + + brew install python + + .. note:: + There are three variants of the powerline client. The fastest is + written in C and will be compiled if the compiler and libraries are + detected during installation. The second fastest option is + :file:`powerline.sh` which requires ``socat`` and ``coreutils``. + ``coreutils`` may be installed using ``brew install + coreutils``. If neither of these are viable, then Powerline will + utilize a fallback client written in Python. + +2. Install Powerline using one of the following commands: + + .. code-block:: sh + + pip install --user powerline-status + + will get current release version and + + .. code-block:: sh + + pip install --user git+https://github.com/powerline/powerline + + will get latest development version. + + .. warning:: + When using ``brew install`` to install Python one must not supply + ``--user`` flag to ``pip``. + + .. note:: + Due to the naming conflict with an unrelated project powerline is named + ``powerline-status`` in PyPI. + + .. note:: + Powerline developers should be aware that ``pip install --editable`` does + not currently fully work. Installation performed this way are missing + ``powerline`` executable that needs to be symlinked. It will be located in + ``scripts/powerline``. + +Vim installation +================ + +Any terminal vim version with Python 3.2+ or Python 2.6+ support should work, +but MacVim users need to install it using the following command:: + + brew install macvim --env-std --with-override-system-vim + +Fonts installation +================== + +To install patched font double-click the font file in Finder, then click +:guilabel:`Install this font` in the preview window. + +After installing the patched font MacVim or terminal emulator (whatever +application powerline should work with) need to be configured to use the patched +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 0000000..5226669 --- /dev/null +++ b/docs/source/license-and-credits.rst @@ -0,0 +1,31 @@ +******************* +License and credits +******************* + +Powerline is licensed under the `MIT license +`_. + +.. + This document is parsed by powerline_automan.py module. Do not forget to + check that file before altering this one. Specifically it expects + ``Authors`` and ``Contributors`` sections underlined by ``---``, a list of + authors in format ``* `{name} <`` in the “Authors” section and fonts + contributor name in format ``The glyphs in the font patcher are created by + {name},`` in the “Contributors” section. + +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 0000000..13b6be3 --- /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, i3 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 the `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 0000000..eba42ed --- /dev/null +++ b/docs/source/powerline_autodoc.py @@ -0,0 +1,64 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +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('\\', '\\\\') + + +class Repr(object): + def __init__(self, repr_contents): + self.repr_contents = repr_contents + + def __repr__(self): + return '<{0}>'.format(self.repr_contents) + + +class EnvironDocumenter(autodoc.AttributeDocumenter): + @classmethod + def can_document_member(cls, member, membername, isattr, parent): + if type(member) is dict and member.get('environ') is os.environ: + return True + else: + return False + + def import_object(self, *args, **kwargs): + ret = super(EnvironDocumenter, self).import_object(*args, **kwargs) + if not ret: + return ret + self.object = self.object.copy() + if 'home' in self.object: + self.object.update(home=Repr('home directory')) + self.object.update(environ=Repr('environ dictionary')) + return True + + +def setup(app): + autodoc.setup(app) + app.add_autodocumenter(ThreadedDocumenter) + app.add_autodocumenter(EnvironDocumenter) diff --git a/docs/source/powerline_automan.py b/docs/source/powerline_automan.py new file mode 100644 index 0000000..85d241c --- /dev/null +++ b/docs/source/powerline_automan.py @@ -0,0 +1,408 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re +import codecs + +from collections import namedtuple +from argparse import REMAINDER + +from functools import reduce + +from docutils.parsers.rst import Directive +from docutils.parsers.rst.directives import unchanged_required +from docutils import nodes + +from powerline.lib.unicode import u + + +AUTHOR_LINE_START = '* `' +GLYPHS_AUTHOR_LINE_START = '* The glyphs in the font patcher are created by ' + + +def get_authors(): + credits_file = os.path.join(os.path.dirname(__file__), 'license-and-credits.rst') + authors = [] + glyphs_author = None + with codecs.open(credits_file, encoding='utf-8') as CF: + section = None + prev_line = None + for line in CF: + line = line[:-1] + if line and not line.replace('-', ''): + section = prev_line + elif section == 'Authors': + if line.startswith(AUTHOR_LINE_START): + authors.append(line[len(AUTHOR_LINE_START):line.index('<')].strip()) + elif section == 'Contributors': + if line.startswith(GLYPHS_AUTHOR_LINE_START): + assert(not glyphs_author) + glyphs_author = line[len(GLYPHS_AUTHOR_LINE_START):line.index(',')].strip() + prev_line = line + return { + 'authors': ', '.join(authors), + 'glyphs_author': glyphs_author, + } + + +class AutoManSubparsers(object): + def __init__(self): + self.parsers = [] + + def add_parser(self, command, *args, **kwargs): + self.parsers.append((command, AutoManParser(*args, **kwargs))) + return self.parsers[-1][1] + + +Argument = namedtuple('Argument', ('names', 'help', 'choices', 'metavar', 'required', 'nargs', 'is_option', 'is_long_option', 'is_short_option', 'multi', 'can_be_joined')) + + +def parse_argument(*args, **kwargs): + is_option = args[0].startswith('-') + is_long_option = args[0].startswith('--') + is_short_option = is_option and not is_long_option + action = kwargs.get('action', 'store') + multi = kwargs.get('action') in ('append',) or kwargs.get('nargs') is REMAINDER + nargs = kwargs.get('nargs', (1 if action in ('append', 'store') else 0)) + return Argument( + names=args, + help=u(kwargs.get('help', '')), + choices=[str(choice) for choice in kwargs.get('choices', [])], + metavar=kwargs.get('metavar') or args[-1].lstrip('-').replace('-', '_').upper(), + required=kwargs.get('required', False) if is_option else ( + kwargs.get('nargs') not in ('?',)), + nargs=nargs, + multi=multi, + is_option=is_option, + is_long_option=is_long_option, + is_short_option=is_short_option, + can_be_joined=(is_short_option and not multi and not nargs) + ) + + +class AutoManGroup(object): + is_short_option = False + is_option = False + is_long_option = False + can_be_joined = False + + def __init__(self): + self.arguments = [] + self.required = False + + def add_argument(self, *args, **kwargs): + self.arguments.append(parse_argument(*args, **kwargs)) + + def add_argument_group(self, *args, **kwargs): + self.arguments.append(AutoManGroup()) + return self.arguments[-1] + + +class SurroundWith(): + def __init__(self, ret, condition, start='[', end=']'): + self.ret = ret + self.condition = condition + self.start = start + self.end = end + + def __enter__(self, *args): + if self.condition: + self.ret.append(nodes.Text(self.start)) + + def __exit__(self, *args): + if self.condition: + self.ret.append(nodes.Text(self.end)) + + +def insert_separators(ret, sep): + for i in range(len(ret) - 1, 0, -1): + ret.insert(i, nodes.Text(sep)) + return ret + + +def format_usage_arguments(arguments, base_length=None): + line = [] + prev_argument = None + arg_indexes = [0] + arguments = arguments[:] + while arguments: + argument = arguments.pop(0) + if isinstance(argument, nodes.Text): + line += [argument] + continue + can_join_arguments = ( + argument.is_short_option + and prev_argument + and prev_argument.can_be_joined + and prev_argument.required == argument.required + ) + if ( + prev_argument + and not prev_argument.required + and prev_argument.can_be_joined + and not can_join_arguments + ): + line.append(nodes.Text(']')) + arg_indexes.append(len(line)) + if isinstance(argument, AutoManGroup): + arguments = ( + [nodes.Text(' (')] + + insert_separators(argument.arguments[:], nodes.Text(' |')) + + [nodes.Text(' )')] + + arguments + ) + else: + if not can_join_arguments: + line.append(nodes.Text(' ')) + with SurroundWith(line, not argument.required and not argument.can_be_joined): + if argument.can_be_joined and not can_join_arguments and not argument.required: + line.append(nodes.Text('[')) + if argument.is_option: + line.append(nodes.strong()) + name = argument.names[0] + if can_join_arguments: + name = name[1:] + # `--` is automatically transformed into – (EN DASH) + # when parsing into HTML. We do not need this. + line[-1] += [nodes.Text(char) for char in name] + elif argument.nargs is REMAINDER: + line.append(nodes.Text('[')) + line.append(nodes.strong()) + line[-1] += [nodes.Text(char) for char in '--'] + line.append(nodes.Text('] ')) + if argument.nargs: + assert(argument.nargs in (1, '?', REMAINDER)) + with SurroundWith( + line, ( + True + if argument.nargs is REMAINDER + else (argument.nargs == '?' and argument.is_option) + ) + ): + if argument.is_long_option: + line.append(nodes.Text('=')) + line.append(nodes.emphasis(text=argument.metavar)) + elif not argument.is_option: + line.append(nodes.strong(text=argument.metavar)) + if argument.multi: + line.append(nodes.Text('…')) + prev_argument = argument + if ( + prev_argument + and prev_argument.can_be_joined + and not prev_argument.required + ): + line.append(nodes.Text(']')) + arg_indexes.append(len(line)) + ret = [] + if base_length is None: + ret = line + else: + length = base_length + prev_arg_idx = arg_indexes.pop(0) + while arg_indexes: + next_arg_idx = arg_indexes.pop(0) + arg_length = sum((len(element.astext()) for element in line[prev_arg_idx:next_arg_idx])) + if length + arg_length > 68: + ret.append(nodes.Text('\n' + (' ' * base_length))) + length = base_length + ret += line[prev_arg_idx:next_arg_idx] + length += arg_length + prev_arg_idx = next_arg_idx + return ret + + +LITERAL_RE = re.compile(r"`(.*?)'") + + +def parse_argparse_text(text): + rst_text = LITERAL_RE.subn(r'``\1``', text)[0] + ret = [] + for i, text in enumerate(rst_text.split('``')): + if i % 2 == 0: + ret.append(nodes.Text(text)) + else: + ret.append(nodes.literal(text=text)) + return ret + + +def flatten_groups(arguments): + for argument in arguments: + if isinstance(argument, AutoManGroup): + for group_argument in flatten_groups(argument.arguments): + yield group_argument + else: + yield argument + + +def format_arguments(arguments): + return [nodes.definition_list( + '', *[ + nodes.definition_list_item( + '', + nodes.term( + # node.Text('') is required because otherwise for some + # reason first name node is seen in HTML output as + # `abc`. + '', *([nodes.Text('')] + ( + insert_separators([ + nodes.strong('', '', *[nodes.Text(ch) for ch in name]) + for name in argument.names + ], ', ') + if argument.is_option else + # Unless node.Text('') is here metavar is written in + # bold in the man page. + [nodes.Text(''), nodes.emphasis(text=argument.metavar)] + ) + ( + [] if not argument.is_option or not argument.nargs else + [nodes.Text(' '), nodes.emphasis('', argument.metavar)] + )) + ), + nodes.definition('', nodes.paragraph('', *parse_argparse_text(argument.help or ''))), + ) + for argument in flatten_groups(arguments) + ] + [ + nodes.definition_list_item( + '', + nodes.term( + '', nodes.Text(''), + nodes.strong(text='-h'), + nodes.Text(', '), + nodes.strong('', '', nodes.Text('-'), nodes.Text('-help')), + ), + nodes.definition('', nodes.paragraph('', nodes.Text('Display help and exit.'))) + ) + ] + )] + + +def format_subcommand_usage(arguments, subcommands, progname, base_length): + return reduce((lambda a, b: a + reduce((lambda c, d: c + d), b, [])), [ + [ + [progname] + + format_usage_arguments(arguments) + + [nodes.Text(' '), nodes.strong(text=subcmd)] + + format_usage_arguments(subparser.arguments) + + [nodes.Text('\n')] + for subcmd, subparser in subparsers.parsers + ] + for subparsers in subcommands + ], []) + + +def format_subcommands(subcommands): + return reduce((lambda a, b: a + reduce((lambda c, d: c + d), b, [])), [ + [ + [ + nodes.section( + '', + nodes.title(text='Arguments specific to ' + subcmd + ' subcommand'), + *format_arguments(subparser.arguments), + ids=['subcmd-' + subcmd] + ) + ] + for subcmd, subparser in subparsers.parsers + ] + for subparsers in subcommands + ], []) + + +class AutoManParser(object): + def __init__(self, description=None, help=None): + self.description = description + self.help = help + self.arguments = [] + self.subcommands = [] + + def add_argument(self, *args, **kwargs): + self.arguments.append(parse_argument(*args, **kwargs)) + + def add_subparsers(self): + self.subcommands.append(AutoManSubparsers()) + return self.subcommands[-1] + + def add_mutually_exclusive_group(self): + self.arguments.append(AutoManGroup()) + return self.arguments[-1] + + def automan_usage(self, prog): + block = nodes.literal_block() + progname = nodes.strong() + progname += [nodes.Text(prog)] + base_length = len(prog) + if self.subcommands: + block += format_subcommand_usage(self.arguments, self.subcommands, progname, base_length) + else: + block += [progname] + block += format_usage_arguments(self.arguments, base_length) + return [block] + + def automan_description(self): + ret = [] + if self.help: + ret += parse_argparse_text(self.help) + ret += format_arguments(self.arguments) + format_subcommands(self.subcommands) + return ret + + +class AutoMan(Directive): + required_arguments = 1 + optional_arguments = 0 + option_spec = dict(prog=unchanged_required, minimal=bool) + has_content = False + + def run(self): + minimal = self.options.get('minimal') + module = self.arguments[0] + template_args = {} + template_args.update(get_authors()) + get_argparser = __import__(str(module), fromlist=[str('get_argparser')]).get_argparser + parser = get_argparser(AutoManParser) + if minimal: + container = nodes.container() + container += parser.automan_usage(self.options['prog']) + container += parser.automan_description() + return [container] + synopsis_section = nodes.section( + '', + nodes.title(text='Synopsis'), + ids=['synopsis-section'], + ) + synopsis_section += parser.automan_usage(self.options['prog']) + description_section = nodes.section( + '', nodes.title(text='Description'), + ids=['description-section'], + ) + description_section += parser.automan_description() + author_section = nodes.section( + '', nodes.title(text='Author'), + nodes.paragraph( + '', + nodes.Text('Written by {authors} and contributors. The glyphs in the font patcher are created by {glyphs_author}.'.format( + **get_authors() + )) + ), + ids=['author-section'] + ) + issues_url = 'https://github.com/powerline/powerline/issues' + reporting_bugs_section = nodes.section( + '', nodes.title(text='Reporting bugs'), + nodes.paragraph( + '', + nodes.Text('Report {prog} bugs to '.format( + prog=self.options['prog'])), + nodes.reference( + issues_url, issues_url, + refuri=issues_url, + internal=False, + ), + nodes.Text('.'), + ), + ids=['reporting-bugs-section'] + ) + return [synopsis_section, description_section, author_section, reporting_bugs_section] + + +def setup(app): + app.add_directive('automan', AutoMan) diff --git a/docs/source/tips-and-tricks.rst b/docs/source/tips-and-tricks.rst new file mode 100644 index 0000000..0af780e --- /dev/null +++ b/docs/source/tips-and-tricks.rst @@ -0,0 +1,110 @@ +*************** +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-vscode: + +VS-Code +======= + +Useful settings +--------------- + +To make powerline work in the internal terminal, add the following settings; +where the shell command needs to be adjusted according to your preferred shell. + +.. code-block:: json + + "terminal.integrated.shell.linux": "/bin/bash" + "terminal.integrated.inheritEnv": true + +.. _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 0000000..2b91747 --- /dev/null +++ b/docs/source/troubleshooting.rst @@ -0,0 +1,333 @@ +*************** +Troubleshooting +*************** + +System-specific issues +====================== + +.. toctree:: + + Linux + OS X + +Common issues +============= + +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 absence 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 + +Tmux/screen-related 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. + + .. warning:: + Both tmux and screen are not resending sequences escaped in such a way. Thus + even though additional escaping will work for the last shown prompt, + highlighting will eventually go away when tmux or screen will redraw screen + for some reason. + + E.g. in screen it will go away when you used copy mode and prompt got out of + screen and in tmux it will go away immediately after you press ````. + +In tmux there is a green bar in place of powerline +-------------------------------------------------- + +In order for tmux bindings to work ``powerline-config`` script is required to be +present in ``$PATH``. Alternatively one may define ``$POWERLINE_CONFIG_COMMAND`` +environment variable pointing to the location of the script. *This variable must +be defined prior to launching tmux server and in the environment where server is +started from.* + +Shell issues +============ + +Pipe status segment displays only last value in bash +---------------------------------------------------- + +Make sure that powerline command that sets prompt appears the very first in +``$PROMPT_COMMAND``. To do this ``powerline.sh`` needs to be sourced the very +last, after all other users of ``$PROMPT_COMMAND``. + +Bash prompt stopped updating +---------------------------- + +Make sure that powerline commands appear in ``$PROMPT_COMMAND``: some users of +``$PROMPT_COMMAND`` have a habit of overwriting the value instead of +prepending/appending to it. All powerline commands start with ``_powerline`` or +``powerline``, e.g. ``_powerline_set_prompt``. + +Bash prompt does not show last exit code +---------------------------------------- + +There are two possibilities here: + +* You are using ``default`` theme in place of ``default_leftonly``. Unlike + ``default_leftonly`` ``default`` theme was designed for shells with right + prompt support (e.g. zsh, tcsh, fish) and status in question is supposed to be + shown on the right side which bash cannot display. + +* There is some other user of ``$PROMPT_COMMAND`` which prepended to this + variable, but did not bother keeping the exit code. For the best experience + powerline must appear first in ``$PROMPT_COMMAND`` which may be achieved by + sourcing powerline bindings the last. + + .. note:: + Resourcing bash bindings will not resolve the problem unless you clear + powerline commands from ``$PROMPT_COMMAND`` first. + +When sourcing shell bindings it complains about missing command or file +----------------------------------------------------------------------- + +If you are using ``pip`` based installation do not forget to add pip-specific +executable path to ``$PATH`` environment variable. This path usually looks +something like ``$HOME/.local/bin`` (linux) or +``$HOME/Library/Python/{python_version}/bin`` (OS X). One may check out where +``powerline-config`` script was installed by using ``pip show -f +powerline-status | grep powerline-config`` (does not always work). + +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. + See installation instructions for more details. +* Compile and install ``libzpython`` module that lives in + https://bitbucket.org/ZyX_I/zpython. This variant is zsh-specific. +* If you are a python package manager, be sure to set ``POWERLINE_COMMAND`` + to your Powerline command. See installation instructions for details. + + +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. + +Urxvt unicode3 and frills +------------------------- + +Make sure that, whatever urxvt package you're installing, both the `unicode3` +and `frills` features are enabled at compile time. Run +``urxvt --help 2>&1 | grep options:`` to get a list of enabled options. +This should contain at least `frills`, `unicode3` and optionally `iso14755` +if you want to input Unicode characters as well. + +Compiler flags example: + + --enable-frills \ + --enable-unicode3 + +As long as your terminal emulator is compiled without unicode rendering, +no amount of configuration will make it display unicode characters. +They're being considered 'unnecessary features', but they add negligible +overhead to the size of the installed package (~100KB). + +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 + +used to automatically source vimrc after saving it then you must 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`` (or any other) 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 0000000..e0493c6 --- /dev/null +++ b/docs/source/troubleshooting/linux.rst @@ -0,0 +1,78 @@ +************************ +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. +* If you’re using xterm make sure you have told it to work with unicode. You may + need ``-u8`` command-line argument, ``uxterm`` shell wrapper that is usually + shipped with xterm for this or ``xterm*utf8`` property set to ``1`` or ``2`` + in ``~/.Xresources`` (applied with ``xrdb``). Note that in case ``uxterm`` is + used configuration is done via ``uxterm*…`` properties and not ``xterm*…``. + + In any case the only absolute requirement is launching xterm with UTF-8 + locale. +* If you are using bitmap font make sure that + :file:`/etc/fonts/conf.d/70-no-bitmaps.conf` does not exist. If it does check + out your distribution documentation to find a proper way to remove it (so that + it won’t reappear after update). E.g. in Gentoo this is:: + + eselect fontconfig disable 70-no-bitmaps.conf + + (currently this only removes the symlink from :file:`/etc/fonts/conf.d`). Also + check out that no other fontconfig file does not have ``rejectfont`` tag that + tells fontconfig to disable bitmap fonts (they are referenced as not + scalable). + +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). + +I am seeing strange blocks in place of playing/paused/stopped signs +------------------------------------------------------------------- + +If you are using ``powerline_unicode7`` :ref:`top-level theme +` then symbols for player segments are taken +from U+23F4–U+23FA range which is missing from most fonts. You may fix the issue +by using `Symbola `_ font (or any other font +which contains these glyphs). + +If your terminal emulator is using fontconfig library then you can create +a fontconfig configuration file with the following contents: + +.. code-block:: xml + + + + + + + Terminus + Symbola + + + +(replace ``Terminus`` with the name of the font you are using). Exact sequence +of actions you need to perform is different across distributions, most likely it +will work if you put the above xml into +:file:`/etc/fonts/conf.d/99-prefer-symbola.conf`. On Gentoo you need to put it +into :file:`/etc/fonts/conf.d/99-prefer-symbola.conf` and run:: + + eselect fontconfig enable 99-prefer-symbola + +. + +.. warning:: + This answer is only applicable if you use ``powerline_unicode7`` theme or if + you configured powerline to use the same characters yourself. diff --git a/docs/source/troubleshooting/osx.rst b/docs/source/troubleshooting/osx.rst new file mode 100644 index 0000000..b61063e --- /dev/null +++ b/docs/source/troubleshooting/osx.rst @@ -0,0 +1,71 @@ +*********************** +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. Also make sure that Preferences>Profiles>Text>Non-ASCII Font is the same as + your main Font. +* You need to set your ``LANG`` and ``LC_*`` environment variables to + a UTF-8 locale (e.g. ``LANG=en_US.utf8``). Consult your Linux distro’s + documentation for information about setting these variables correctly. + +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. +* If you're using transparency, check “Keep background colors opaque”. + +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. + +I receive “FSEventStreamStart: register_with_server: ERROR” with status_colors +------------------------------------------------------------------------------ + +This is `a known `_ libuv issue that +happens if one is trying to watch too many files. It should be fixed in +libuv-0.12. Until then it is suggested to either disable ``status_colors`` (from +:py:func:`powerline.segments.common.vcs.branch`) or choose stat-based watcher +(will have effectively the same effect as disabling ``status_colors``). diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 0000000..5bfd304 --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,88 @@ +***** +Usage +***** + +Application-specific requirements +--------------------------------- + +Vim plugin requirements +^^^^^^^^^^^^^^^^^^^^^^^ + +The vim plugin requires a vim version with Python support compiled in. Presence +of Python support in Vim can be checked by running ``vim --version | grep ++python``. + +If Python support is absent then Vim needs to be compiled with it. To do this +use ``--enable-pythoninterp`` :file:`./configure` flag (Python 3 uses +``--enable-python3interp`` flag instead). Note that this also requires the +related Python headers to be installed. Please consult 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 +supports Vim 7.0.112 and higher. + +Shell prompts requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Due to fish having incorrect code for prompt width calculations up to version +2.1 and no way to tell that certain sequence of characters has no width +(``%{…%}`` in zsh and ``\[…\]`` in bash prompts serve exactly this purpose) +users that have fish versions below 2.1 are not supported.. + + +WM widgets requirements +^^^^^^^^^^^^^^^^^^^^^^^ + +Awesome is supported starting from version 3.5.1, inclusive. QTile is supported +from version 0.6, inclusive. + +.. _usage-terminal-emulators: + +Terminal emulator requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Powerline uses several special glyphs to get the arrow effect and some custom +symbols for developers. This requires either a symbol font or a patched font +installed. Used terminal emulator must also support either patched fonts or +fontconfig for Powerline to work properly. + +:ref:`24-bit color support ` can also be enabled +if 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| [#]_ + fbterm Linux |i_yes| |i_yes| |i_no| + ===================== ======= ===================== ===================== ===================== + +.. |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 0000000..ff0ccae --- /dev/null +++ b/docs/source/usage/other.rst @@ -0,0 +1,224 @@ +************* +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 the :file:`vimrc`. + +.. note:: + Status line will not appear by default when there is only a single window + displayed. Run ``:h 'laststatus'`` in Vim for more information. + +If the repository was just cloned the following line needs to be added to the +:file:`vimrc`: + +.. code-block:: vim + + set rtp+={repository_root}/powerline/bindings/vim + +where ``{repository_root}`` is the absolute path to the Powerline installation +directory (see :ref:`repository root `). + +If pathogen is used and Powerline functionality is not needed outside of Vim +then it is possible to simply add Powerline as a bundle and point the path above +to the Powerline bundle directory, e.g. +:file:`~/.vim/bundle/powerline/powerline/bindings/vim`. + +Vundle and NeoBundle users may instead use + +.. code-block:: vim + + Bundle 'powerline/powerline', {'rtp': 'powerline/bindings/vim/'} + +(NeoBundle users need ``NeoBundle`` in place of ``Bundle``, otherwise setup is +the same). + +Vim-addon-manager setup is even easier because it is not needed to write this +big path or install anything by hand: ``powerline`` can be installed and +activated 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 powerline functionality is needed in applications other then Vim then + system-wide installation (in case used OS distribution has powerline + package), pip-only or ``pip install --editable`` kind of installation + performed on the repository installed by Vim plugin manager should be used. + + No issues are accepted in powerline issue tracker for double pip/non-pip + installations, especially if these issues occur after update. + +.. note:: + If supplied :file:`powerline.vim` file is used 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 ``g:powerline_pyeval`` must also be set. Powerline will not check + that Vim is compiled with Python support if ``g:powerline_pycmd`` is set to + an unexpected value. + + These values are to be used to specify the only Python that is to be loaded + if both versions are present: Vim may disable loading one python version if + other was already loaded. They should also be used if two python versions + are able to load simultaneously, but powerline was installed only for + python-3 version. + +Tmux statusline +=============== + +Add the following lines to :file:`.tmux.conf`, where ``{repository_root}`` is +the absolute path to the Powerline installation directory (see :ref:`repository +root `):: + + source "{repository_root}/powerline/bindings/tmux/powerline.conf" + +.. note:: + The availability of the ``powerline-config`` command is required for + powerline support. The location of this script may be specified via + the ``$POWERLINE_CONFIG_COMMAND`` environment variable. + +.. note:: + It is advised to run ``powerline-daemon`` before adding the above line to + tmux.conf. To do so add:: + + run-shell "powerline-daemon -q" + + to :file:`.tmux.conf`. + +.. warning:: + Segments which depend on current working directory (e.g. + :py:func:`powerline.segments.common.vcs.branch`) require also setting up + :ref:`shell bindings `. It is not required to use powerline + shell prompt, :ref:`components setting ` allows to + set up only powerline bindings for tmux without altering your prompt. + Without setting up shell bindings powerline will use current working + directory of *tmux server* which is probably not what you need. + + Segments which depend on environment like + :py:func:`powerline.segments.common.env.virtualenv` will not work at all + (i.e. they will use environment of the tmux server), tracking environment + changes is going to slow down shell a lot. + + In any case it is suggested to avoid both kinds of segments in tmux + :ref:`themes ` because even support for tracking current + directory is very limited: + + #. It works only in shell. Should you e.g. run Vim and run ``:cd`` there you + will get current working directory from shell. + #. It works only in local shell and requires configuring it. + #. Some shells are not supported at all. + +IPython prompt +============== + +For IPython>=7.0, add the following line to +:file:`~/.ipython/profile_default/ipython_config.py` file in the used profile: + + +.. code-block:: Python + + from powerline.bindings.ipython.since_7 import PowerlinePrompts + c.TerminalInteractiveShell.prompts_class = PowerlinePrompts + +.. note:: + If certain graphical/colored elements are not showing, make sure `c.TerminalInteractiveShell.simple_prompt` + is set to `False` in your config. + Setting ``simple_prompt`` to False after IPython-5.0 is required regardless + of whether you use ``c.InteractiveShellApp.extensions`` setting or + ``c.TerminalInteractiveShell.prompts_class``. But you probably already have + this line because ``simple_prompt`` is set to ``False`` by default and IPython + is not very useful without it. + +For IPython>=5.0 and <7.0 it is suggested to use + +.. code-block:: Python + + from powerline.bindings.ipython.since_5 import PowerlinePrompts + c = get_config() + c.TerminalInteractiveShell.simple_prompt = False + c.TerminalInteractiveShell.prompts_class = PowerlinePrompts + + + +For IPython>=5.0 and <7.0 you may use the below set up, but it is deprecated. +For IPython>=0.11 add the following line to +:file:`~/.ipython/profile_default/ipython_config.py` file in the used profile: + +.. code-block:: Python + + c = get_config() + c.InteractiveShellApp.extensions = [ + 'powerline.bindings.ipython.post_0_11' + ] + +For IPython<0.11 add the following lines to :file:`.ipython/ipy_user_conf.py`: + +.. code-block:: Python + + # top + from powerline.bindings.ipython.pre_0_11 import setup as powerline_setup + + # main() function (assuming ipython was launched without configuration to + # create skeleton ipy_user_conf.py file): + powerline_setup() + + +IPython=0.11* is not supported and does not work. IPython<0.10 was not +tested (not installable by pip). + +.. _pdb-prompt: + +PDB prompt +========== + +To use Powerline with PDB prompt you need to use custom class. Inherit your +class from :py:class:`pdb.Pdb` and decorate it with +:py:func:`powerline.bindings.pdb.use_powerline_prompt`: + +.. code-block:: Python + + import pdb + + from powerline.bindings.pdb import use_powerline_prompt + + @use_powerline_prompt + class MyPdb(pdb.Pdb): + pass + + MyPdb.run('some.code.to.debug()') + +. Alternatively you may use + +.. code-block:: bash + + python -mpowerline.bindings.pdb path/to/script.py + +just like you used ``python -m pdb``. + +.. note: + If you are using Python-2.6 you need to use ``python + -mpowerline.bindings.pdb.__main__``, not what is shown above. + +.. warning: + Using PyPy (not PyPy3) forces ASCII-only prompts. In other cases unicode + characters are allowed, even if you use `pdbpp + `_. diff --git a/docs/source/usage/shell-prompts.rst b/docs/source/usage/shell-prompts.rst new file mode 100644 index 0000000..17774da --- /dev/null +++ b/docs/source/usage/shell-prompts.rst @@ -0,0 +1,161 @@ +.. _usage-shell: + +************* +Shell prompts +************* + +.. note:: + Powerline can operate without a background daemon, but the shell performance + can be very slow. The Powerline daemon improves this performance + significantly, but must be started separately. It is advised to add + + .. code-block:: bash + + powerline-daemon -q + + before any other powerline-related code in the shell configuration file. + +Bash prompt +=========== + +Add the following line to the :file:`bashrc`, where ``{repository_root}`` is the +absolute path to the Powerline installation directory (see :ref:`repository root +`): + +.. 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 the 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. Thus putting e.g. current time into PS2 or + PS3 prompt will not work as expected. + + At minimum they are computed once on startup. + +Zsh prompt +========== + +Add the following line to the :file:`zshrc`, where ``{repository_root}`` is the +absolute path to the Powerline installation directory (see :ref:`repository root +`): + +.. code-block:: bash + + . {repository_root}/powerline/bindings/zsh/powerline.zsh + +Fish prompt +=========== + +Add the following line to :file:`config.fish`, where ``{repository_root}`` is +the absolute path to the Powerline installation directory (see :ref:`repository +root `): + +.. code-block:: bash + + set fish_function_path $fish_function_path "{repository_root}/powerline/bindings/fish" + powerline-setup + +.. warning:: Fish is supported only starting from version 2.1. + +Rcsh prompt +=========== + +Powerline supports Plan9 rc reimplementation *by Byron Rakitzis* packaged by +many \*nix distributions. To use it add + +.. code-block:: bash + + . {repository_root}/powerline/bindings/rc/powerline.rc + +(``{repository_root}`` is the absolute path to the Powerline installation +directory, see :ref:`repository root `) to :file:`rcrc` file +(usually :file:`~/.rcrc`) and make sure ``rc`` is started as a login shell (with +``-l`` argument): otherwise this configuration file is not read. + +.. warning:: + Original Plan9 shell and its \*nix port are not supported because they are + missing ``prompt`` special function (it is being called once before each + non-continuation prompt). Since powerline could not support shell without + this or equivalent feature some other not-so-critical features of that port + were used. + +Busybox (ash), mksh and dash prompt +===================================== + +After launching busybox run the following command: + +.. code-block:: bash + + . {repository_root}/powerline/bindings/shell/powerline.sh + +where ``{repository_root}`` is the absolute path to the Powerline installation +directory (see :ref:`repository root `). + +Mksh users may put this line into ``~/.mkshrc`` file. Dash users may use the +following in ``~/.profile``: + +.. code-block:: bash + + if test "$0" != "${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. + +Python Virtual Environments (conda, pyenv) +========================================== + +If your system uses virtual environments to manage different Python versions, +such as pyenv or anaconda, these tools will add a performance delay to every +shell prompt. This delay can be bypassed by explicitly specifying your command +path. + + .. code-block:: bash + + export POWERLINE_COMMAND={path_to_powerline} + +where ``{path_to_powerline}`` is the full filepath for powerline. If you +installed Powerline within an environment, you can find this path for pyenv +with ``pyenv which powerline`` or for conda with ``which powerline``. diff --git a/docs/source/usage/wm-widgets.rst b/docs/source/usage/wm-widgets.rst new file mode 100644 index 0000000..bc63da6 --- /dev/null +++ b/docs/source/usage/wm-widgets.rst @@ -0,0 +1,101 @@ +********************** +Window manager widgets +********************** + +Awesome widget +============== + +.. note:: Powerline currently only supports awesome 3.5 and 4+. + +.. 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 :file:`rc.lua`, where ``{repository_root}`` is the absolute +path to Powerline installation directory (see :ref:`repository root +`): + +.. code-block:: lua + + package.path = package.path .. ';{repository_root}/powerline/bindings/awesome/?.lua' + require('powerline') + +Then add the ``powerline_widget`` to ``wibox``: + +.. code-block:: lua + + -- awesome3.5 + right_layout:add(powerline_widget) + + -- awesome4+ + s.mywibox:setup { + ... + { -- Right widgets + ... + powerline_widget, + }, + } + +Qtile widget +============ + +Add the following to :file:`~/.config/qtile/config.py`: + +.. code-block:: python + + from libqtile.bar import Bar + from libqtile.config import Screen + from libqtile.widget import Spacer + + from powerline.bindings.qtile.widget import PowerlineTextBox + + screens = [ + Screen( + top=Bar([ + PowerlineTextBox(update_interval=2, side='left'), + Spacer(), + PowerlineTextBox(update_interval=2, side='right'), + ], + 35 # width + ), + ), + ] + +.. _lemonbar-usage: + +lemonbar (formerly bar-aint-recursive) +====================================== + +To run the bar simply start the binding script: + + python /path/to/powerline/bindings/lemonbar/powerline-lemonbar.py + +You can specify options to be passed to ``lemonbar`` after ``--``, like so: + + python /path/to/powerline/bindings/lemonbar/powerline-lemonbar.py --height 16 -- -f "Source Code Pro for Powerline-9" + +to run with i3, simply ``exec`` this in the i3 config file and set the ``--i3`` switch: + + exec python /path/to/powerline/bindings/lemonbar/powerline-lemonbar.py --i3 + +Running the binding in i3-mode will require `i3ipc `_. + +See the `lemonbar documentation `_ for more +information and options. + +All ``powerline-lemonbar.py`` arguments: + +.. automan:: powerline.commands.lemonbar + :prog: powerline-lemonbar.py + :minimal: true + +I3 bar +====== + +Add the following to :file:`~/.config/i3/config`:: + + bar { + status_command python /path/to/powerline/bindings/i3/powerline-i3.py + font pango:PowerlineFont 12 + } + +where ``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 0000000..7e34a12 --- /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 0000000..b1582af 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 0000000..a50b939 --- /dev/null +++ b/powerline/__init__.py @@ -0,0 +1,991 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import logging + +from threading import Lock, Event + +from powerline.colorscheme import Colorscheme +from powerline.lib.config import ConfigLoader +from powerline.lib.unicode import unicode, safe_unicode, FailedUnicode +from powerline.config import DEFAULT_SYSTEM_CONFIG_DIR +from powerline.lib.dict import mergedicts +from powerline.lib.encoding import get_preferred_output_encoding +from powerline.lib.path import join +from powerline.version import __version__ + +class NotInterceptedError(BaseException): + pass + + +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 = 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.Logger('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 = 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([join(d, 'powerline') for d in config_dirs.split(':')]) + plugin_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 _set_log_handlers(common_config, logger, get_module_attr, stream=None): + '''Set log handlers + + :param dict common_config: + Configuration dictionary used to create handler. + :param logging.Logger logger: + Logger to which handlers will be attached. + :param func get_module_attr: + :py:func:`gen_module_attr_getter` output. + :param file stream: + Stream to use by default for :py:class:`logging.StreamHandler` in place + of :py:attr:`sys.stderr`. May be ``None``. + ''' + log_targets = common_config['log_file'] + num_handlers = 0 + for log_target in log_targets: + if log_target is None: + log_target = ['logging.StreamHandler', []] + elif isinstance(log_target, unicode): + log_target = os.path.expanduser(log_target) + log_dir = os.path.dirname(log_target) + if log_dir and not os.path.isdir(log_dir): + os.mkdir(log_dir) + log_target = ['logging.FileHandler', [[log_target]]] + module, handler_class_name = log_target[0].rpartition('.')[::2] + module = module or 'logging.handlers' + try: + handler_class_args = log_target[1][0] + except IndexError: + if module == 'logging' and handler_class_name == 'StreamHandler': + handler_class_args = [stream] + else: + handler_class_args = () + try: + handler_class_kwargs = log_target[1][1] + except IndexError: + handler_class_kwargs = {} + module = str(module) + handler_class_name = str(handler_class_name) + handler_class = get_module_attr(module, handler_class_name) + if not handler_class: + continue + handler = handler_class(*handler_class_args, **handler_class_kwargs) + try: + handler_level_name = log_target[2] + except IndexError: + handler_level_name = common_config['log_level'] + try: + handler_format = log_target[3] + except IndexError: + handler_format = common_config['log_format'] + handler.setLevel(getattr(logging, handler_level_name)) + handler.setFormatter(logging.Formatter(handler_format)) + logger.addHandler(handler) + num_handlers += 1 + if num_handlers == 0 and log_targets: + raise ValueError('Failed to set up any handlers') + + +def create_logger(common_config, use_daemon_threads=True, ext='__unknown__', + import_paths=None, imported_modules=None, stream=None): + '''Create logger according to provided configuration + + :param dict common_config: + Common configuration, from :py:func:`finish_common_config`. + :param bool use_daemon_threads: + Whether daemon threads should be used. Argument to + :py:class:`PowerlineLogger` constructor. + :param str ext: + Used extension. Argument to :py:class:`PowerlineLogger` constructor. + :param set imported_modules: + Set where imported modules are saved. Argument to + :py:func:`gen_module_attr_getter`. May be ``None``, in this case new + empty set is used. + :param file stream: + Stream to use by default for :py:class:`logging.StreamHandler` in place + of :py:attr:`sys.stderr`. May be ``None``. + + :return: Three objects: + + #. :py:class:`logging.Logger` instance. + #. :py:class:`PowerlineLogger` instance. + #. Function, output of :py:func:`gen_module_attr_getter`. + ''' + logger = logging.Logger('powerline') + level = getattr(logging, common_config['log_level']) + logger.setLevel(level) + + pl = PowerlineLogger(use_daemon_threads, logger, ext) + get_module_attr = gen_module_attr_getter( + pl, common_config['paths'], + set() if imported_modules is None else imported_modules) + + _set_log_handlers(common_config, logger, get_module_attr, stream) + + return logger, pl, get_module_attr + + +def get_default_theme(is_unicode=True): + '''Get default theme used by powerline + + :param bool is_unicode: + If true, return theme for unicode environments, otherwise return theme + that is supposed to be ASCII-only. + + :return: theme name. + ''' + return 'powerline_terminus' if is_unicode else 'ascii' + + +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() + default_top_theme = get_default_theme( + encoding.startswith('utf') or encoding.startswith('ucs')) + + 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('term_escape_style', 'auto') + 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]) + + if not isinstance(common_config['log_file'], list): + common_config['log_file'] = [common_config['log_file']] + + common_config['paths'] = [ + os.path.expanduser(path) for path in common_config['paths'] + ] + + 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 successful import of attribute equal to ``None`` and + unsuccessful 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 + + +LOG_KEYS = set(('log_format', 'log_level', 'log_file', 'paths')) +'''List of keys related to logging +''' + + +def _get_log_keys(common_config): + '''Return a common configuration copy with only log-related config left + + :param dict common_config: + Common configuration. + + :return: + :py:class:`dict` instance which has only keys from + :py:attr:`powerline.LOG_KEYS` left. + ''' + return dict(( + (k, v) for k, v in common_config.items() if k in LOG_KEYS + )) + + +DEFAULT_UPDATE_INTERVAL = 2 +'''Default value for :ref:`update_interval ` +''' + + +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 overridable ``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.had_logger = bool(self.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() + self.update_interval = DEFAULT_UPDATE_INTERVAL + + get_encoding = staticmethod(get_preferred_output_encoding) + '''Get encoding used by the current application + + Usually returns encoding of the current locale. + ''' + + def create_logger(self): + '''Create logger + + This function is used to create logger unless it was already specified + at initialization. + + :return: Three objects: + + #. :py:class:`logging.Logger` instance. + #. :py:class:`PowerlineLogger` instance. + #. Function, output of :py:func:`gen_module_attr_getter`. + ''' + return create_logger( + common_config=self.common_config, + use_daemon_threads=self.use_daemon_threads, + ext=self.ext, + imported_modules=self.imported_modules, + stream=self.default_log_stream, + ) + + def create_renderer(self, load_main=False, load_colors=False, load_colorscheme=False, load_theme=False): + '''(Re)create renderer object. Can be used after Powerline object was + 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']) + + log_keys_differ = (not self.prev_common_config or ( + _get_log_keys(self.prev_common_config) != _get_log_keys(self.common_config) + )) + + self.prev_common_config = self.common_config + + if log_keys_differ: + if self.had_logger: + self.pl = PowerlineLogger(self.use_daemon_threads, self.logger, self.ext) + self.get_module_attr = gen_module_attr_getter( + self.pl, self.common_config['paths'], self.imported_modules) + else: + self.logger, self.pl, self.get_module_attr = self.create_logger() + self.config_loader.pl = self.pl + + if not self.run_once: + self.config_loader.set_watcher(self.common_config['watcher']) + + mergedicts(self.renderer_options, dict( + pl=self.pl, + term_truecolor=self.common_config['term_truecolor'], + term_escape_style=self.common_config['term_escape_style'], + 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')) + self.update_interval = self.ext_config.get('update_interval', 2) + 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 components 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: + exc = e + try: + self.exception('Failed to render: {0}', str(e)) + except Exception as e: + exc = e + ret = FailedUnicode(safe_unicode(exc)) + if kwargs.get('output_width', False): + ret = ret, len(ret) + return ret + + 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: + exc = e + try: + self.exception('Failed to render: {0}', str(e)) + except Exception as e: + exc = e + yield FailedUnicode(safe_unicode(exc)) + + 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 0000000..e69de29 diff --git a/powerline/bindings/awesome/powerline-awesome.py b/powerline/bindings/awesome/powerline-awesome.py new file mode 100755 index 0000000..500d47d --- /dev/null +++ b/powerline/bindings/awesome/powerline-awesome.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from powerline.bindings.wm import DEFAULT_UPDATE_INTERVAL +from powerline.bindings.wm.awesome import run + + +def main(): + try: + interval = float(sys.argv[1]) + except IndexError: + interval = DEFAULT_UPDATE_INTERVAL + run(interval=interval) + + +if __name__ == '__main__': + main() diff --git a/powerline/bindings/awesome/powerline.lua b/powerline/bindings/awesome/powerline.lua new file mode 100644 index 0000000..470901f --- /dev/null +++ b/powerline/bindings/awesome/powerline.lua @@ -0,0 +1,15 @@ +local wibox = require('wibox') +local awful = require('awful') + +powerline_widget = wibox.widget.textbox() +powerline_widget:set_align('right') + +function powerline(mode, widget) end + +if string.find(awesome.version, 'v4') then + awful.spawn.with_shell('powerline-daemon -q') + awful.spawn.with_shell('powerline wm.awesome') +else + awful.util.spawn_with_shell('powerline-daemon -q') + awful.util.spawn_with_shell('powerline wm.awesome') +end diff --git a/powerline/bindings/bar/powerline-bar.py b/powerline/bindings/bar/powerline-bar.py new file mode 100755 index 0000000..71e8ae3 --- /dev/null +++ b/powerline/bindings/bar/powerline-bar.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import time + +from threading import Lock, Timer +from argparse import ArgumentParser + +from powerline.lemonbar import LemonbarPowerline +from powerline.lib.encoding import get_unicode_writer +from powerline.bindings.wm import DEFAULT_UPDATE_INTERVAL + + +if __name__ == '__main__': + parser = ArgumentParser(description='Powerline lemonbar bindings.') + parser.add_argument( + '--i3', action='store_true', + help='Subscribe for i3 events.' + ) + args = parser.parse_args() + powerline = LemonbarPowerline() + powerline.update_renderer() + powerline.pl.warn("The 'bar' bindings are deprecated, please switch to 'lemonbar'") + lock = Lock() + modes = ['default'] + write = get_unicode_writer(encoding='utf-8') + + def render(reschedule=False): + if reschedule: + Timer(DEFAULT_UPDATE_INTERVAL, render, kwargs={'reschedule': True}).start() + + global lock + with lock: + write(powerline.render(mode=modes[0])) + write('\n') + sys.stdout.flush() + + def update(evt): + modes[0] = evt.change + render() + + render(reschedule=True) + + if args.i3: + try: + import i3ipc + except ImportError: + import i3 + i3.Subscription(lambda evt, data, sub: print(render()), 'workspace') + else: + conn = i3ipc.Connection() + conn.on('workspace::focus', lambda conn, evt: render()) + conn.on('mode', lambda conn, evt: update(evt)) + conn.main() + + while True: + time.sleep(1e8) diff --git a/powerline/bindings/bash/powerline.sh b/powerline/bindings/bash/powerline.sh new file mode 100644 index 0000000..2c0943c --- /dev/null +++ b/powerline/bindings/bash/powerline.sh @@ -0,0 +1,153 @@ +_powerline_columns_fallback() { + if command -v 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_pane() { + echo "${TMUX_PANE:-`TMUX="$_POWERLINE_TMUX" tmux display -p "#D"`}" | \ + tr -d ' %' +} + +_powerline_tmux_setenv() { + TMUX="$_POWERLINE_TMUX" tmux setenv -g TMUX_"$1"_`_powerline_tmux_pane` "$2" + TMUX="$_POWERLINE_TMUX" tmux refresh -S +} + +_powerline_tmux_set_pwd() { + if test "$_POWERLINE_SAVED_PWD" != "$PWD" ; then + _POWERLINE_SAVED_PWD="$PWD" + _powerline_tmux_setenv PWD "$PWD" + fi +} + +_powerline_return() { + return $1 +} + +_POWERLINE_HAS_PIPESTATUS="$( + _powerline_return 0 | _powerline_return 43 + test "${PIPESTATUS[*]}" = "0 43" + echo "$?" +)" + +_powerline_has_pipestatus() { + return $_POWERLINE_HAS_PIPESTATUS +} + +_powerline_status_wrapper() { + local last_exit_code=$? last_pipe_status=( "${PIPESTATUS[@]}" ) + + if ! _powerline_has_pipestatus \ + || test "${#last_pipe_status[@]}" -eq "0" \ + || test "$last_exit_code" != "${last_pipe_status[$(( ${#last_pipe_status[@]} - 1 ))]}" ; then + last_pipe_status=() + fi + "$@" $last_exit_code "${last_pipe_status[*]}" + return $last_exit_code +} + +_powerline_add_status_wrapped_command() { + local action="$1" ; shift + local cmd="$1" ; shift + full_cmd="_powerline_status_wrapper $cmd" + if test "$action" = "append" ; then + PROMPT_COMMAND="$PROMPT_COMMAND"$'\n'"$full_cmd" + else + PROMPT_COMMAND="$full_cmd"$'\n'"$PROMPT_COMMAND" + 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 "$PROMPT_COMMAND" != "${PROMPT_COMMAND/_powerline_tmux_set_pwd}" \ + || _powerline_add_status_wrapped_command append _powerline_tmux_set_pwd + fi +} + +_powerline_local_prompt() { + # Arguments: + # 1: side + # 2: renderer_module arg + # 3: last_exit_code + # 4: last_pipe_status + # 5: jobnum + # 6: local theme + "$POWERLINE_COMMAND" $POWERLINE_COMMAND_ARGS shell $1 \ + $2 \ + --last-exit-code=$3 \ + --last-pipe-status="$4" \ + --jobnum=$5 \ + --renderer-arg="client_id=$$" \ + --renderer-arg="local_theme=$6" +} + +_powerline_prompt() { + # Arguments: + # 1: side + # 2: last_exit_code + # 3: last_pipe_status + # 4: jobnum + "$POWERLINE_COMMAND" $POWERLINE_COMMAND_ARGS shell $1 \ + --width="${COLUMNS:-$(_powerline_columns_fallback)}" \ + -r.bash \ + --last-exit-code=$2 \ + --last-pipe-status="$3" \ + --jobnum=$4 \ + --renderer-arg="client_id=$$" +} + +_powerline_set_prompt() { + local last_exit_code=$1 ; shift + local last_pipe_status=$1 ; shift + local jobnum="$(jobs -p|wc -l)" + PS1="$(_powerline_prompt aboveleft $last_exit_code "$last_pipe_status" $jobnum)" + if test -n "$POWERLINE_SHELL_CONTINUATION$POWERLINE_BASH_CONTINUATION" ; then + PS2="$(_powerline_local_prompt left -r.bash $last_exit_code "$last_pipe_status" $jobnum continuation)" + fi + if test -n "$POWERLINE_SHELL_SELECT$POWERLINE_BASH_SELECT" ; then + PS3="$(_powerline_local_prompt left '' $last_exit_code "$last_pipe_status" $jobnum select)" + fi +} + +_powerline_setup_prompt() { + VIRTUAL_ENV_DISABLE_PROMPT=1 + if test -z "${POWERLINE_COMMAND}" ; then + POWERLINE_COMMAND="$("$POWERLINE_CONFIG_COMMAND" shell command)" + fi + test "$PROMPT_COMMAND" != "${PROMPT_COMMAND%_powerline_set_prompt*}" \ + || _powerline_add_status_wrapped_command prepend _powerline_set_prompt + PS2="$(_powerline_local_prompt left -r.bash 0 0 0 continuation)" + PS3="$(_powerline_local_prompt left '' 0 0 0 select)" +} + +if test -z "${POWERLINE_CONFIG_COMMAND}" ; then + if command -v powerline-config >/dev/null ; then + POWERLINE_CONFIG_COMMAND=powerline-config + else + POWERLINE_CONFIG_COMMAND="$(dirname "$BASH_SOURCE")/../../../scripts/powerline-config" + fi +fi + +if "${POWERLINE_CONFIG_COMMAND}" shell --shell=bash uses prompt ; then + _powerline_setup_prompt +fi +if "${POWERLINE_CONFIG_COMMAND}" 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 0000000..3100633 --- /dev/null +++ b/powerline/bindings/config.py @@ -0,0 +1,286 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re +import sys +import subprocess +import shlex + +from powerline.config import POWERLINE_ROOT, TMUX_CONFIG_DIRECTORY +from powerline.lib.config import ConfigLoader +from powerline import generate_config_finder, load_config, create_logger, finish_common_config +from powerline.shell import ShellPowerline +from powerline.lib.shell import which +from powerline.bindings.tmux import (TmuxVersionInfo, run_tmux_command, set_tmux_environment, get_tmux_version, + source_tmux_file) +from powerline.lib.encoding import get_preferred_output_encoding +from powerline.renderers.tmux import attrs_to_tmux_attrs +from powerline.commands.main import finish_args + + +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''' + for root, dirs, files in os.walk(TMUX_CONFIG_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, tmux_version=None, source_tmux_file=source_tmux_file): + '''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. + ''' + tmux_version = tmux_version or get_tmux_version(pl) + source_tmux_file(os.path.join(TMUX_CONFIG_DIRECTORY, 'powerline-base.conf')) + for fname, priority in sorted(get_tmux_configs(tmux_version), key=(lambda v: v[1])): + source_tmux_file(fname) + if not os.environ.get('POWERLINE_COMMAND'): + cmd = deduce_command() + if cmd: + set_tmux_environment('POWERLINE_COMMAND', deduce_command(), remove=False) + try: + run_tmux_command('refresh-client') + except subprocess.CalledProcessError: + # On tmux-2.0 this command may fail for whatever reason. Since it is + # critical just ignore the failure. + pass + + +class EmptyArgs(object): + def __init__(self, ext, config_path): + self.ext = [ext] + self.side = 'left' + self.config_path = None + + def __getattr__(self, attr): + return None + + +def init_tmux_environment(pl, args, set_tmux_environment=set_tmux_environment): + '''Initialize tmux environment from tmux configuration + ''' + powerline = ShellPowerline(finish_args(None, os.environ, EmptyArgs('tmux', args.config_path))) + # TODO Move configuration files loading out of Powerline object and use it + # directly + powerline.update_renderer() + # FIXME Use something more stable then `theme_kwargs` + colorscheme = powerline.renderer_options['theme_kwargs']['colorscheme'] + + def get_highlighting(group): + return colorscheme.get_highlighting([group], None) + + for varname, highlight_group in ( + ('_POWERLINE_BACKGROUND_COLOR', 'background'), + ('_POWERLINE_ACTIVE_WINDOW_STATUS_COLOR', 'active_window_status'), + ('_POWERLINE_WINDOW_STATUS_COLOR', 'window_status'), + ('_POWERLINE_ACTIVITY_STATUS_COLOR', 'activity_status'), + ('_POWERLINE_BELL_STATUS_COLOR', 'bell_status'), + ('_POWERLINE_WINDOW_COLOR', 'window'), + ('_POWERLINE_WINDOW_DIVIDER_COLOR', 'window:divider'), + ('_POWERLINE_WINDOW_CURRENT_COLOR', 'window:current'), + ('_POWERLINE_WINDOW_NAME_COLOR', 'window_name'), + ('_POWERLINE_SESSION_COLOR', 'session'), + ): + highlight = get_highlighting(highlight_group) + set_tmux_environment(varname, powerline.renderer.hlstyle(**highlight)[2:-1]) + for varname, prev_group, next_group in ( + ('_POWERLINE_WINDOW_CURRENT_HARD_DIVIDER_COLOR', 'window', 'window:current'), + ('_POWERLINE_WINDOW_CURRENT_HARD_DIVIDER_NEXT_COLOR', 'window:current', 'window'), + ('_POWERLINE_SESSION_HARD_DIVIDER_NEXT_COLOR', 'session', 'background'), + ): + prev_highlight = get_highlighting(prev_group) + next_highlight = get_highlighting(next_group) + set_tmux_environment( + varname, + powerline.renderer.hlstyle( + fg=prev_highlight['bg'], + bg=next_highlight['bg'], + attrs=0, + )[2:-1] + ) + for varname, attr, group in ( + ('_POWERLINE_ACTIVE_WINDOW_FG', 'fg', 'active_window_status'), + ('_POWERLINE_WINDOW_STATUS_FG', 'fg', 'window_status'), + ('_POWERLINE_ACTIVITY_STATUS_FG', 'fg', 'activity_status'), + ('_POWERLINE_ACTIVITY_STATUS_ATTR', 'attrs', 'activity_status'), + ('_POWERLINE_BELL_STATUS_FG', 'fg', 'bell_status'), + ('_POWERLINE_BELL_STATUS_ATTR', 'attrs', 'bell_status'), + ('_POWERLINE_BACKGROUND_FG', 'fg', 'background'), + ('_POWERLINE_BACKGROUND_BG', 'bg', 'background'), + ('_POWERLINE_SESSION_FG', 'fg', 'session'), + ('_POWERLINE_SESSION_BG', 'bg', 'session'), + ('_POWERLINE_SESSION_ATTR', 'attrs', 'session'), + ('_POWERLINE_SESSION_PREFIX_FG', 'fg', 'session:prefix'), + ('_POWERLINE_SESSION_PREFIX_BG', 'bg', 'session:prefix'), + ('_POWERLINE_SESSION_PREFIX_ATTR', 'attrs', 'session:prefix'), + ): + if attr == 'attrs': + attrs = attrs_to_tmux_attrs(get_highlighting(group)[attr]) + set_tmux_environment(varname, ']#['.join(attrs)) + set_tmux_environment(varname + '_LEGACY', (','.join( + # Tmux-1.6 does not accept no… attributes in + # window-status-…-attr options. + (attr for attr in attrs if not attr.startswith('no'))) + # But it does not support empty attributes as well. + or 'none')) + else: + if powerline.common_config['term_truecolor']: + set_tmux_environment(varname, '#{0:06x}'.format(get_highlighting(group)[attr][1])) + else: + set_tmux_environment(varname, 'colour' + str(get_highlighting(group)[attr][0])) + + left_dividers = powerline.renderer.theme.dividers['left'] + set_tmux_environment('_POWERLINE_LEFT_HARD_DIVIDER', left_dividers['hard']) + set_tmux_environment('_POWERLINE_LEFT_SOFT_DIVIDER', left_dividers['soft']) + set_tmux_environment('_POWERLINE_LEFT_HARD_DIVIDER_SPACES', ( + ' ' * powerline.renderer.strwidth(left_dividers['hard']))) + + +TMUX_VAR_RE = re.compile('\$(_POWERLINE_\w+)') + + +def tmux_setup(pl, args): + tmux_environ = {} + tmux_version = get_tmux_version(pl) + + def set_tmux_environment_nosource(varname, value, remove=True): + tmux_environ[varname] = value + + def replace_cb(match): + return tmux_environ[match.group(1)] + + def replace_env(s): + return TMUX_VAR_RE.subn(replace_cb, s)[0] + + def source_tmux_file_nosource(fname): + with open(fname) as fd: + for line in fd: + if line.startswith('#') or line == '\n': + continue + args = shlex.split(line) + args = [args[0]] + [replace_env(arg) for arg in args[1:]] + run_tmux_command(*args) + + if args.source is None: + args.source = tmux_version < (1, 9) + + if args.source: + ste = set_tmux_environment + stf = source_tmux_file + else: + ste = set_tmux_environment_nosource + stf = source_tmux_file_nosource + + init_tmux_environment(pl, args, set_tmux_environment=ste) + source_tmux_files(pl, args, tmux_version=tmux_version, source_tmux_file=stf) + + +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(get_preferred_output_encoding(), config['common']) + logger, pl, get_module_attr = create_logger(common_config) + return pl + + +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): + sys.exit(1) + config = get_main_config(args) + if component in config.get('ext', {}).get('shell', {}).get('components', ('tmux', 'prompt')): + sys.exit(0) + else: + sys.exit(1) diff --git a/powerline/bindings/fish/powerline-setup.fish b/powerline/bindings/fish/powerline-setup.fish new file mode 100644 index 0000000..f5d02d6 --- /dev/null +++ b/powerline/bindings/fish/powerline-setup.fish @@ -0,0 +1,100 @@ +function powerline-setup + function _powerline_columns + if which stty >/dev/null + if stty size >/dev/null + stty size | cut -d' ' -f2 + return 0 + end + end + echo 0 + return 0 + end + + if test -z "$POWERLINE_CONFIG_COMMAND" + if which powerline-config >/dev/null + set -g POWERLINE_CONFIG_COMMAND powerline-config + else + set -g POWERLINE_CONFIG_COMMAND (dirname (status -f))/../../../scripts/powerline-config + end + end + + if env $POWERLINE_CONFIG_COMMAND shell --shell=fish uses prompt + if test -z "$POWERLINE_COMMAND" + set -g POWERLINE_COMMAND (env $POWERLINE_CONFIG_COMMAND shell command) + end + function _powerline_set_default_mode --on-variable fish_key_bindings + if test $fish_key_bindings != fish_vi_key_bindings + set -g _POWERLINE_DEFAULT_MODE default + else + set -g -e _POWERLINE_DEFAULT_MODE + end + end + function _powerline_update --on-variable POWERLINE_COMMAND + 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 addargs "$addargs --renderer-arg=mode=\$fish_bind_mode" + set -l addargs "$addargs --renderer-arg=default_mode=\$_POWERLINE_DEFAULT_MODE" + 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 + echo " + function fish_prompt + env \$POWERLINE_COMMAND $POWERLINE_COMMAND_ARGS shell $promptside $addargs + end + function fish_right_prompt + env \$POWERLINE_COMMAND $POWERLINE_COMMAND_ARGS shell right $addargs + $rpromptpast + end + function fish_mode_prompt + end + function _powerline_set_columns --on-signal WINCH + set -g _POWERLINE_COLUMNS $columnsexpr + end + " | source + _powerline_set_columns + end + _powerline_set_default_mode + _powerline_update + end + if env $POWERLINE_CONFIG_COMMAND shell --shell=fish uses tmux + if test -n "$TMUX" + if tmux refresh -S ^/dev/null + set -g _POWERLINE_TMUX "$TMUX" + function _powerline_tmux_pane + if test -z "$TMUX_PANE" + env TMUX="$_POWERLINE_TMUX" tmux display -p "#D" | tr -d ' %' + else + echo "$TMUX_PANE" | tr -d ' %' + end + end + function _powerline_tmux_setenv + env TMUX="$_POWERLINE_TMUX" tmux setenv -g TMUX_$argv[1]_(_powerline_tmux_pane) "$argv[2]" + env TMUX="$_POWERLINE_TMUX" tmux refresh -S + end + function _powerline_tmux_set_pwd --on-variable PWD + _powerline_tmux_setenv PWD "$PWD" + end + function _powerline_tmux_set_columns --on-signal WINCH + _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 0000000..f44e928 --- /dev/null +++ b/powerline/bindings/i3/powerline-i3.py @@ -0,0 +1,52 @@ +#!/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 + +from powerline.bindings.wm import get_i3_connection, i3_subscribe + +from powerline import Powerline +from powerline.lib.monotonic import monotonic + + +class I3Powerline(Powerline): + '''Powerline child for i3bar + + Currently only changes the default log target. + ''' + default_log_stream = sys.stderr + + +if __name__ == '__main__': + name = 'wm' + if len(sys.argv) > 1: + name = sys.argv[1] + + powerline = I3Powerline(name, renderer_module='i3bar') + powerline.update_renderer() + + interval = 0.5 + + print ('{"version": 1}') + print ('[') + print ('[]') + + lock = Lock() + + def render(event=None, data=None, sub=None): + global lock + with lock: + print (',[' + powerline.render()[:-1] + ']') + sys.stdout.flush() + + i3 = get_i3_connection() + i3_subscribe(i3, 'workspace', render) + + 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 0000000..e69de29 diff --git a/powerline/bindings/ipython/post_0_11.py b/powerline/bindings/ipython/post_0_11.py new file mode 100644 index 0000000..3213c51 --- /dev/null +++ b/powerline/bindings/ipython/post_0_11.py @@ -0,0 +1,126 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, + absolute_import, print_function) + +from weakref import ref +from warnings import warn + +try: + from IPython.core.prompts import PromptManager + has_prompt_manager = True +except ImportError: + has_prompt_manager = False +from IPython.core.magic import Magics, magics_class, line_magic + +from powerline.ipython import IPythonPowerline, IPythonInfo + +if has_prompt_manager: + from powerline.ipython import 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)) + + +old_prompt_manager = None + + +class ShutdownHook(object): + def __init__(self, ip): + self.powerline = lambda: None + ip.hooks.shutdown_hook.add(self) + + def __call__(self): + from IPython.core.hooks import TryNext + powerline = self.powerline() + if powerline is not None: + powerline.shutdown() + raise TryNext() + + +if has_prompt_manager: + 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 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.config_paths = config.get('config_paths') + if has_prompt_manager: + renderer_module = '.pre_5' + else: + renderer_module = '.since_7' + super(ConfigurableIPythonPowerline, self).init( + renderer_module=renderer_module) + + def do_setup(self, ip, shutdown_hook): + global old_prompt_manager + + if old_prompt_manager is None: + old_prompt_manager = ip.prompt_manager + prompt_manager = PowerlinePromptManager( + powerline=self, + shell=ip.prompt_manager.shell, + ) + ip.prompt_manager = prompt_manager + + magics = PowerlineMagics(ip, self) + shutdown_hook.powerline = ref(self) + ip.register_magics(magics) + + +def load_ipython_extension(ip): + if has_prompt_manager: + shutdown_hook = ShutdownHook(ip) + powerline = ConfigurableIPythonPowerline(ip) + powerline.setup(ip, shutdown_hook) + else: + from powerline.bindings.ipython.since_7 import PowerlinePrompts + ip.prompts_class = PowerlinePrompts + ip.prompts = PowerlinePrompts(ip) + warn(DeprecationWarning( + 'post_0_11 extension is deprecated since IPython 5, use\n' + ' from powerline.bindings.ipython.since_7 import PowerlinePrompts\n' + ' c.TerminalInteractiveShell.prompts_class = PowerlinePrompts\n' + 'or check: \n' + 'https://powerline.readthedocs.io/en/master/usage/other.html\n' + )) + + +def unload_ipython_extension(ip): + global old_prompt_manager + if old_prompt_manager is not None: + ip.prompt_manager = old_prompt_manager + old_prompt_manager = None diff --git a/powerline/bindings/ipython/pre_0_11.py b/powerline/bindings/ipython/pre_0_11.py new file mode 100644 index 0000000..2bd8095 --- /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={}, config_paths=None): + self.config_overrides = config_overrides + self.theme_overrides = theme_overrides + self.config_paths = config_paths + super(ConfigurableIPythonPowerline, self).init(renderer_module='.pre_5') + + 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/ipython/since_5.py b/powerline/bindings/ipython/since_5.py new file mode 100644 index 0000000..5a899ae --- /dev/null +++ b/powerline/bindings/ipython/since_5.py @@ -0,0 +1,81 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from weakref import ref + +from IPython.terminal.prompts import Prompts +from pygments.token import Token # NOQA + +from powerline.ipython import IPythonPowerline +from powerline.renderers.ipython.since_5 import PowerlinePromptStyle +from powerline.bindings.ipython.post_0_11 import PowerlineMagics, ShutdownHook + + +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.config_paths = config.get('config_paths') + super(ConfigurableIPythonPowerline, self).init( + renderer_module='.since_5') + + def do_setup(self, ip, prompts, shutdown_hook): + prompts.powerline = self + + msfn_missing = () + saved_msfn = getattr(ip, '_make_style_from_name', msfn_missing) + + if hasattr(saved_msfn, 'powerline_original'): + saved_msfn = saved_msfn.powerline_original + + def _make_style_from_name(ip, name): + prev_style = saved_msfn(name) + new_style = PowerlinePromptStyle(lambda: prev_style) + return new_style + + _make_style_from_name.powerline_original = saved_msfn + + if not isinstance(ip._style, PowerlinePromptStyle): + prev_style = ip._style + ip._style = PowerlinePromptStyle(lambda: prev_style) + + if not isinstance(saved_msfn, type(self.init)): + _saved_msfn = saved_msfn + saved_msfn = lambda: _saved_msfn(ip) + + if saved_msfn is not msfn_missing: + ip._make_style_from_name = _make_style_from_name + + magics = PowerlineMagics(ip, self) + ip.register_magics(magics) + + if shutdown_hook: + shutdown_hook.powerline = ref(self) + + +class PowerlinePrompts(Prompts): + '''Class that returns powerline prompts + ''' + def __init__(self, shell): + shutdown_hook = ShutdownHook(shell) + powerline = ConfigurableIPythonPowerline(shell) + self.shell = shell + powerline.do_setup(shell, self, shutdown_hook) + self.last_output_count = None + self.last_output = {} + + for prompt in ('in', 'continuation', 'rewrite', 'out'): + exec(( + 'def {0}_prompt_tokens(self, *args, **kwargs):\n' + ' if self.last_output_count != self.shell.execution_count:\n' + ' self.last_output.clear()\n' + ' self.last_output_count = self.shell.execution_count\n' + ' if "{0}" not in self.last_output:\n' + ' self.last_output["{0}"] = self.powerline.render(' + ' side="left",' + ' matcher_info="{1}",' + ' segment_info=self.shell,' + ' ) + [(Token.Generic.Prompt, " ")]\n' + ' return self.last_output["{0}"]' + ).format(prompt, 'in2' if prompt == 'continuation' else prompt)) diff --git a/powerline/bindings/ipython/since_7.py b/powerline/bindings/ipython/since_7.py new file mode 100644 index 0000000..4d35b68 --- /dev/null +++ b/powerline/bindings/ipython/since_7.py @@ -0,0 +1,78 @@ +# vim:fileencoding=utf-8:noet +from weakref import ref +from atexit import register as atexit + +from IPython.terminal.prompts import Prompts +from pygments.token import Token # NOQA + +from powerline.ipython import IPythonPowerline +from powerline.renderers.ipython.since_7 import PowerlinePromptStyle +from powerline.bindings.ipython.post_0_11 import PowerlineMagics + + +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.config_paths = config.get('config_paths') + super(ConfigurableIPythonPowerline, self).init( + renderer_module='.since_7') + + def do_setup(self, ip, prompts): + prompts.powerline = self + + msfn_missing = () + saved_msfn = getattr(ip, '_make_style_from_name', msfn_missing) + + if hasattr(saved_msfn, 'powerline_original'): + saved_msfn = saved_msfn.powerline_original + + def _make_style_from_name(ip, name): + prev_style = saved_msfn(name) + new_style = PowerlinePromptStyle(lambda: prev_style) + return new_style + + _make_style_from_name.powerline_original = saved_msfn + + if not isinstance(ip._style, PowerlinePromptStyle): + prev_style = ip._style + ip._style = PowerlinePromptStyle(lambda: prev_style) + + if not isinstance(saved_msfn, type(self.init)): + _saved_msfn = saved_msfn + saved_msfn = lambda: _saved_msfn(ip) + + if saved_msfn is not msfn_missing: + ip._make_style_from_name = _make_style_from_name + + magics = PowerlineMagics(ip, self) + ip.register_magics(magics) + + atexit(self.shutdown) + + +class PowerlinePrompts(Prompts): + '''Class that returns powerline prompts + ''' + def __init__(self, shell): + powerline = ConfigurableIPythonPowerline(shell) + self.shell = shell + powerline.do_setup(shell, self) + self.last_output_count = None + self.last_output = {} + + for prompt in ('in', 'continuation', 'rewrite', 'out'): + exec(( + 'def {0}_prompt_tokens(self, *args, **kwargs):\n' + ' if self.last_output_count != self.shell.execution_count:\n' + ' self.last_output.clear()\n' + ' self.last_output_count = self.shell.execution_count\n' + ' if "{0}" not in self.last_output:\n' + ' self.last_output["{0}"] = self.powerline.render(' + ' side="left",' + ' matcher_info="{1}",' + ' segment_info=self.shell,' + ' ) + [(Token.Generic.Prompt, " ")]\n' + ' return self.last_output["{0}"]' + ).format(prompt, 'in2' if prompt == 'continuation' else prompt)) diff --git a/powerline/bindings/lemonbar/powerline-lemonbar.py b/powerline/bindings/lemonbar/powerline-lemonbar.py new file mode 100755 index 0000000..9511f28 --- /dev/null +++ b/powerline/bindings/lemonbar/powerline-lemonbar.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import time +import re +import subprocess + +from threading import Lock, Timer + +from powerline.lemonbar import LemonbarPowerline +from powerline.commands.lemonbar import get_argparser +from powerline.bindings.wm import get_connected_xrandr_outputs + + +if __name__ == '__main__': + parser = get_argparser() + args = parser.parse_args() + + powerline = LemonbarPowerline() + powerline.update_renderer() + bars = [] + + for screen in get_connected_xrandr_outputs(powerline.pl): + command = [args.bar_command, '-g', '{0}x{1}+{2}+{3}'.format(screen['width'], args.height, screen['x'], screen['y'])] + args.args[1:] + process = subprocess.Popen(command, stdin=subprocess.PIPE) + bars.append((screen['name'], process, int(screen['width']) / 5)) + + lock = Lock() + modes = ['default'] + + def render(reschedule=False): + if reschedule: + Timer(args.interval, render, kwargs={'reschedule': True}).start() + + global lock + with lock: + for output, process, width in bars: + process.stdin.write(powerline.render(mode=modes[0], width=width, matcher_info=output).encode('utf-8') + b'\n') + process.stdin.flush() + + def update(evt): + modes[0] = evt.change + render() + + render(reschedule=True) + + if args.i3: + try: + import i3ipc + except ImportError: + import i3 + i3.Subscription(lambda evt, data, sub: render(), 'workspace') + else: + conn = i3ipc.Connection() + conn.on('workspace::focus', lambda conn, evt: render()) + conn.on('mode', lambda conn, evt: update(evt)) + conn.main() + + while True: + time.sleep(1e8) diff --git a/powerline/bindings/pdb/__init__.py b/powerline/bindings/pdb/__init__.py new file mode 100644 index 0000000..4033e61 --- /dev/null +++ b/powerline/bindings/pdb/__init__.py @@ -0,0 +1,183 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import pdb + +from powerline.pdb import PDBPowerline +from powerline.lib.encoding import get_preferred_output_encoding +from powerline.lib.unicode import unicode + + +if sys.version_info < (3,): + # XXX The below classes make code compatible with PDBpp which uses pyrepl + # which does not expect unicode or something above ASCII. They are + # completely not needed if pdbpp is not used, but that’s not always the + # case. + class PowerlineRenderBytesResult(bytes): + def __new__(cls, s, encoding=None): + encoding = encoding or s.encoding + if isinstance(s, PowerlineRenderResult): + return s.encode(encoding) + self = bytes.__new__(cls, s.encode(encoding) if isinstance(s, unicode) else s) + self.encoding = encoding + return self + + for meth in ( + '__contains__', + 'partition', 'rpartition', + 'split', 'rsplit', + 'count', 'join', + ): + exec(( + 'def {0}(self, *args):\n' + ' if any((isinstance(arg, unicode) for arg in args)):\n' + ' return self.__unicode__().{0}(*args)\n' + ' else:\n' + ' return bytes.{0}(self, *args)' + ).format(meth)) + + for meth in ( + 'find', 'rfind', + 'index', 'rindex', + ): + exec(( + 'def {0}(self, *args):\n' + ' if any((isinstance(arg, unicode) for arg in args)):\n' + ' args = [arg.encode(self.encoding) if isinstance(arg, unicode) else arg for arg in args]\n' + ' return bytes.{0}(self, *args)' + ).format(meth)) + + def __len__(self): + return len(self.decode(self.encoding)) + + def __getitem__(self, *args): + return PowerlineRenderBytesResult(bytes.__getitem__(self, *args), encoding=self.encoding) + + def __getslice__(self, *args): + return PowerlineRenderBytesResult(bytes.__getslice__(self, *args), encoding=self.encoding) + + @staticmethod + def add(encoding, *args): + if any((isinstance(arg, unicode) for arg in args)): + return PowerlineRenderResult(''.join(( + arg + if isinstance(arg, unicode) + else arg.decode(encoding) + for arg in args + )), encoding) + else: + return PowerlineRenderBytesResult(b''.join(args), encoding=encoding) + + def __add__(self, other): + return self.add(self.encoding, self, other) + + def __radd__(self, other): + return self.add(self.encoding, other, self) + + def __unicode__(self): + return PowerlineRenderResult(self) + + class PowerlineRenderResult(unicode): + def __new__(cls, s, encoding=None): + encoding = ( + encoding + or getattr(s, 'encoding', None) + or get_preferred_output_encoding() + ) + if isinstance(s, unicode): + self = unicode.__new__(cls, s) + else: + self = unicode.__new__(cls, s, encoding, 'replace') + self.encoding = encoding + return self + + def __str__(self): + return PowerlineRenderBytesResult(self) + + def __getitem__(self, *args): + return PowerlineRenderResult(unicode.__getitem__(self, *args)) + + def __getslice__(self, *args): + return PowerlineRenderResult(unicode.__getslice__(self, *args)) + + @staticmethod + def add(encoding, *args): + return PowerlineRenderResult(''.join(( + arg + if isinstance(arg, unicode) + else arg.decode(encoding) + for arg in args + )), encoding) + + def __add__(self, other): + return self.add(self.encoding, self, other) + + def __radd__(self, other): + return self.add(self.encoding, other, self) + + def encode(self, *args, **kwargs): + return PowerlineRenderBytesResult(unicode.encode(self, *args, **kwargs), args[0]) +else: + PowerlineRenderResult = str + + +def use_powerline_prompt(cls): + '''Decorator that installs powerline prompt to the class + + :param pdb.Pdb cls: + Class that should be decorated. + + :return: + ``cls`` argument or a class derived from it. Latter is used to turn + old-style classes into new-style classes. + ''' + @property + def prompt(self): + try: + powerline = self.powerline + except AttributeError: + powerline = PDBPowerline() + powerline.setup(self) + self.powerline = powerline + return PowerlineRenderResult(powerline.render(side='left')) + + @prompt.setter + def prompt(self, _): + pass + + @prompt.deleter + def prompt(self): + pass + + if not hasattr(cls, '__class__'): + # Old-style class: make it new-style or @property will not work. + old_cls = cls + + class cls(cls, object): + __module__ = cls.__module__ + __doc__ = cls.__doc__ + + cls.__name__ = old_cls.__name__ + + cls.prompt = prompt + + return cls + + +def main(): + '''Run module as a script + + Uses :py:func:`pdb.main` function directly, but prior to that it mocks + :py:class:`pdb.Pdb` class with powerline-specific class instance. + ''' + orig_pdb = pdb.Pdb + + @use_powerline_prompt + class Pdb(pdb.Pdb, object): + def __init__(self): + orig_pdb.__init__(self) + + pdb.Pdb = Pdb + + return pdb.main() diff --git a/powerline/bindings/pdb/__main__.py b/powerline/bindings/pdb/__main__.py new file mode 100755 index 0000000..768b2f2 --- /dev/null +++ b/powerline/bindings/pdb/__main__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.bindings.pdb import main + + +if __name__ == '__main__': + main() diff --git a/powerline/bindings/qtile/__init__.py b/powerline/bindings/qtile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/powerline/bindings/qtile/widget.py b/powerline/bindings/qtile/widget.py new file mode 100644 index 0000000..92e3a27 --- /dev/null +++ b/powerline/bindings/qtile/widget.py @@ -0,0 +1,61 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from libqtile.bar import CALCULATED +from libqtile.widget import TextBox + +from powerline import Powerline + + +class QTilePowerline(Powerline): + def do_setup(self, obj): + obj.powerline = self + + +class PowerlineTextBox(TextBox): + # TODO Replace timeout argument with update_interval argument in next major + # release. + def __init__(self, timeout=2, text=b' ', width=CALCULATED, side='right', update_interval=None, **config): + super(PowerlineTextBox, self).__init__(text, width, **config) + self.side = side + self.update_interval = update_interval or timeout + self.did_run_timer_setup = False + powerline = QTilePowerline(ext='wm', renderer_module='pango_markup') + powerline.setup(self) + + def update(self): + if not self.configured: + return True + self.text = self.powerline.render(side=self.side).encode('utf-8') + self.bar.draw() + return True + + def cmd_update(self, text): + self.update(text) + + def cmd_get(self): + return self.text + + def timer_setup(self): + if not self.did_run_timer_setup: + self.did_run_timer_setup = True + self.timeout_add(self.update_interval, self.update) + + def _configure(self, qtile, bar): + super(PowerlineTextBox, self)._configure(qtile, bar) + if self.layout.markup: + # QTile-0.9.1: no need to recreate layout or run timer_setup + return + self.layout = self.drawer.textlayout( + self.text, + self.foreground, + self.font, + self.fontsize, + self.fontshadow, + markup=True, + ) + self.timer_setup() + + +# TODO: Remove this at next major release +Powerline = PowerlineTextBox diff --git a/powerline/bindings/rc/powerline.rc b/powerline/bindings/rc/powerline.rc new file mode 100644 index 0000000..b2d6538 --- /dev/null +++ b/powerline/bindings/rc/powerline.rc @@ -0,0 +1,92 @@ +fn _powerline_sigwinch { + _POWERLINE_COLUMNS = `{ + stty size | cut -d' ' -f2 + } + _powerline_tmux_setenv COLUMNS $_POWERLINE_COLUMNS +} +fn _powerline_update_pwd { + _POWERLINE_NEW_PWD = `{pwd} + if (test $^_POWERLINE_NEW_PWD '=' $^_POWERLINE_SAVED_PWD) { + _POWERLINE_SAVED_PWD = $_POWERLINE_NEW_PWD + _powerline_tmux_setenv PWD $_POWERLINE_SAVED_PWD + } +} +fn _powerline_continuation_prompt { + _powerline_prompt --renderer-arg 'local_theme=continuation' $* +} +fn _powerline_prompt { + $POWERLINE_COMMAND $POWERLINE_COMMAND_ARGS shell aboveleft -r.readline --last-pipe-status $^_POWERLINE_STATUS --last-exit-code $_POWERLINE_STATUS($#_POWERLINE_STATUS) --jobnum $_POWERLINE_JOBNUM --renderer-arg 'client_id='$pid $* +} +fn _powerline_set_prompt { + _POWERLINE_STATUS = ( $status ) + _POWERLINE_JOBNUM = $#apids + prompt = (``() { + _powerline_prompt + } ``() { + _powerline_continuation_prompt + }) + _powerline_update_pwd +} + +fn _powerline_common_setup { + fn sigwinch { + _powerline_sigwinch + } + _powerline_sigwinch + _POWERLINE_SAVED_PWD = '' +} + +fn _powerline_tmux_pane { + if (test -n $TMUX_PANE) { + echo $TMUX_PANE | tr -d ' %' + } else { + TMUX=$_POWERLINE_TMUX tmux display -p '#D' | tr -d ' %' + } +} + +fn _powerline_tmux_setenv { +} + +if (test -z $POWERLINE_CONFIG_COMMAND) { + if (which powerline-config >/dev/null) { + POWERLINE_CONFIG_COMMAND = powerline-config + } else { + echo powerline-config executable not found, unable to proceed >[2=1] + } +} +if (test -n $POWERLINE_CONFIG_COMMAND) { + if ($POWERLINE_CONFIG_COMMAND shell --shell rcsh uses prompt) { + if (test -n $POWERLINE_COMMAND_ARGS) { + # Perform splitting + POWERLINE_COMMAND_ARGS=( `{echo $POWERLINE_COMMAND_ARGS} ) + } + fn prompt { + _powerline_set_prompt + } + if (test -z $POWERLINE_SHELL_CONTINUATION$POWERLINE_RCSH_CONTINUATION) { + _POWERLINE_STATUS = 0 + _POWERLINE_JOBNUM = 0 + _POWERLINE_CONTINUATION = `{ + _powerline_continuation_prompt + } + fn _powerline_continuation_prompt { + echo -n $_POWERLINE_CONTINUATION + } + } + _powerline_common_setup + } + if (test -n $TMUX) { + if ($POWERLINE_CONFIG_COMMAND shell --shell rcsh uses tmux) { + _POWERLINE_TMUX=$TMUX + fn _powerline_tmux_setenv { + if (test -n $2) { + TMUX=$_POWERLINE_TMUX tmux setenv -g TMUX_$1^_`{ + _powerline_tmux_pane + } $2 + } + } + _powerline_common_setup + } + } +} +# vim: ft=rcshell diff --git a/powerline/bindings/shell/powerline.sh b/powerline/bindings/shell/powerline.sh new file mode 100644 index 0000000..e1b067d --- /dev/null +++ b/powerline/bindings/shell/powerline.sh @@ -0,0 +1,239 @@ +_POWERLINE_SOURCED="$_" +_powerline_columns_fallback() { + if command -v 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 "${TMPDIR:-/tmp}/powerline.XXXXXXXX")" + _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|tr -d ' ')" + } + 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 | tr -d ' ')" + } + fi + _powerline_set_set_jobs() { + return 0 + } +} + +_powerline_set_command() { + if test -z "${POWERLINE_COMMAND}" ; then + POWERLINE_COMMAND="$("$POWERLINE_CONFIG_COMMAND" shell command)" + fi +} + +_powerline_tmux_pane() { + echo "${TMUX_PANE:-`TMUX="$_POWERLINE_TMUX" tmux display -p "#D"`}" | \ + tr -d ' %' +} + +_powerline_tmux_setenv() { + TMUX="$_POWERLINE_TMUX" tmux setenv -g TMUX_"$1"_`_powerline_tmux_pane` "$2" + TMUX="$_POWERLINE_TMUX" tmux refresh -S +} + +_powerline_tmux_set_pwd() { + if test "$_POWERLINE_SAVED_PWD" != "$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" $POWERLINE_COMMAND_ARGS 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" $POWERLINE_COMMAND_ARGS 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_COMMAND}" ; then + if command -v powerline-config >/dev/null ; then + POWERLINE_CONFIG_COMMAND=powerline-config + else + POWERLINE_CONFIG_COMMAND="$(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##*/} + +if "${POWERLINE_CONFIG_COMMAND}" shell uses tmux ; then + _powerline_init_tmux_support $_POWERLINE_USED_SHELL +fi +if "${POWERLINE_CONFIG_COMMAND}" 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 0000000..4897b4c --- /dev/null +++ b/powerline/bindings/tcsh/powerline.tcsh @@ -0,0 +1,60 @@ +# 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_COMMAND then + if ( { which powerline-config > /dev/null } ) then + set POWERLINE_CONFIG_COMMAND="powerline-config" + else + set POWERLINE_CONFIG_COMMAND="$POWERLINE_SOURCED[2]:h:h:h:h/scripts/powerline-config" + endif +else + if "$POWERLINE_CONFIG_COMMAND" == "" then + if ( { which powerline-config > /dev/null } ) then + set POWERLINE_CONFIG_COMMAND="powerline-config" + else + set POWERLINE_CONFIG_COMMAND="$POWERLINE_SOURCED[2]:h:h:h:h/scripts/powerline-config" + endif + endif +endif +if ( { $POWERLINE_CONFIG_COMMAND shell --shell=tcsh uses tmux } ) then + if ( $?TMUX_PANE ) then + if ( "$TMUX_PANE" == "" ) then + set _POWERLINE_TMUX_PANE="`tmux display -p '#D'`" + else + set _POWERLINE_TMUX_PANE="$TMUX_PANE" + endif + else + set _POWERLINE_TMUX_PANE="`tmux display -p '#D'`" + endif + set _POWERLINE_TMUX_PANE="`echo $_POWERLINE_TMUX_PANE:q | tr -d '% '`" + alias _powerline_tmux_set_pwd 'if ( $?TMUX && { tmux refresh -S >&/dev/null } ) tmux setenv -g TMUX_PWD_$_POWERLINE_TMUX_PANE $PWD:q ; if ( $?TMUX ) tmux refresh -S >&/dev/null' + alias cwdcmd "`alias cwdcmd` ; _powerline_tmux_set_pwd" +endif +if ( { $POWERLINE_CONFIG_COMMAND shell --shell=tcsh uses prompt } ) then + if ! $?POWERLINE_COMMAND then + set POWERLINE_COMMAND="`$POWERLINE_CONFIG_COMMAND:q shell command`" + else + if "$POWERLINE_COMMAND" == "" then + set POWERLINE_COMMAND="`$POWERLINE_CONFIG_COMMAND:q shell command`" + endif + endif + if ! $?POWERLINE_COMMAND_ARGS then + set POWERLINE_COMMAND_ARGS="" + endif + + if ( $?POWERLINE_NO_TCSH_ABOVE || $?POWERLINE_NO_SHELL_ABOVE ) then + alias _powerline_above true + else + alias _powerline_above '$POWERLINE_COMMAND:q $POWERLINE_COMMAND_ARGS shell above --renderer-arg=client_id=$$ --last-exit-code=$POWERLINE_STATUS --width=$POWERLINE_COLUMNS' + endif + + alias _powerline_set_prompt 'set prompt="`$POWERLINE_COMMAND:q $POWERLINE_COMMAND_ARGS shell left -r .tcsh --renderer-arg=client_id=$$ --last-exit-code=$POWERLINE_STATUS --width=$POWERLINE_COLUMNS`"' + alias _powerline_set_rprompt 'set rprompt="`$POWERLINE_COMMAND:q $POWERLINE_COMMAND_ARGS 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 0000000..eb84e7a --- /dev/null +++ b/powerline/bindings/tmux/__init__.py @@ -0,0 +1,85 @@ +# 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) + + +def set_tmux_environment(varname, value, remove=True): + '''Set tmux global environment variable + + :param str varname: + Name of the variable to set. + :param str value: + Variable value. + :param bool remove: + True if variable should be removed from the environment prior to + attaching any client (runs ``tmux set-environment -r {varname}``). + ''' + run_tmux_command('set-environment', '-g', varname, value) + if remove: + try: + run_tmux_command('set-environment', '-r', varname) + except subprocess.CalledProcessError: + # On tmux-2.0 this command may fail for whatever reason. Since it is + # critical just ignore the failure. + pass + + +def source_tmux_file(fname): + '''Source tmux configuration file + + :param str fname: + Full path to the sourced file. + ''' + run_tmux_command('source', fname) + + +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() + if version_string == 'master': + return TmuxVersionInfo(float('inf'), 0, version_string) + major, minor = version_string.split('.') + major = NON_DIGITS.subn('', major)[0] + 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-base.conf b/powerline/bindings/tmux/powerline-base.conf new file mode 100644 index 0000000..50a1079 --- /dev/null +++ b/powerline/bindings/tmux/powerline-base.conf @@ -0,0 +1,11 @@ +set -g status on +set -g status-interval 2 +set -g status-left-length 20 +set -g status-right '#(env "$POWERLINE_COMMAND" $POWERLINE_COMMAND_ARGS tmux right -R pane_id=#{pane_id})' +set -g status-right-length 150 +set -g window-status-format "#[$_POWERLINE_WINDOW_COLOR]$_POWERLINE_LEFT_HARD_DIVIDER_SPACES#I#F #[$_POWERLINE_WINDOW_DIVIDER_COLOR]$_POWERLINE_LEFT_SOFT_DIVIDER#[default]#W $_POWERLINE_LEFT_HARD_DIVIDER_SPACES" +set -g window-status-current-format "#[$_POWERLINE_WINDOW_CURRENT_HARD_DIVIDER_COLOR]$_POWERLINE_LEFT_HARD_DIVIDER#[$_POWERLINE_WINDOW_CURRENT_COLOR]#I#F $_POWERLINE_LEFT_SOFT_DIVIDER#[$_POWERLINE_WINDOW_NAME_COLOR]#W #[$_POWERLINE_WINDOW_CURRENT_HARD_DIVIDER_NEXT_COLOR]$_POWERLINE_LEFT_HARD_DIVIDER" + +# Legacy status-left definition to be overwritten for tmux Versions 1.8+ +set -g status-left "#[$_POWERLINE_SESSION_COLOR] #S #[$_POWERLINE_SESSION_HARD_DIVIDER_NEXT_COLOR]$_POWERLINE_LEFT_HARD_DIVIDER#(env \"\$POWERLINE_COMMAND\" tmux left -R pane_id=#{pane_id})" +# vim: ft=tmux diff --git a/powerline/bindings/tmux/powerline.conf b/powerline/bindings/tmux/powerline.conf new file mode 100644 index 0000000..29ec6a4 --- /dev/null +++ b/powerline/bindings/tmux/powerline.conf @@ -0,0 +1,2 @@ +if-shell 'env "$POWERLINE_CONFIG_COMMAND" tmux setup' '' 'run-shell "powerline-config tmux setup"' +# vim: ft=tmux diff --git a/powerline/bindings/tmux/powerline_tmux_1.7_plus.conf b/powerline/bindings/tmux/powerline_tmux_1.7_plus.conf new file mode 100644 index 0000000..ab7d0b4 --- /dev/null +++ b/powerline/bindings/tmux/powerline_tmux_1.7_plus.conf @@ -0,0 +1,3 @@ +set -g status-right '#(env "$POWERLINE_COMMAND" $POWERLINE_COMMAND_ARGS tmux right -R pane_id=#{pane_id} --width=#{client_width} -R width_adjust=#{status-left-length})' +set -g status-left "#[$_POWERLINE_SESSION_COLOR] #S #[$_POWERLINE_SESSION_HARD_DIVIDER_NEXT_COLOR]$_POWERLINE_LEFT_HARD_DIVIDER#(env \"\$POWERLINE_COMMAND\" tmux left --width=#{client_width} -R width_adjust=#{status-right-length} -R pane_id=#{pane_id})" +# 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 0000000..fbcd2a5 --- /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 "$_POWERLINE_ACTIVE_WINDOW_FG" +# 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 0000000..284eee0 --- /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 "$_POWERLINE_BACKGROUND_FG" +set -g status-bg "$_POWERLINE_BACKGROUND_BG" +set-window-option -g window-status-fg "$_POWERLINE_WINDOW_STATUS_FG" +set-window-option -g window-status-activity-attr "$_POWERLINE_ACTIVITY_STATUS_ATTR_LEGACY" +set-window-option -g window-status-bell-attr "$_POWERLINE_BELL_STATUS_ATTR_LEGACY" +set-window-option -g window-status-activity-fg "$_POWERLINE_ACTIVITY_STATUS_FG" +set-window-option -g window-status-bell-fg "$_POWERLINE_BELL_STATUS_FG" +# 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 0000000..e7144fb --- /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=$_POWERLINE_SESSION_PREFIX_FG]#[bg=$_POWERLINE_SESSION_PREFIX_BG]#[$_POWERLINE_SESSION_PREFIX_ATTR],#[fg=$_POWERLINE_SESSION_FG]#[bg=$_POWERLINE_SESSION_BG]#[$_POWERLINE_SESSION_ATTR]} #S #{?client_prefix,#[fg=$_POWERLINE_SESSION_PREFIX_BG],#[fg=$_POWERLINE_SESSION_BG]}#[bg=$_POWERLINE_BACKGROUND_BG]#[nobold]$_POWERLINE_LEFT_HARD_DIVIDER#(env \$POWERLINE_COMMAND \$POWERLINE_COMMAND_ARGS tmux left --width=#{client_width} -R width_adjust=#{status-right-length} -R pane_id=#{pane_id})" +# 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 0000000..b1afaf4 --- /dev/null +++ b/powerline/bindings/tmux/powerline_tmux_1.9_plus.conf @@ -0,0 +1,9 @@ +# powerline_tmux_1.9_plus.conf +# Version 1.9 introduces the foo-style options, applicable to version 1.9+ +set-option -qg status-style "$_POWERLINE_BACKGROUND_COLOR" +set-option -qg window-status-last-style "$_POWERLINE_ACTIVE_WINDOW_STATUS_COLOR" +set-window-option -qg window-status-style "$_POWERLINE_WINDOW_STATUS_COLOR" +set-window-option -qg window-status-activity-style "$_POWERLINE_ACTIVITY_STATUS_COLOR" +set-window-option -qg window-status-bell-style "$_POWERLINE_BELL_STATUS_COLOR" +set -g status-right '#(env "$POWERLINE_COMMAND" $POWERLINE_COMMAND_ARGS tmux right --width=#{client_width} -R width_adjust=#{status-left-length} -R pane_id=#{pane_id} -R pane_current_path=#{q:pane_current_path})' +# vim: ft=tmux diff --git a/powerline/bindings/tmux/powerline_tmux_2.1_plus.conf b/powerline/bindings/tmux/powerline_tmux_2.1_plus.conf new file mode 100644 index 0000000..16703b7 --- /dev/null +++ b/powerline/bindings/tmux/powerline_tmux_2.1_plus.conf @@ -0,0 +1,3 @@ +# Starting from tmux-2.1 escaping of dollar signs inside #() is harmful +set -qg status-left "#{?client_prefix,#[fg=$_POWERLINE_SESSION_PREFIX_FG]#[bg=$_POWERLINE_SESSION_PREFIX_BG]#[$_POWERLINE_SESSION_PREFIX_ATTR],#[fg=$_POWERLINE_SESSION_FG]#[bg=$_POWERLINE_SESSION_BG]#[$_POWERLINE_SESSION_ATTR]} #S #{?client_prefix,#[fg=$_POWERLINE_SESSION_PREFIX_BG],#[fg=$_POWERLINE_SESSION_BG]}#[bg=$_POWERLINE_BACKGROUND_BG]#[nobold]$_POWERLINE_LEFT_HARD_DIVIDER#(env $POWERLINE_COMMAND $POWERLINE_COMMAND_ARGS tmux left --width=#{client_width} -R width_adjust=#{status-right-length} -R pane_id=#{pane_id} -R pane_current_path=#{q:pane_current_path})" +set -g window-status-format "#[$_POWERLINE_WINDOW_COLOR]$_POWERLINE_LEFT_HARD_DIVIDER_SPACES#I#{?window_flags,#F, } #[$_POWERLINE_WINDOW_DIVIDER_COLOR]$_POWERLINE_LEFT_SOFT_DIVIDER#[default]#W $_POWERLINE_LEFT_HARD_DIVIDER_SPACES" diff --git a/powerline/bindings/vim/__init__.py b/powerline/bindings/vim/__init__.py new file mode 100644 index 0000000..b754c1f --- /dev/null +++ b/powerline/bindings/vim/__init__.py @@ -0,0 +1,482 @@ +# 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 = object() + +from powerline.lib.unicode import unicode + + +if ( + hasattr(vim, 'options') + and hasattr(vim, 'vvars') + and vim.vvars['version'] > 703 +): + if sys.version_info < (3,): + def get_vim_encoding(): + return vim.options['encoding'] or 'ascii' + else: + def get_vim_encoding(): + return vim.options['encoding'].decode('ascii') or 'ascii' +elif hasattr(vim, 'eval'): + def get_vim_encoding(): + return vim.eval('&encoding') or 'ascii' +else: + def get_vim_encoding(): + return 'utf-8' + +get_vim_encoding.__doc__ = ( + '''Get encoding used for Vim strings + + :return: + Value of ``&encoding``. If it is empty (i.e. Vim is compiled + without +multibyte) returns ``'ascii'``. When building documentation + outputs ``'utf-8'`` unconditionally. + ''' +) + + +vim_encoding = get_vim_encoding() + + +python_to_vim_types = { + unicode: ( + lambda o: b'\'' + (o.translate({ + ord('\''): '\'\'', + }).encode(vim_encoding)) + b'\'' + ), + list: ( + lambda o: b'[' + ( + b','.join((python_to_vim(i) for i in o)) + ) + b']' + ), + bytes: (lambda o: b'\'' + o.replace(b'\'', b'\'\'') + b'\''), + int: (str if str is bytes else (lambda o: unicode(o).encode('ascii'))), +} +python_to_vim_types[float] = python_to_vim_types[int] + + +def python_to_vim(o): + return python_to_vim_types[type(o)](o) + + +if sys.version_info < (3,): + def str_to_bytes(s): + return s + + def unicode_eval(expr): + ret = vim.eval(expr) + return ret.decode(vim_encoding, 'powerline_vim_strtrans_error') +else: + def str_to_bytes(s): + return s.encode(vim_encoding) + + def unicode_eval(expr): + return vim.eval(expr) + + +def safe_bytes_eval(expr): + return bytes(bytearray(( + int(chunk) for chunk in ( + vim.eval( + b'substitute(' + expr + b', ' + + b'\'^.*$\', \'\\=join(map(range(len(submatch(0))), ' + + b'"char2nr(submatch(0)[v:val])"))\', "")' + ).split() + ) + ))) + + +def eval_bytes(expr): + try: + return str_to_bytes(vim.eval(expr)) + except UnicodeDecodeError: + return safe_bytes_eval(expr) + + +def eval_unicode(expr): + try: + return unicode_eval(expr) + except UnicodeDecodeError: + return safe_bytes_eval(expr).decode(vim_encoding, 'powerline_vim_strtrans_error') + + +if hasattr(vim, 'bindeval'): + rettype_func = { + None: lambda f: f, + 'unicode': ( + lambda f: ( + lambda *args, **kwargs: ( + f(*args, **kwargs).decode( + vim_encoding, 'powerline_vim_strtrans_error' + )))) + } + rettype_func['int'] = rettype_func['bytes'] = rettype_func[None] + rettype_func['str'] = rettype_func['bytes'] if str is bytes else rettype_func['unicode'] + + def vim_get_func(f, rettype=None): + '''Return a vim function binding.''' + try: + func = vim.bindeval('function("' + f + '")') + except vim.error: + return None + else: + return rettype_func[rettype](func) +else: + rettype_eval = { + None: getattr(vim, 'eval', None), + 'int': lambda expr: int(vim.eval(expr)), + 'bytes': eval_bytes, + 'unicode': eval_unicode, + } + rettype_eval['str'] = rettype_eval[None] + + class VimFunc(object): + '''Evaluate a vim function using vim.eval(). + + This is a fallback class for older vim versions. + ''' + __slots__ = ('f', 'eval') + + def __init__(self, f, rettype=None): + self.f = f.encode('utf-8') + self.eval = rettype_eval[rettype] + + def __call__(self, *args): + return self.eval(self.f + b'(' + (b','.join(( + python_to_vim(o) for o in args + ))) + b')') + + vim_get_func = VimFunc + + +def vim_get_autoload_func(f, rettype=None): + func = vim_get_func(f) + if not func: + vim.command('runtime! ' + f.replace('#', '/')[:f.rindex('#')] + '.vim') + func = vim_get_func(f) + return func + + +if hasattr(vim, 'Function'): + def vim_func_exists(f): + try: + vim.Function(f) + except ValueError: + return False + else: + return True +else: + def vim_func_exists(f): + try: + return bool(int(vim.eval('exists("*{0}")'.format(f)))) + except vim.error: + return False + + +if type(vim) is object: + vim_get_func = lambda *args, **kwargs: None + + +_getbufvar = vim_get_func('getbufvar') +_vim_exists = vim_get_func('exists', rettype='int') + + +# 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[str('version')] > 703: + _vim_to_python_types = { + getattr(vim, 'Dictionary', None) or type(vim.bindeval('{}')): + lambda value: dict(( + (_vim_to_python(k), _vim_to_python(v)) + for k, v in value.items() + )), + 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)]) + + def vim_global_exists(name): + try: + vim.vars[name] + except KeyError: + return False + else: + return True +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]), + } + + 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 + + def vim_global_exists(name): + return int(vim.eval('exists("g:' + name + '")')) + + +def vim_command_exists(name): + return _vim_exists(':' + name) + + +if sys.version_info < (3,): + getbufvar = _getbufvar +else: + _vim_to_python_types[bytes] = lambda value: value.decode(vim_encoding) + + 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 _vim_to_python(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=python_to_vim(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(segment_info): + return segment_info['buffer'].name +else: + vim_bufname = vim_get_func('bufname', rettype='bytes') + + def buffer_name(segment_info): + try: + name = segment_info['buffer'].name + except UnicodeDecodeError: + return vim_bufname(segment_info['bufnr']) + else: + return name.encode(segment_info['encoding']) if name else None + + +vim_strtrans = vim_get_func('strtrans', rettype='unicode') + + +def powerline_vim_strtrans_error(e): + if not isinstance(e, UnicodeDecodeError): + raise NotImplementedError + text = vim_strtrans(e.object[e.start:e.end]) + 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() + + +def create_ruby_dpowerline(): + vim.command(( + ''' + ruby + if $powerline == nil + class Powerline + end + $powerline = Powerline.new + end + ''' + )) diff --git a/powerline/bindings/vim/autoload/powerline/debug.vim b/powerline/bindings/vim/autoload/powerline/debug.vim new file mode 100644 index 0000000..244319a --- /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 0000000..b06a389 --- /dev/null +++ b/powerline/bindings/vim/plugin/powerline.vim @@ -0,0 +1,169 @@ +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('python3') + let s:has_python = 1 + let s:pycmd = 'py3' + let s:pyeval = get(g:, 'powerline_pyeval', 'py3eval') +elseif has('python') + let s:has_python = 1 + let s:pycmd = 'py' + let s:pyeval = get(g:, 'powerline_pyeval', 'pyeval') +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(' powerline_instance = VimPowerline()') + call s:rcmd(' powerline_instance.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(' del powerline_instance') + 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 module.' + 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(' from powerline.lint import check') + call s:rcmd(' except ImportError:') + call s:rcmd(' print("Failed to import powerline.lint.check, cannot run powerline-lint")') + call s:rcmd(' else:') + call s:rcmd(' try:') + call s:rcmd(' paths = powerline_instance.get_config_paths()') + call s:rcmd(' except NameError:') + call s:rcmd(' pass') + call s:rcmd(' else:') + call s:rcmd(' from powerline.lint.markedjson.error import echoerr') + call s:rcmd(' ee = lambda *args, **kwargs: echoerr(*args, stream=sys.stdout, **kwargs)') + call s:rcmd(' check(paths=paths, echoerr=ee, require_ext="vim")') + 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/wm/__init__.py b/powerline/bindings/wm/__init__.py new file mode 100644 index 0000000..d2c6f30 --- /dev/null +++ b/powerline/bindings/wm/__init__.py @@ -0,0 +1,72 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re + +from powerline.theme import requires_segment_info +from powerline.lib.shell import run_cmd +from powerline.bindings.wm.awesome import AwesomeThread + + +DEFAULT_UPDATE_INTERVAL = 0.5 + + +conn = None + + +def i3_subscribe(conn, event, callback): + '''Subscribe to i3 workspace event + + :param conn: + Connection returned by :py:func:`get_i3_connection`. + :param str event: + Event to subscribe to, e.g. ``'workspace'``. + :param func callback: + Function to run on event. + ''' + conn.on(event, callback) + + from threading import Thread + + class I3Thread(Thread): + daemon = True + + def __init__(self, conn): + super(I3Thread, self).__init__() + self.__conn = conn + + def run(self): + self.__conn.main() + + thread = I3Thread(conn=conn) + + thread.start() + + +def get_i3_connection(): + '''Return a valid, cached i3 Connection instance + ''' + global conn + if not conn: + import i3ipc + conn = i3ipc.Connection() + return conn + + +XRANDR_OUTPUT_RE = re.compile(r'^(?P[0-9A-Za-z-]+) connected(?P primary)? (?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+)', re.MULTILINE) + + +def get_connected_xrandr_outputs(pl): + '''Iterate over xrandr outputs + + Outputs are represented by a dictionary with ``name``, ``width``, + ``height``, ``primary``, ``x`` and ``y`` keys. + ''' + return (match.groupdict() for match in XRANDR_OUTPUT_RE.finditer( + run_cmd(pl, ['xrandr', '-q']) + )) + + +wm_threads = { + 'awesome': AwesomeThread, +} diff --git a/powerline/bindings/wm/awesome.py b/powerline/bindings/wm/awesome.py new file mode 100644 index 0000000..b6e07f2 --- /dev/null +++ b/powerline/bindings/wm/awesome.py @@ -0,0 +1,59 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from threading import Thread, Event +from time import sleep +from subprocess import Popen, PIPE + +from powerline import Powerline +from powerline.lib.monotonic import monotonic + + +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') + + +def run(thread_shutdown_event=None, pl_shutdown_event=None, pl_config_loader=None, + interval=None): + powerline = Powerline( + 'wm', + renderer_module='pango_markup', + shutdown_event=pl_shutdown_event, + config_loader=pl_config_loader, + ) + powerline.update_renderer() + + if not thread_shutdown_event: + thread_shutdown_event = powerline.shutdown_event + + while not thread_shutdown_event.is_set(): + # powerline.update_interval may change over time + used_interval = interval or powerline.update_interval + start_time = monotonic() + s = powerline.render(side='right') + request = 'powerline_widget:set_markup(\'' + s.translate({'\'': '\\\'', '\\': '\\\\'}) + '\')\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) + thread_shutdown_event.wait(max(used_interval - (monotonic() - start_time), 0.1)) + + +class AwesomeThread(Thread): + __slots__ = ('powerline_shutdown_event',) + + def __init__(self, **kwargs): + super(AwesomeThread, self).__init__() + self.powerline_run_kwargs = kwargs + + def run(self): + run(**self.powerline_run_kwargs) diff --git a/powerline/bindings/zsh/__init__.py b/powerline/bindings/zsh/__init__.py new file mode 100644 index 0000000..44f5c69 --- /dev/null +++ b/powerline/bindings/zsh/__init__.py @@ -0,0 +1,225 @@ +# 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.overrides import parsedotval, parse_override_var +from powerline.lib.unicode import unicode, u +from powerline.lib.encoding import (get_preferred_output_encoding, + get_preferred_environment_encoding) +from powerline.lib.dict import mergeargs + + +used_powerlines = WeakValueDictionary() + + +def shutdown(): + for powerline in tuple(used_powerlines.values()): + powerline.shutdown() + + +def get_var_config(var): + try: + val = zsh.getvalue(var) + if isinstance(val, dict): + return mergeargs([parsedotval((u(k), u(v))) for k, v in val.items()]) + elif isinstance(val, (unicode, str, bytes)): + return mergeargs(parse_override_var(u(val))) + else: + return None + except: + return None + + +class Args(object): + __slots__ = ('last_pipe_status', 'last_exit_code') + ext = ['shell'] + renderer_module = '.zsh' + + @property + def config_override(self): + return get_var_config('POWERLINE_CONFIG_OVERRIDES') + + @property + def theme_override(self): + return get_var_config('POWERLINE_THEME_OVERRIDES') + + @property + def config_path(self): + try: + ret = zsh.getvalue('POWERLINE_CONFIG_PATHS') + except IndexError: + return None + else: + if isinstance(ret, (unicode, str, bytes)): + return [ + path + for path in ret.split((b':' if isinstance(ret, bytes) else ':')) + if path + ] + else: + return ret + + @property + def jobnum(self): + return zsh.getvalue('_POWERLINE_JOBNUM') + + +def string(s): + if type(s) is bytes: + return s.decode(get_preferred_environment_encoding(), '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() + + +if hasattr(zsh, 'expand') and zsh.expand('${:-}') == '': + zsh_expand = zsh.expand +else: + def zsh_expand(s): + zsh.eval('local _POWERLINE_REPLY="' + s + '"') + ret = zsh.getvalue('_POWERLINE_REPLY') + zsh.setvalue('_POWERLINE_REPLY', None) + return ret + + +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): + parser_state = u(zsh_expand('${(%):-%_}')) + shortened_path = u(zsh_expand('${(%):-%~}')) + try: + mode = u(zsh.getvalue('_POWERLINE_MODE')) + except IndexError: + mode = None + try: + default_mode = u(zsh.getvalue('_POWERLINE_DEFAULT_MODE')) + except IndexError: + default_mode = None + segment_info = { + 'args': self.args, + 'environ': environ, + 'client_id': 1, + 'local_theme': self.theme, + 'parser_state': parser_state, + 'shortened_path': shortened_path, + 'mode': mode, + 'default_mode': default_mode, + } + try: + zle_rprompt_indent = zsh.getvalue('ZLE_RPROMPT_INDENT') + except IndexError: + zle_rprompt_indent = 1 + r = '' + if self.above: + for line in self.powerline.render_above_lines( + width=zsh.columns() - zle_rprompt_indent, + segment_info=segment_info, + ): + if line: + r += line + '\n' + r += self.powerline.render( + width=zsh.columns(), + side=self.side, + segment_info=segment_info, + mode=mode, + ) + if type(r) is not str: + if type(r) is bytes: + return r.decode(get_preferred_output_encoding(), 'replace') + else: + return r.encode(get_preferred_output_encoding(), 'replace') + 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.setvalue(zpyvar, None) + 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 reload_config(): + for powerline in used_powerlines.values(): + powerline.create_renderer(load_main=True, load_colors=True, load_colorscheme=True, load_theme=True) + + +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 0000000..ff46cb0 --- /dev/null +++ b/powerline/bindings/zsh/powerline.zsh @@ -0,0 +1,216 @@ +local _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 +} + +_powerline_append_precmd_function() { + if test -z "${precmd_functions[(re)$1]}" ; then + precmd_functions+=( $1 ) + fi +} + +integer -g _POWERLINE_JOBNUM=0 + +_powerline_tmux_pane() { + local -x TMUX="$_POWERLINE_TMUX" + echo "${TMUX_PANE:-`tmux display -p "#D"`}" | tr -d ' %' +} + +_powerline_tmux_pane() { + local -x TMUX="$_POWERLINE_TMUX" + echo "${TMUX_PANE:-`tmux display -p "#D"`}" | tr -d ' %' +} + +_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"_$(_powerline_tmux_pane) "$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 + + local -a 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() { + typeset -g _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 + typeset -g _POWERLINE_DEFAULT_MODE="$_POWERLINE_MODE" + fi + + _powerline_append_precmd_function _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. + integer -g _POWERLINE_JOBNUM=${(%):-%j} +} + +_powerline_update_counter() { + zpython '_powerline.precmd()' +} + +_powerline_setup_prompt() { + emulate -L zsh + + _powerline_append_precmd_function _powerline_set_jobnum + + typeset -g VIRTUAL_ENV_DISABLE_PROMPT=1 + + if test -z "${POWERLINE_NO_ZSH_ZPYTHON}" && { zmodload libzpython || zmodload zsh/zpython } &>/dev/null ; then + _powerline_append_precmd_function _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' + } + powerline-reload-config() { + zpython 'from powerline.bindings.zsh import reload_config as _powerline_reload_config' + zpython '_powerline_reload_config()' + zpython 'del _powerline_reload_config' + } + else + if test -z "${POWERLINE_COMMAND}" ; then + typeset -g POWERLINE_COMMAND="$($POWERLINE_CONFIG_COMMAND 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=${(%):-%~}"' + add_args+=' --jobnum=$_POWERLINE_JOBNUM' + add_args+=' --renderer-arg="mode=$_POWERLINE_MODE"' + add_args+=' --renderer-arg="default_mode=$_POWERLINE_DEFAULT_MODE"' + 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)} - ${ZLE_RPROMPT_INDENT:-1} ))' + local add_args_r2=$add_args$new_args_2 + typeset -g PS1='$("$POWERLINE_COMMAND" $=POWERLINE_COMMAND_ARGS shell aboveleft '$add_args')' + typeset -g RPS1='$("$POWERLINE_COMMAND" $=POWERLINE_COMMAND_ARGS shell right '$add_args')' + typeset -g PS2='$("$POWERLINE_COMMAND" $=POWERLINE_COMMAND_ARGS shell left '$add_args_2')' + typeset -g RPS2='$("$POWERLINE_COMMAND" $=POWERLINE_COMMAND_ARGS shell right '$add_args_r2')' + typeset -g PS3='$("$POWERLINE_COMMAND" $=POWERLINE_COMMAND_ARGS 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 + typeset -g _POWERLINE_SAVE_WIDGET="$save_widget" + fi +} + +if test -z "${POWERLINE_CONFIG_COMMAND}" ; then + if which powerline-config >/dev/null 2>/dev/null ; then + typeset -g POWERLINE_CONFIG_COMMAND=powerline-config + else + typeset -g POWERLINE_CONFIG_COMMAND="${_POWERLINE_SOURCED:h:h:h:h}/scripts/powerline-config" + fi +fi + +setopt promptpercent +setopt promptsubst + +if "${POWERLINE_CONFIG_COMMAND}" shell --shell=zsh uses prompt ; then + _powerline_setup_prompt + _powerline_init_modes_support +fi +if "${POWERLINE_CONFIG_COMMAND}" 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 0000000..66416b5 --- /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_attrs_flag(attrs): + '''Convert an attribute array to a renderer flag.''' + attrs_flag = 0 + if 'bold' in attrs: + attrs_flag |= ATTR_BOLD + if 'italic' in attrs: + attrs_flag |= ATTR_ITALIC + if 'underline' in attrs: + attrs_flag |= ATTR_UNDERLINE + return attrs_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']), + 'attrs': get_attrs_flag(group_props.get('attrs', [])), + } + + +# 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/commands/__init__.py b/powerline/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/powerline/commands/config.py b/powerline/commands/config.py new file mode 100644 index 0000000..e9ca5f9 --- /dev/null +++ b/powerline/commands/config.py @@ -0,0 +1,109 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (division, absolute_import, print_function) + +import argparse + +import powerline.bindings.config as config + + +class StrFunction(object): + def __init__(self, function, name=None): + self.name = name or function.__name__ + self.function = function + + def __call__(self, *args, **kwargs): + self.function(*args, **kwargs) + + def __str__(self): + return self.name + + +TMUX_ACTIONS = { + 'source': StrFunction(config.source_tmux_files, 'source'), + 'setenv': StrFunction(config.init_tmux_environment, 'setenv'), + 'setup': StrFunction(config.tmux_setup, 'setup'), +} + + +SHELL_ACTIONS = { + 'command': StrFunction(config.shell_command, 'command'), + 'uses': StrFunction(config.uses), +} + + +class ConfigArgParser(argparse.ArgumentParser): + def parse_args(self, *args, **kwargs): + ret = super(ConfigArgParser, self).parse_args(*args, **kwargs) + if not hasattr(ret, 'function'): + # In Python-3* `powerline-config` (without arguments) raises + # AttributeError. I have not found any standard way to display same + # error message as in Python-2*. + self.error('too few arguments') + return ret + + +def get_argparser(ArgumentParser=ConfigArgParser): + parser = ArgumentParser(description='Script used to obtain powerline configuration.') + parser.add_argument( + '-p', '--config-path', action='append', metavar='PATH', + help='Path to configuration directory. If it is present ' + 'then configuration files will only be sought in the provided path. ' + 'May be provided multiple times to search in a list of directories.' + ) + 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, if it is `setenv\' then special ' + '(prefixed with `_POWERLINE\') tmux global environment variables ' + 'are filled with data from powerline configuration. ' + 'Action `setup\' is just doing `setenv\' then `source\'.' + ) + tpg = tmux_parser.add_mutually_exclusive_group() + tpg.add_argument( + '-s', '--source', action='store_true', default=None, + help='When using `setup\': always use configuration file sourcing. ' + 'By default this is determined automatically based on tmux ' + 'version: this is the default for tmux 1.8 and below.', + ) + tpg.add_argument( + '-n', '--no-source', action='store_false', dest='source', default=None, + help='When using `setup\': in place of sourcing directly execute ' + 'configuration files. That is, read each needed ' + 'powerline-specific configuration file, substitute ' + '`$_POWERLINE_…\' variables with appropriate values and run ' + '`tmux config line\'. This is the default behaviour for ' + 'tmux 1.9 and above.' + ) + + 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', + help='Only applicable for `uses\' subcommand: makes `powerline-config\' ' + 'exit with 0 if specific component is enabled and with 1 otherwise. ' + '`tmux\' component stands for tmux bindings ' + '(e.g. those that notify tmux about current directory changes), ' + '`prompt\' component stands for shell prompt.' + ) + shell_parser.add_argument( + '-s', '--shell', + metavar='SHELL', + help='Shell for which query is run', + ) + return parser diff --git a/powerline/commands/daemon.py b/powerline/commands/daemon.py new file mode 100644 index 0000000..7e8c8ab --- /dev/null +++ b/powerline/commands/daemon.py @@ -0,0 +1,24 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (division, absolute_import, print_function) + +import argparse + + +def get_argparser(ArgumentParser=argparse.ArgumentParser): + parser = ArgumentParser(description='Daemon that improves powerline performance.') + parser.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.' + ) + parser.add_argument('--socket', '-s', help='Specify socket which will be used for connecting to daemon.') + exclusive_group = parser.add_mutually_exclusive_group() + exclusive_group.add_argument('--kill', '-k', action='store_true', help='Kill an already running instance.') + replace_group = exclusive_group.add_argument_group() + replace_group.add_argument('--foreground', '-f', action='store_true', help='Run in the foreground (don’t daemonize).') + replace_group.add_argument('--replace', '-r', action='store_true', help='Replace an already running instance.') + return parser diff --git a/powerline/commands/lemonbar.py b/powerline/commands/lemonbar.py new file mode 100644 index 0000000..547c52c --- /dev/null +++ b/powerline/commands/lemonbar.py @@ -0,0 +1,35 @@ +# vim:fileencoding=utf-8:noet +# WARNING: using unicode_literals causes errors in argparse +from __future__ import (division, absolute_import, print_function) + +import argparse + + +def get_argparser(ArgumentParser=argparse.ArgumentParser): + parser = ArgumentParser( + description='Powerline BAR bindings.' + ) + parser.add_argument( + '--i3', action='store_true', + help='Subscribe for i3 events.' + ) + parser.add_argument( + '--height', default='', + metavar='PIXELS', help='Bar height.' + ) + parser.add_argument( + '--interval', '-i', + type=float, default=0.5, + metavar='SECONDS', help='Refresh interval.' + ) + parser.add_argument( + '--bar-command', '-C', + default='lemonbar', + metavar='CMD', help='Name of the lemonbar executable to use.' + ) + parser.add_argument( + 'args', nargs=argparse.REMAINDER, + help='Extra arguments for lemonbar. Should be preceded with ``--`` ' + 'argument in order not to be confused with script own arguments.' + ) + return parser diff --git a/powerline/commands/lint.py b/powerline/commands/lint.py new file mode 100755 index 0000000..8961a65 --- /dev/null +++ b/powerline/commands/lint.py @@ -0,0 +1,21 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (division, absolute_import, print_function) + +import argparse + + +def get_argparser(ArgumentParser=argparse.ArgumentParser): + parser = ArgumentParser(description='Powerline configuration checker.') + parser.add_argument( + '-p', '--config-path', action='append', metavar='PATH', + help='Paths where configuration should be checked, in order. You must ' + 'supply all paths necessary for powerline to work, ' + 'checking partial (e.g. only user overrides) configuration ' + 'is not supported.' + ) + parser.add_argument( + '-d', '--debug', action='store_const', const=True, + help='Display additional information. Used for debugging ' + '`powerline-lint\' itself, not for debugging configuration.' + ) + return parser diff --git a/powerline/commands/main.py b/powerline/commands/main.py new file mode 100644 index 0000000..373225f --- /dev/null +++ b/powerline/commands/main.py @@ -0,0 +1,190 @@ +# vim:fileencoding=utf-8:noet +# WARNING: using unicode_literals causes errors in argparse +from __future__ import (division, absolute_import, print_function) + +import argparse +import sys + +from itertools import chain + +from powerline.lib.overrides import parsedotval, parse_override_var +from powerline.lib.dict import mergeargs +from powerline.lib.encoding import get_preferred_arguments_encoding +from powerline.lib.unicode import u, unicode +from powerline.bindings.wm import wm_threads + + +if sys.version_info < (3,): + encoding = get_preferred_arguments_encoding() + + def arg_to_unicode(s): + return unicode(s, encoding, 'replace') if not isinstance(s, unicode) else s # NOQA +else: + def arg_to_unicode(s): + return s + + +def finish_args(parser, environ, args, is_daemon=False): + '''Do some final transformations + + Transforms ``*_override`` arguments into dictionaries, adding overrides from + environment variables. Transforms ``renderer_arg`` argument into dictionary + as well, but only if it is true. + + :param dict environ: + Environment from which additional overrides should be taken from. + :param args: + Arguments object returned by + :py:meth:`argparse.ArgumentParser.parse_args`. Will be modified + in-place. + + :return: Object received as second (``args``) argument. + ''' + args.config_override = mergeargs(chain( + parse_override_var(environ.get('POWERLINE_CONFIG_OVERRIDES', '')), + (parsedotval(v) for v in args.config_override or ()), + )) + args.theme_override = mergeargs(chain( + parse_override_var(environ.get('POWERLINE_THEME_OVERRIDES', '')), + (parsedotval(v) for v in args.theme_override or ()), + )) + if args.renderer_arg: + args.renderer_arg = mergeargs((parsedotval(v) for v in args.renderer_arg), remove=True) + if 'pane_id' in args.renderer_arg: + if isinstance(args.renderer_arg['pane_id'], (bytes, unicode)): + try: + args.renderer_arg['pane_id'] = int(args.renderer_arg['pane_id'].lstrip(' %')) + except ValueError: + pass + if 'client_id' not in args.renderer_arg: + args.renderer_arg['client_id'] = args.renderer_arg['pane_id'] + args.config_path = ( + [path for path in environ.get('POWERLINE_CONFIG_PATHS', '').split(':') if path] + + (args.config_path or []) + ) + if args.ext[0].startswith('wm.'): + if not is_daemon: + parser.error('WM bindings must be used with daemon only') + elif args.ext[0][3:] not in wm_threads: + parser.error('WM binding not found') + elif not args.side: + parser.error('expected one argument') + return args + + +def int_or_sig(s): + if s.startswith('sig'): + return u(s) + else: + return int(s) + + +def get_argparser(ArgumentParser=argparse.ArgumentParser): + parser = ArgumentParser(description='Powerline prompt and statusline script.') + parser.add_argument( + 'ext', nargs=1, + help='Extension: application for which powerline command is launched ' + '(usually `shell\' or `tmux\'). Also supports `wm.\' extensions: ' + + ', '.join(('`wm.' + key + '\'' for key in wm_threads.keys())) + '.' + ) + parser.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. May be omitted for `wm.*\' extensions.' + ) + parser.add_argument( + '-r', '--renderer-module', metavar='MODULE', type=str, + help='Renderer module. Usually something like `.bash\' or `.zsh\' ' + '(with leading dot) which is `powerline.renderers.{ext}{MODULE}\', ' + 'may also be full module name (must contain at least one dot or ' + 'end with a dot in case it is top-level module) or ' + '`powerline.renderers\' submodule (in case there are no dots).' + ) + parser.add_argument( + '-w', '--width', type=int, + help='Maximum prompt with. Triggers truncation of some segments.' + ) + parser.add_argument( + '--last-exit-code', metavar='INT', type=int_or_sig, + help='Last exit code.' + ) + parser.add_argument( + '--last-pipe-status', metavar='LIST', default='', + type=lambda s: [int_or_sig(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.' + ) + parser.add_argument( + '--jobnum', metavar='INT', type=int, + help='Number of jobs.' + ) + parser.add_argument( + '-c', '--config-override', metavar='KEY.KEY=VALUE', type=arg_to_unicode, + 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.' + ) + parser.add_argument( + '-t', '--theme-override', metavar='THEME.KEY.KEY=VALUE', type=arg_to_unicode, + 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.' + ) + parser.add_argument( + '-R', '--renderer-arg', + metavar='KEY=VAL', type=arg_to_unicode, 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 reasons: `--renderer-arg\' ' + 'was added later).' + ) + parser.add_argument( + '-p', '--config-path', action='append', metavar='PATH', + help='Path to configuration directory. If it is present then ' + 'configuration files will only be sought in the provided path. ' + 'May be provided multiple times to search in a list of directories.' + ) + parser.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 parser + + +def write_output(args, powerline, segment_info, write): + 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.get('mode', None), + ): + if line: + write(line + '\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.get('mode', None), + ) + write(rendered) diff --git a/powerline/config.py b/powerline/config.py new file mode 100644 index 0000000..edcf921 --- /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 0000000..1564f18 --- /dev/null +++ b/powerline/config_files/colors.json @@ -0,0 +1,124 @@ +{ + "colors": { + "black": 16, + "white": 231, + + "green": 2, + "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, + + "yellow": 11, + "brightyellow": 220, + + "gray0": 233, + "gray1": 235, + "gray2": 236, + "gray3": 239, + "gray4": 240, + "gray5": 241, + "gray6": 244, + "gray7": 245, + "gray8": 247, + "gray9": 250, + "gray10": 252, + + "gray11": 234, + "gray90": 254, + + "gray70": [249, "b3b3b3"], + + "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 0000000..767e72e --- /dev/null +++ b/powerline/config_files/colorschemes/default.json @@ -0,0 +1,57 @@ +{ + "name": "Default", + "groups": { + "information:additional": { "fg": "gray9", "bg": "gray4", "attrs": [] }, + "information:regular": { "fg": "gray10", "bg": "gray4", "attrs": ["bold"] }, + "information:highlighted": { "fg": "white", "bg": "gray4", "attrs": [] }, + "information:priority": { "fg": "brightyellow", "bg": "mediumorange", "attrs": [] }, + "warning:regular": { "fg": "white", "bg": "brightred", "attrs": ["bold"] }, + "critical:failure": { "fg": "white", "bg": "darkestred", "attrs": [] }, + "critical:success": { "fg": "white", "bg": "darkestgreen", "attrs": [] }, + "background": { "fg": "white", "bg": "gray0", "attrs": [] }, + "background:divider": { "fg": "gray5", "bg": "gray0", "attrs": [] }, + "session": { "fg": "black", "bg": "gray10", "attrs": ["bold"] }, + "date": { "fg": "gray8", "bg": "gray2", "attrs": [] }, + "time": { "fg": "gray10", "bg": "gray2", "attrs": ["bold"] }, + "time:divider": { "fg": "gray5", "bg": "gray2", "attrs": [] }, + "email_alert": "warning:regular", + "email_alert_gradient": { "fg": "white", "bg": "yellow_orange_red", "attrs": ["bold"] }, + "hostname": { "fg": "black", "bg": "gray10", "attrs": ["bold"] }, + "weather": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "weather_temp_gradient": { "fg": "blue_red", "bg": "gray0", "attrs": [] }, + "weather_condition_hot": { "fg": "khaki1", "bg": "gray0", "attrs": [] }, + "weather_condition_snowy": { "fg": "skyblue1", "bg": "gray0", "attrs": [] }, + "weather_condition_rainy": { "fg": "skyblue1", "bg": "gray0", "attrs": [] }, + "uptime": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "external_ip": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "internal_ip": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "network_load": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "network_load_gradient": { "fg": "green_yellow_orange_red", "bg": "gray0", "attrs": [] }, + "network_load_sent_gradient": "network_load_gradient", + "network_load_recv_gradient": "network_load_gradient", + "network_load:divider": "background:divider", + "system_load": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "system_load_gradient": { "fg": "green_yellow_orange_red", "bg": "gray0", "attrs": [] }, + "environment": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "cpu_load_percent": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "cpu_load_percent_gradient": { "fg": "green_yellow_orange_red", "bg": "gray0", "attrs": [] }, + "battery": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "battery_gradient": { "fg": "white_red", "bg": "gray0", "attrs": [] }, + "battery_full": { "fg": "red", "bg": "gray0", "attrs": [] }, + "battery_empty": { "fg": "white", "bg": "gray0", "attrs": [] }, + "player": { "fg": "gray10", "bg": "black", "attrs": [] }, + "user": { "fg": "white", "bg": "darkblue", "attrs": ["bold"] }, + "branch": { "fg": "gray9", "bg": "gray2", "attrs": [] }, + "branch_dirty": { "fg": "brightyellow", "bg": "gray2", "attrs": [] }, + "branch_clean": { "fg": "gray9", "bg": "gray2", "attrs": [] }, + "branch:divider": { "fg": "gray7", "bg": "gray2", "attrs": [] }, + "stash": "branch_dirty", + "stash:divider": "branch:divider", + "cwd": "information:additional", + "cwd:current_folder": "information:regular", + "cwd:divider": { "fg": "gray7", "bg": "gray4", "attrs": [] }, + "virtualenv": { "fg": "white", "bg": "darkcyan", "attrs": [] }, + "attached_clients": { "fg": "gray8", "bg": "gray0", "attrs": [] }, + "workspace": "information:regular" + } +} diff --git a/powerline/config_files/colorschemes/ipython/__main__.json b/powerline/config_files/colorschemes/ipython/__main__.json new file mode 100644 index 0000000..982ea35 --- /dev/null +++ b/powerline/config_files/colorschemes/ipython/__main__.json @@ -0,0 +1,6 @@ +{ + "groups": { + "prompt": "information:additional", + "prompt_count": "information:highlighted" + } +} diff --git a/powerline/config_files/colorschemes/pdb/__main__.json b/powerline/config_files/colorschemes/pdb/__main__.json new file mode 100644 index 0000000..01a51fe --- /dev/null +++ b/powerline/config_files/colorschemes/pdb/__main__.json @@ -0,0 +1,8 @@ +{ + "groups": { + "current_code_name": "information:additional", + "current_context": "current_code_name", + "current_line": "information:regular", + "current_file": "information:regular" + } +} diff --git a/powerline/config_files/colorschemes/pdb/default.json b/powerline/config_files/colorschemes/pdb/default.json new file mode 100644 index 0000000..b97acf7 --- /dev/null +++ b/powerline/config_files/colorschemes/pdb/default.json @@ -0,0 +1,5 @@ +{ + "groups": { + "stack_depth": { "fg": "gray1", "bg": "gray10", "attrs": ["bold"] } + } +} diff --git a/powerline/config_files/colorschemes/pdb/solarized.json b/powerline/config_files/colorschemes/pdb/solarized.json new file mode 100644 index 0000000..2e1c787 --- /dev/null +++ b/powerline/config_files/colorschemes/pdb/solarized.json @@ -0,0 +1,5 @@ +{ + "groups": { + "stack_depth": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": ["bold"] } + } +} diff --git a/powerline/config_files/colorschemes/shell/__main__.json b/powerline/config_files/colorschemes/shell/__main__.json new file mode 100644 index 0000000..6e3856f --- /dev/null +++ b/powerline/config_files/colorschemes/shell/__main__.json @@ -0,0 +1,10 @@ +{ + "groups": { + "continuation": "cwd", + "continuation:current": "cwd:current_folder", + "exit_fail": "critical:failure", + "exit_success": "critical:success", + "jobnum": "information:priority", + "superuser": "warning:regular" + } +} diff --git a/powerline/config_files/colorschemes/shell/default.json b/powerline/config_files/colorschemes/shell/default.json new file mode 100644 index 0000000..1126feb --- /dev/null +++ b/powerline/config_files/colorschemes/shell/default.json @@ -0,0 +1,16 @@ +{ + "name": "Default color scheme for shell prompts", + "groups": { + "hostname": { "fg": "brightyellow", "bg": "mediumorange", "attrs": [] }, + "environment": { "fg": "white", "bg": "darkestgreen", "attrs": [] }, + "mode": { "fg": "darkestgreen", "bg": "brightgreen", "attrs": ["bold"] }, + "attached_clients": { "fg": "white", "bg": "darkestgreen", "attrs": [] } + }, + "mode_translations": { + "vicmd": { + "groups": { + "mode": {"fg": "darkestcyan", "bg": "white", "attrs": ["bold"]} + } + } + } +} diff --git a/powerline/config_files/colorschemes/shell/solarized.json b/powerline/config_files/colorschemes/shell/solarized.json new file mode 100644 index 0000000..69dcab1 --- /dev/null +++ b/powerline/config_files/colorschemes/shell/solarized.json @@ -0,0 +1,13 @@ +{ + "name": "Solarized dark for shell", + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:green", "attrs": ["bold"] } + }, + "mode_translations": { + "vicmd": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attrs": ["bold"] } + } + } + } +} diff --git a/powerline/config_files/colorschemes/solarized.json b/powerline/config_files/colorschemes/solarized.json new file mode 100644 index 0000000..7951f0b --- /dev/null +++ b/powerline/config_files/colorschemes/solarized.json @@ -0,0 +1,40 @@ +{ + "name": "Solarized dark", + "groups": { + "information:additional": { "fg": "solarized:base2", "bg": "solarized:base01", "attrs": [] }, + "information:regular": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"] }, + "information:highlighted": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"]}, + "information:priority": { "fg": "solarized:base3", "bg": "solarized:yellow", "attrs": [] }, + "warning:regular": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": [] }, + "critical:failure": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": [] }, + "critical:success": { "fg": "solarized:base3", "bg": "solarized:green", "attrs": [] }, + "background": { "fg": "solarized:base3", "bg": "solarized:base02", "attrs": [] }, + "background:divider": { "fg": "solarized:base1", "bg": "solarized:base03", "attrs": [] }, + "user": { "fg": "solarized:base3", "bg": "solarized:blue", "attrs": ["bold"] }, + "virtualenv": { "fg": "solarized:base3", "bg": "solarized:green", "attrs": [] }, + "branch": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base02", "attrs": [] }, + "branch_clean": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "stash": "branch_dirty", + "email_alert_gradient": { "fg": "solarized:base3", "bg": "yellow_orange_red", "attrs": [] }, + "email_alert": "warning:regular", + "cwd": "information:additional", + "cwd:current_folder": "information:regular", + "cwd:divider": { "fg": "solarized:base1", "bg": "solarized:base01", "attrs": [] }, + "network_load": { "fg": "solarized:base1", "bg": "solarized:base03", "attrs": [] }, + "network_load:divider": "network_load", + "network_load_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base03", "attrs": [] }, + "network_load_sent_gradient": "network_load_gradient", + "network_load_recv_gradient": "network_load_gradient", + "hostname": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] }, + "environment": { "fg": "solarized:base3", "bg": "solarized:green", "attrs": [] }, + "attached_clients": { "fg": "solarized:base3", "bg": "solarized:green", "attrs": [] }, + "date": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "time": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": ["bold"] }, + "time:divider": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "system_load": { "fg": "solarized:base1", "bg": "solarized:base03", "attrs": [] }, + "weather_temp_gradient": { "fg": "blue_red", "bg": "solarized:base03", "attrs": [] }, + "weather": { "fg": "solarized:base1", "bg": "solarized:base03", "attrs": [] }, + "uptime": { "fg": "solarized:base1", "bg": "solarized:base03", "attrs": [] } + } +} diff --git a/powerline/config_files/colorschemes/tmux/default.json b/powerline/config_files/colorschemes/tmux/default.json new file mode 100644 index 0000000..8525fb0 --- /dev/null +++ b/powerline/config_files/colorschemes/tmux/default.json @@ -0,0 +1,14 @@ +{ + "groups": { + "active_window_status": {"fg": "darkblue", "bg": "gray0", "attrs": []}, + "window_status": {"fg": "gray70", "bg": "gray0", "attrs": []}, + "activity_status": {"fg": "yellow", "bg": "gray0", "attrs": []}, + "bell_status": {"fg": "red", "bg": "gray0", "attrs": []}, + "window": {"fg": "gray6", "bg": "gray0", "attrs": []}, + "window:divider": {"fg": "gray4", "bg": "gray0", "attrs": []}, + "window:current": {"fg": "mediumcyan", "bg": "darkblue", "attrs": []}, + "window_name": {"fg": "white", "bg": "darkblue", "attrs": ["bold"]}, + "session": {"fg": "black", "bg": "gray90", "attrs": ["bold"]}, + "session:prefix": {"fg": "gray90", "bg": "darkblue", "attrs": ["bold"]} + } +} diff --git a/powerline/config_files/colorschemes/tmux/solarized.json b/powerline/config_files/colorschemes/tmux/solarized.json new file mode 100644 index 0000000..20c42d3 --- /dev/null +++ b/powerline/config_files/colorschemes/tmux/solarized.json @@ -0,0 +1,14 @@ +{ + "groups": { + "active_window_status": { "fg": "solarized:blue", "bg": "solarized:base02", "attrs": [] }, + "window_status": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "activity_status": { "fg": "solarized:yellow", "bg": "solarized:base02", "attrs": [] }, + "bell_status": { "fg": "solarized:red", "bg": "solarized:base02", "attrs": [] }, + "window": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "window:divider": { "fg": "solarized:base01", "bg": "solarized:base02", "attrs": [] }, + "window:current": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] }, + "window_name": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"] }, + "session": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] }, + "session:prefix": { "fg": "solarized:base01", "bg": "solarized:base3", "attrs": [] } + } +} diff --git a/powerline/config_files/colorschemes/vim/__main__.json b/powerline/config_files/colorschemes/vim/__main__.json new file mode 100644 index 0000000..4f2a817 --- /dev/null +++ b/powerline/config_files/colorschemes/vim/__main__.json @@ -0,0 +1,51 @@ +{ + "groups": { + "branch_clean": "branch", + "environment": "information:unimportant", + "file_size": "information:unimportant", + "file_format": "information:unimportant", + "file_encoding": "file_format", + "file_bom": "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", + "capslock_indicator": "paste_indicator", + + "csv:column_number": "line_current", + "csv:column_name": "line_current_symbol", + + "tab:background": "background", + "tab:divider": "background:divider", + + "tab_nc:modified_indicator": "modified_indicator", + "tab_nc:file_directory": "information:unimportant", + "tab_nc:file_name": "tab_nc:file_directory", + "tab_nc:tabnr": "tab_nc:file_directory", + + "buf_nc:file_directory": "tab_nc:file_directory", + "buf_nc:file_name": "buf_nc:file_directory", + "buf_nc:bufnr": "buf_nc:file_directory", + "buf_nc:modified_indicator": "tab_nc:modified_indicator", + + "buf_nc_mod:file_directory": "tab_nc:file_directory", + "buf_nc_mod:file_name": "buf_nc_mod:file_directory", + "buf_nc_mod:bufnr": "buf_nc_mod:file_directory", + "buf_nc_mod:modified_indicator": "tab_nc:modified_indicator", + + + "commandt:label": "file_name", + "commandt:background": "background", + "commandt:finder": "file_name", + "commandt:path": "file_directory" + } +} diff --git a/powerline/config_files/colorschemes/vim/default.json b/powerline/config_files/colorschemes/vim/default.json new file mode 100644 index 0000000..e02a160 --- /dev/null +++ b/powerline/config_files/colorschemes/vim/default.json @@ -0,0 +1,154 @@ +{ + "name": "Default color scheme", + "groups": { + "information:unimportant": { "fg": "gray8", "bg": "gray2", "attrs": [] }, + "information:additional": { "fg": "gray9", "bg": "gray4", "attrs": [] }, + "background": { "fg": "white", "bg": "gray2", "attrs": [] }, + "background:divider": { "fg": "gray6", "bg": "gray2", "attrs": [] }, + "mode": { "fg": "darkestgreen", "bg": "brightgreen", "attrs": ["bold"] }, + "visual_range": { "fg": "brightestorange", "bg": "darkorange", "attrs": ["bold"] }, + "modified_indicator": { "fg": "brightyellow", "bg": "gray4", "attrs": ["bold"] }, + "paste_indicator": { "fg": "white", "bg": "mediumorange", "attrs": ["bold"] }, + "readonly_indicator": { "fg": "brightestred", "bg": "gray4", "attrs": [] }, + "branch_dirty": { "fg": "brightyellow", "bg": "gray4", "attrs": [] }, + "branch:divider": { "fg": "gray7", "bg": "gray4", "attrs": [] }, + "file_name": { "fg": "white", "bg": "gray4", "attrs": ["bold"] }, + "window_title": { "fg": "white", "bg": "gray4", "attrs": [] }, + "file_name_no_file": { "fg": "gray9", "bg": "gray4", "attrs": ["bold"] }, + "file_vcs_status": { "fg": "brightestred", "bg": "gray4", "attrs": [] }, + "file_vcs_status_M": { "fg": "brightyellow", "bg": "gray4", "attrs": [] }, + "file_vcs_status_A": { "fg": "brightgreen", "bg": "gray4", "attrs": [] }, + "line_percent": { "fg": "gray9", "bg": "gray4", "attrs": [] }, + "line_percent_gradient": { "fg": "dark_green_gray", "bg": "gray4", "attrs": [] }, + "position": { "fg": "gray9", "bg": "gray4", "attrs": [] }, + "position_gradient": { "fg": "green_yellow_red", "bg": "gray4", "attrs": [] }, + "line_current": { "fg": "gray1", "bg": "gray10", "attrs": ["bold"] }, + "line_current_symbol": { "fg": "gray1", "bg": "gray10", "attrs": [] }, + "virtcol_current_gradient": { "fg": "dark_GREEN_Orange_red", "bg": "gray10", "attrs": [] }, + "col_current": { "fg": "gray6", "bg": "gray10", "attrs": [] }, + "modified_buffers": { "fg": "brightyellow", "bg": "gray2", "attrs": [] }, + "attached_clients": { "fg": "gray8", "bg": "gray2", "attrs": [] }, + "error": { "fg": "brightestred", "bg": "darkred", "attrs": ["bold"] }, + "warning": { "fg": "brightyellow", "bg": "darkorange", "attrs": ["bold"] }, + "current_tag": { "fg": "gray9", "bg": "gray2", "attrs": [] }, + + "tab_nc:modified_indicator": { "fg": "brightyellow", "bg": "gray2", "attrs": ["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", "attrs": ["bold"] }, + "background:divider": { "fg": "darkcyan", "bg": "darkestblue", "attrs": [] }, + "branch:divider": { "fg": "darkcyan", "bg": "darkblue", "attrs": [] } + } + }, + "ic": { + "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", "attrs": ["bold"] }, + "background:divider": { "fg": "darkcyan", "bg": "darkestblue", "attrs": [] }, + "branch:divider": { "fg": "darkcyan", "bg": "darkblue", "attrs": [] } + } + }, + "ix": { + "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", "attrs": ["bold"] }, + "background:divider": { "fg": "darkcyan", "bg": "darkestblue", "attrs": [] }, + "branch:divider": { "fg": "darkcyan", "bg": "darkblue", "attrs": [] } + } + }, + "v": { + "groups": { + "mode": { "fg": "darkorange", "bg": "brightestorange", "attrs": ["bold"] } + } + }, + "V": { + "groups": { + "mode": { "fg": "darkorange", "bg": "brightestorange", "attrs": ["bold"] } + } + }, + "^V": { + "groups": { + "mode": { "fg": "darkorange", "bg": "brightestorange", "attrs": ["bold"] } + } + }, + "R": { + "groups": { + "mode": { "fg": "white", "bg": "brightred", "attrs": ["bold"] } + } + }, + "Rc": { + "groups": { + "mode": { "fg": "white", "bg": "brightred", "attrs": ["bold"] } + } + }, + "Rx": { + "groups": { + "mode": { "fg": "white", "bg": "brightred", "attrs": ["bold"] } + } + } + } +} diff --git a/powerline/config_files/colorschemes/vim/solarized.json b/powerline/config_files/colorschemes/vim/solarized.json new file mode 100644 index 0000000..55cfaa7 --- /dev/null +++ b/powerline/config_files/colorschemes/vim/solarized.json @@ -0,0 +1,121 @@ +{ + "name": "Solarized dark for vim", + "groups": { + "information:additional": { "fg": "solarized:base2", "bg": "solarized:base01", "attrs": [] }, + "information:unimportant": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] }, + "background": { "fg": "solarized:base3", "bg": "solarized:base02", "attrs": [] }, + "background:divider": { "fg": "solarized:base00", "bg": "solarized:base02", "attrs": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:green", "attrs": ["bold"] }, + "visual_range": { "fg": "solarized:green", "bg": "solarized:base3", "attrs": ["bold"] }, + "modified_indicator": { "fg": "solarized:yellow", "bg": "solarized:base01", "attrs": ["bold"] }, + "paste_indicator": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] }, + "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base01", "attrs": [] }, + "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base01", "attrs": [] }, + "branch:divider": { "fg": "solarized:base1", "bg": "solarized:base01", "attrs": [] }, + "stash:divider": "branch:divider", + "file_name": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"] }, + "window_title": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] }, + "file_name_no_file": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": ["bold"] }, + "file_format": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "file_vcs_status": { "fg": "solarized:red", "bg": "solarized:base01", "attrs": [] }, + "file_vcs_status_M": { "fg": "solarized:yellow", "bg": "solarized:base01", "attrs": [] }, + "file_vcs_status_A": { "fg": "solarized:green", "bg": "solarized:base01", "attrs": [] }, + "line_percent": { "fg": "solarized:base3", "bg": "solarized:base00", "attrs": [] }, + "line_percent_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base00", "attrs": [] }, + "position": { "fg": "solarized:base3", "bg": "solarized:base00", "attrs": [] }, + "position_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base00", "attrs": [] }, + "line_current": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": ["bold"] }, + "line_current_symbol": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, + "virtcol_current_gradient": { "fg": "GREEN_Orange_red", "bg": "solarized:base2", "attrs": [] }, + "col_current": { "fg": "solarized:base0", "bg": "solarized:base2", "attrs": [] }, + "environment": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "attached_clients": { "fg": "solarized:base1", "bg": "solarized:base02", "attrs": [] }, + "error": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": ["bold"] }, + "warning": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] }, + "current_tag": { "fg": "solarized:base3", "bg": "solarized:base02", "attrs": ["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", "attrs": [] }, + "background:divider": { "fg": "solarized:base2", "bg": "solarized:base01", "attrs": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attrs": ["bold"] }, + "modified_indicator": { "fg": "solarized:yellow", "bg": "solarized:base2", "attrs": ["bold"] }, + "paste_indicator": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] }, + "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base2", "attrs": [] }, + "branch": { "fg": "solarized:base01", "bg": "solarized:base2", "attrs": [] }, + "branch:divider": { "fg": "solarized:base00", "bg": "solarized:base2", "attrs": [] }, + "file_directory": { "fg": "solarized:base01", "bg": "solarized:base2", "attrs": [] }, + "file_name": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": ["bold"] }, + "file_size": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": [] }, + "file_name_no_file": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": ["bold"] }, + "file_name_empty": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": [] }, + "file_format": { "fg": "solarized:base2", "bg": "solarized:base01", "attrs": [] }, + "file_vcs_status": { "fg": "solarized:red", "bg": "solarized:base2", "attrs": [] }, + "file_vcs_status_M": { "fg": "solarized:yellow", "bg": "solarized:base2", "attrs": [] }, + "file_vcs_status_A": { "fg": "solarized:green", "bg": "solarized:base2", "attrs": [] }, + "line_percent": { "fg": "solarized:base3", "bg": "solarized:base1", "attrs": [] }, + "line_percent_gradient": { "fg": "solarized:base3", "bg": "solarized:base1", "attrs": [] }, + "position": { "fg": "solarized:base3", "bg": "solarized:base1", "attrs": [] }, + "position_gradient": { "fg": "solarized:base3", "bg": "solarized:base1", "attrs": [] }, + "line_current": { "fg": "solarized:base03", "bg": "solarized:base3", "attrs": ["bold"] }, + "line_current_symbol": { "fg": "solarized:base03", "bg": "solarized:base3", "attrs": [] }, + "col_current": { "fg": "solarized:base0", "bg": "solarized:base3", "attrs": [] } + } + }, + "ic": { + "groups": { + "background": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] }, + "background:divider": { "fg": "solarized:base2", "bg": "solarized:base01", "attrs": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attrs": ["bold"] } + } + }, + "ix": { + "groups": { + "background": { "fg": "solarized:base3", "bg": "solarized:base01", "attrs": [] }, + "background:divider": { "fg": "solarized:base2", "bg": "solarized:base01", "attrs": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attrs": ["bold"] } + } + }, + "v": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] } + } + }, + "V": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] } + } + }, + "^V": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] } + } + }, + "R": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": ["bold"] } + } + }, + "Rc": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": ["bold"] } + } + }, + "Rx": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": ["bold"] } + } + } + } +} diff --git a/powerline/config_files/colorschemes/vim/solarizedlight.json b/powerline/config_files/colorschemes/vim/solarizedlight.json new file mode 100644 index 0000000..f862d39 --- /dev/null +++ b/powerline/config_files/colorschemes/vim/solarizedlight.json @@ -0,0 +1,122 @@ +{ + "name": "Solarized light for vim", + "groups": { + "information:additional": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": [] }, + "information:unimportant": { "fg": "solarized:base1", "bg": "solarized:base01", "attrs": [] }, + "background": { "fg": "solarized:base03", "bg": "solarized:base01", "attrs": [] }, + "background:divider": { "fg": "solarized:base0", "bg": "solarized:base01", "attrs": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:green", "attrs": ["bold"] }, + "visual_range": { "fg": "solarized:green", "bg": "solarized:base3", "attrs": ["bold"] }, + "modified_indicator": { "fg": "solarized:yellow", "bg": "solarized:base2", "attrs": ["bold"] }, + "paste_indicator": { "fg": "solarized:red", "bg": "solarized:base2", "attrs": ["bold"] }, + "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base2", "attrs": [] }, + "branch_dirty": { "fg": "solarized:yellow", "bg": "solarized:base2", "attrs": [] }, + "branch:divider": { "fg": "solarized:base1", "bg": "solarized:base2", "attrs": [] }, + "stash": "branch_dirty", + "stash:divider": "branch:divider", + "file_name": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": ["bold"] }, + "window_title": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, + "file_size": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, + "file_name_no_file": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": ["bold"] }, + "file_name_empty": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, + "file_vcs_status": { "fg": "solarized:red", "bg": "solarized:base2", "attrs": [] }, + "file_vcs_status_M": { "fg": "solarized:yellow", "bg": "solarized:base2", "attrs": [] }, + "file_vcs_status_A": { "fg": "solarized:green", "bg": "solarized:base2", "attrs": [] }, + "line_percent": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, + "line_percent_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base2", "attrs": [] }, + "position": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, + "position_gradient": { "fg": "green_yellow_orange_red", "bg": "solarized:base2", "attrs": [] }, + "line_current": { "fg": "solarized:base3", "bg": "solarized:base02", "attrs": ["bold"] }, + "line_current_symbol": { "fg": "solarized:base3", "bg": "solarized:base02", "attrs": [] }, + "virtcol_current_gradient": { "fg": "yellow_orange_red", "bg": "solarized:base02", "attrs": [] }, + "col_current": { "fg": "solarized:base00", "bg": "solarized:base02", "attrs": [] }, + "error": { "fg": "solarized:base03", "bg": "solarized:red", "attrs": ["bold"] }, + "warning": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": ["bold"] }, + "current_tag": { "fg": "solarized:base03", "bg": "solarized:base01", "attrs": ["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", "attrs": [] }, + "background:divider": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attrs": ["bold"] }, + "modified_indicator": { "fg": "solarized:yellow", "bg": "solarized:base02", "attrs": ["bold"] }, + "paste_indicator": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] }, + "readonly_indicator": { "fg": "solarized:red", "bg": "solarized:base02", "attrs": [] }, + "branch": { "fg": "solarized:base2", "bg": "solarized:base02", "attrs": [] }, + "branch:divider": { "fg": "solarized:base0", "bg": "solarized:base02", "attrs": [] }, + "file_directory": { "fg": "solarized:base2", "bg": "solarized:base02", "attrs": [] }, + "file_name": { "fg": "solarized:base01", "bg": "solarized:base02", "attrs": ["bold"] }, + "file_size": { "fg": "solarized:base01", "bg": "solarized:base02", "attrs": [] }, + "file_name_no_file": { "fg": "solarized:base01", "bg": "solarized:base02", "attrs": ["bold"] }, + "file_name_empty": { "fg": "solarized:base01", "bg": "solarized:base02", "attrs": [] }, + "file_format": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": [] }, + "file_vcs_status": { "fg": "solarized:red", "bg": "solarized:base02", "attrs": [] }, + "file_vcs_status_M": { "fg": "solarized:yellow", "bg": "solarized:base02", "attrs": [] }, + "file_vcs_status_A": { "fg": "solarized:green", "bg": "solarized:base02", "attrs": [] }, + "line_percent": { "fg": "solarized:base03", "bg": "solarized:base1", "attrs": [] }, + "line_percent_gradient": { "fg": "solarized:base03", "bg": "solarized:base1", "attrs": [] }, + "position": { "fg": "solarized:base03", "bg": "solarized:base1", "attrs": [] }, + "position_gradient": { "fg": "solarized:base03", "bg": "solarized:base1", "attrs": [] }, + "line_current": { "fg": "solarized:base3", "bg": "solarized:base03", "attrs": ["bold"] }, + "line_current_symbol": { "fg": "solarized:base3", "bg": "solarized:base03", "attrs": [] }, + "virtcol_current_gradient": { "fg": "yellow_orange_red", "bg": "solarized:base03", "attrs": [] }, + "col_current": { "fg": "solarized:base00", "bg": "solarized:base03", "attrs": [] } + } + }, + "ic": { + "groups": { + "background": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, + "background:divider": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attrs": ["bold"] } + } + }, + "ix": { + "groups": { + "background": { "fg": "solarized:base03", "bg": "solarized:base2", "attrs": [] }, + "background:divider": { "fg": "solarized:base02", "bg": "solarized:base2", "attrs": [] }, + "mode": { "fg": "solarized:base3", "bg": "solarized:blue", "attrs": ["bold"] } + } + }, + "v": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] } + } + }, + "V": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] } + } + }, + "^V": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:orange", "attrs": ["bold"] } + } + }, + "R": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": ["bold"] } + } + }, + "Rc": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": ["bold"] } + } + }, + "Rx": { + "groups": { + "mode": { "fg": "solarized:base3", "bg": "solarized:red", "attrs": ["bold"] } + } + } + } +} diff --git a/powerline/config_files/config.json b/powerline/config_files/config.json new file mode 100644 index 0000000..4491885 --- /dev/null +++ b/powerline/config_files/config.json @@ -0,0 +1,53 @@ +{ + "common": { + "term_truecolor": false + }, + "ext": { + "ipython": { + "colorscheme": "default", + "theme": "in", + "local_themes": { + "rewrite": "rewrite", + "out": "out", + "in2": "in2" + } + }, + "pdb": { + "colorscheme": "default", + "theme": "default" + }, + "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.commandt.commandt": "plugin_commandt", + "powerline.matchers.vim.plugin.gundo.gundo": "plugin_gundo", + "powerline.matchers.vim.plugin.gundo.gundo_preview": "plugin_gundo-preview" + } + }, + "wm": { + "colorscheme": "default", + "theme": "default", + "update_interval": 2 + } + } +} diff --git a/powerline/config_files/themes/ascii.json b/powerline/config_files/themes/ascii.json new file mode 100644 index 0000000..0ea05e7 --- /dev/null +++ b/powerline/config_files/themes/ascii.json @@ -0,0 +1,153 @@ +{ + "use_non_breaking_spaces": false, + "dividers": { + "left": { + "hard": " ", + "soft": "| " + }, + "right": { + "hard": " ", + "soft": " |" + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": "BR " + }, + "stash": { + "before": "ST " + }, + "cwd": { + "args": { + "ellipsis": "..." + } + }, + "player": { + "args": { + "state_symbols": { + "fallback": "", + "play": ">", + "pause": "~", + "stop": "X" + } + } + }, + + "line_current_symbol": { + "contents": "LN " + }, + + "time": { + "before": "" + }, + + "powerline.segments.common.net.network_load": { + "args": { + "recv_format": "DL {value:>8}", + "sent_format": "UL {value:>8}" + } + }, + "powerline.segments.common.net.hostname": { + "before": "H " + }, + "powerline.segments.common.bat.battery": { + "args": { + "full_heart": "O", + "empty_heart": "O", + "online": "C", + "offline": " " + } + }, + "powerline.segments.common.sys.uptime": { + "before": "UP " + }, + "powerline.segments.common.mail.email_imap_alert": { + "before": "MAIL " + }, + "powerline.segments.common.env.virtualenv": { + "before": "(e) " + }, + "powerline.segments.common.wthr.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.time.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", + "ic": "I-COMP", + "ix": "I-C_X ", + "R": "RPLACE", + "Rv": "V-RPLC", + "Rc": "R-COMP", + "Rx": "R-C_X ", + "c": "COMMND", + "cv": "VIM-EX", + "ce": "NRM-EX", + "r": "PROMPT", + "rm": "-MORE-", + "r?": "CNFIRM", + "!": "!SHELL", + "t": "TERM " + } + } + }, + "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": "+" + } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "O", + "changed": "X" + } + } + } + } +} diff --git a/powerline/config_files/themes/ipython/in.json b/powerline/config_files/themes/ipython/in.json new file mode 100644 index 0000000..edd4d29 --- /dev/null +++ b/powerline/config_files/themes/ipython/in.json @@ -0,0 +1,25 @@ +{ + "segments": { + "left": [ + { + "function": "powerline.segments.common.env.virtualenv", + "priority": 10 + }, + { + "type": "string", + "contents": "In [", + "draw_soft_divider": false, + "highlight_groups": ["prompt"] + }, + { + "function": "powerline.segments.ipython.prompt_count", + "draw_soft_divider": false + }, + { + "type": "string", + "contents": "]", + "highlight_groups": ["prompt"] + } + ] + } +} diff --git a/powerline/config_files/themes/ipython/in2.json b/powerline/config_files/themes/ipython/in2.json new file mode 100644 index 0000000..422c44b --- /dev/null +++ b/powerline/config_files/themes/ipython/in2.json @@ -0,0 +1,12 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "contents": "", + "width": "auto", + "highlight_groups": ["prompt"] + } + ] + } +} diff --git a/powerline/config_files/themes/ipython/out.json b/powerline/config_files/themes/ipython/out.json new file mode 100644 index 0000000..2425d0b --- /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_groups": ["prompt"] + }, + { + "function": "prompt_count", + "draw_soft_divider": false + }, + { + "type": "string", + "contents": "]", + "highlight_groups": ["prompt"] + } + ] + } +} diff --git a/powerline/config_files/themes/ipython/rewrite.json b/powerline/config_files/themes/ipython/rewrite.json new file mode 100644 index 0000000..8192fe4 --- /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_groups": ["prompt"] + }, + { + "function": "prompt_count", + "draw_soft_divider": false + }, + { + "type": "string", + "contents": ">", + "highlight_groups": ["prompt"] + } + ] + } +} diff --git a/powerline/config_files/themes/pdb/default.json b/powerline/config_files/themes/pdb/default.json new file mode 100644 index 0000000..dcae108 --- /dev/null +++ b/powerline/config_files/themes/pdb/default.json @@ -0,0 +1,27 @@ +{ + "default_module": "powerline.segments.pdb", + "segments": { + "left": [ + { + "function": "stack_depth" + }, + { + "type": "segment_list", + "function": "powerline.listers.pdb.frame_lister", + "segments": [ + { + "function": "current_file", + "after": ":" + }, + { + "function": "current_line", + "after": " " + }, + { + "function": "current_code_name" + } + ] + } + ] + } +} diff --git a/powerline/config_files/themes/powerline.json b/powerline/config_files/themes/powerline.json new file mode 100644 index 0000000..366a7ea --- /dev/null +++ b/powerline/config_files/themes/powerline.json @@ -0,0 +1,151 @@ +{ + "dividers": { + "left": { + "hard": " ", + "soft": " " + }, + "right": { + "hard": " ", + "soft": " " + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": " " + }, + "stash": { + "before": "⌆ " + }, + "cwd": { + "args": { + "ellipsis": "⋯" + } + }, + + "line_current_symbol": { + "contents": " " + }, + "player": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + + "time": { + "before": "⌚ " + }, + + "powerline.segments.common.net.network_load": { + "args": { + "recv_format": "⬇ {value:>8}", + "sent_format": "⬆ {value:>8}" + } + }, + "powerline.segments.common.net.hostname": { + "before": " " + }, + "powerline.segments.common.bat.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥", + "online": "⚡︎", + "offline": " " + } + }, + "powerline.segments.common.sys.uptime": { + "before": "⇑ " + }, + "powerline.segments.common.mail.email_imap_alert": { + "before": "✉ " + }, + "powerline.segments.common.env.virtualenv": { + "before": "ⓔ " + }, + "powerline.segments.common.wthr.weather": { + "args": { + "icons": { + "day": "〇", + "blustery": "⚑", + "rainy": "☔", + "cloudy": "☁", + "snowy": "❅", + "stormy": "☈", + "foggy": "≡", + "sunny": "☼", + "night": "☾", + "windy": "☴", + "not_available": "�", + "unknown": "⚠" + } + } + }, + "powerline.segments.common.time.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", + "ic": "I·COMP", + "ix": "I·C-X ", + "R": "RPLACE", + "Rv": "V·RPLC", + "Rc": "R·COMP", + "Rx": "R·C-X ", + "c": "COMMND", + "cv": "VIM·EX", + "ce": "NRM·EX", + "r": "PROMPT", + "rm": "-MORE-", + "r?": "CNFIRM", + "!": "!SHELL", + "t": "TERM " + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "↕{rows} ↔{vcols}", + "v_text_oneline": "↔{vcols}", + "v_text_multiline": "↕{rows}", + "V_text": "⇕{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } + } + } +} diff --git a/powerline/config_files/themes/powerline_terminus.json b/powerline/config_files/themes/powerline_terminus.json new file mode 100644 index 0000000..e5fb1c8 --- /dev/null +++ b/powerline/config_files/themes/powerline_terminus.json @@ -0,0 +1,151 @@ +{ + "dividers": { + "left": { + "hard": " ", + "soft": " " + }, + "right": { + "hard": " ", + "soft": " " + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": " " + }, + "stash": { + "before": "ST " + }, + "cwd": { + "args": { + "ellipsis": "…" + } + }, + + "line_current_symbol": { + "contents": " " + }, + "player": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + + "time": { + "before": "" + }, + + "powerline.segments.common.net.network_load": { + "args": { + "recv_format": "⇓ {value:>8}", + "sent_format": "⇑ {value:>8}" + } + }, + "powerline.segments.common.net.hostname": { + "before": " " + }, + "powerline.segments.common.bat.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥", + "online": "⚡︎", + "offline": " " + } + }, + "powerline.segments.common.sys.uptime": { + "before": "↑ " + }, + "powerline.segments.common.mail.email_imap_alert": { + "before": "MAIL " + }, + "powerline.segments.common.env.virtualenv": { + "before": "(e) " + }, + "powerline.segments.common.wthr.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.time.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", + "ic": "I·COMP", + "ix": "I·C-X ", + "R": "RPLACE", + "Rv": "V·RPLC", + "Rc": "R·COMP", + "Rx": "R·C-X ", + "c": "COMMND", + "cv": "VIM·EX", + "ce": "NRM·EX", + "r": "PROMPT", + "rm": "-MORE-", + "r?": "CNFIRM", + "!": "!SHELL", + "t": "TERM " + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "↕{rows} ↔{vcols}", + "v_text_oneline": "↔{vcols}", + "v_text_multiline": "↕{rows}", + "V_text": "⇕{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } + } + } +} diff --git a/powerline/config_files/themes/powerline_unicode7.json b/powerline/config_files/themes/powerline_unicode7.json new file mode 100644 index 0000000..bd62826 --- /dev/null +++ b/powerline/config_files/themes/powerline_unicode7.json @@ -0,0 +1,165 @@ +{ + "dividers": { + "left": { + "hard": " ", + "soft": " " + }, + "right": { + "hard": " ", + "soft": " " + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": "🔀 " + }, + "stash": { + "before": "📝" + }, + "cwd": { + "args": { + "ellipsis": "⋯" + } + }, + + "line_current_symbol": { + "contents": " " + }, + "player": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "⏵", + "pause": "⏸", + "stop": "⏹" + } + } + }, + + "time": { + "before": "🕐 " + }, + + "powerline.segments.common.net.network_load": { + "args": { + "recv_format": "⬇ {value:>8}", + "sent_format": "⬆ {value:>8}" + } + }, + "powerline.segments.common.net.hostname": { + "before": "🏠 " + }, + "powerline.segments.common.bat.battery": { + "args": { + "full_heart": "💙", + "empty_heart": "💛", + "online": "⚡️", + "offline": " " + } + }, + "powerline.segments.common.sys.uptime": { + "before": "⇑ " + }, + "powerline.segments.common.mail.email_imap_alert": { + "before": "✉ " + }, + "powerline.segments.common.env.virtualenv": { + "before": "🐍 " + }, + "powerline.segments.common.wthr.weather": { + "args": { + "icons": { + "tornado": "🌪", + "hurricane": "🌀", + "showers": "☔", + "scattered_showers": "☔", + "thunderstorms": "🌩", + "isolated_thunderstorms": "🌩", + "scattered_thunderstorms": "🌩", + "dust": "🌫", + "fog": "🌫", + "cold": "❄", + "partly_cloudy_day": "🌤", + "mostly_cloudy_day": "🌥", + "sun": "🌣", + "hot": "♨", + "day": "☀", + "blustery": "⚑", + "rainy": "☂", + "cloudy": "☁", + "snowy": "☃", + "stormy": "☈", + "foggy": "🌁", + "sunny": "🌣", + "night": "☾", + "windy": "☴", + "not_available": "�", + "unknown": "⚠" + } + } + }, + "powerline.segments.common.time.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", + "ic": "I·COMP", + "ix": "I·C-X ", + "R": "RPLACE", + "Rv": "V·RPLC", + "Rc": "R·COMP", + "Rx": "R·C-X ", + "c": "COMMND", + "cv": "VIM·EX", + "ce": "NRM·EX", + "r": "PROMPT", + "rm": "-MORE-", + "r?": "CNFIRM", + "!": "!SHELL", + "t": "TERM " + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "↕{rows} ↔{vcols}", + "v_text_oneline": "↔{vcols}", + "v_text_multiline": "↕{rows}", + "V_text": "⇕{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "🔏" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "🖫⃥" + } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } + } + } +} diff --git a/powerline/config_files/themes/shell/__main__.json b/powerline/config_files/themes/shell/__main__.json new file mode 100644 index 0000000..13ae942 --- /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 0000000..9307fc0 --- /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 0000000..38039d8 --- /dev/null +++ b/powerline/config_files/themes/shell/default.json @@ -0,0 +1,43 @@ +{ + "segments": { + "left": [ + { + "function": "powerline.segments.shell.mode" + }, + { + "function": "powerline.segments.common.net.hostname", + "priority": 10 + }, + { + "function": "powerline.segments.common.env.user", + "priority": 30 + }, + { + "function": "powerline.segments.common.env.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": "powerline.segments.common.vcs.stash", + "priority": 50 + }, + { + "function": "powerline.segments.common.vcs.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 0000000..b576273 --- /dev/null +++ b/powerline/config_files/themes/shell/default_leftonly.json @@ -0,0 +1,34 @@ +{ + "segments": { + "left": [ + { + "function": "powerline.segments.common.net.hostname", + "priority": 10 + }, + { + "function": "powerline.segments.common.env.user", + "priority": 30 + }, + { + "function": "powerline.segments.common.env.virtualenv", + "priority": 50 + }, + { + "function": "powerline.segments.common.vcs.branch", + "priority": 40 + }, + { + "function": "powerline.segments.shell.cwd", + "priority": 10 + }, + { + "function": "powerline.segments.shell.jobnum", + "priority": 20 + }, + { + "function": "powerline.segments.shell.last_pipe_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 0000000..3d81408 --- /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_groups": ["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 0000000..4532ced --- /dev/null +++ b/powerline/config_files/themes/tmux/default.json @@ -0,0 +1,28 @@ +{ + "segments": { + "right": [ + { + "function": "powerline.segments.common.sys.uptime", + "priority": 50 + }, + { + "function": "powerline.segments.common.sys.system_load", + "priority": 50 + }, + { + "function": "powerline.segments.common.time.date" + }, + { + "function": "powerline.segments.common.time.date", + "name": "time", + "args": { + "format": "%H:%M", + "istime": true + } + }, + { + "function": "powerline.segments.common.net.hostname" + } + ] + } +} diff --git a/powerline/config_files/themes/unicode.json b/powerline/config_files/themes/unicode.json new file mode 100644 index 0000000..0802852 --- /dev/null +++ b/powerline/config_files/themes/unicode.json @@ -0,0 +1,151 @@ +{ + "dividers": { + "left": { + "hard": "▌ ", + "soft": "│ " + }, + "right": { + "hard": " ▐", + "soft": " │" + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": "⎇ " + }, + "stash": { + "before": "⌆" + }, + "cwd": { + "args": { + "ellipsis": "⋯" + } + }, + "player": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + + "line_current_symbol": { + "contents": "␤ " + }, + + "time": { + "before": "⌚ " + }, + + "powerline.segments.common.net.network_load": { + "args": { + "recv_format": "⬇ {value:>8}", + "sent_format": "⬆ {value:>8}" + } + }, + "powerline.segments.common.net.hostname": { + "before": "⌂ " + }, + "powerline.segments.common.bat.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥", + "online": "⚡︎", + "offline": " " + } + }, + "powerline.segments.common.sys.uptime": { + "before": "⇑ " + }, + "powerline.segments.common.mail.email_imap_alert": { + "before": "✉ " + }, + "powerline.segments.common.env.virtualenv": { + "before": "ⓔ " + }, + "powerline.segments.common.wthr.weather": { + "args": { + "icons": { + "day": "〇", + "blustery": "⚑", + "rainy": "☔", + "cloudy": "☁", + "snowy": "❅", + "stormy": "☈", + "foggy": "≡", + "sunny": "☼", + "night": "☾", + "windy": "☴", + "not_available": "�", + "unknown": "⚠" + } + } + }, + "powerline.segments.common.time.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", + "ic": "I·COMP", + "ix": "I·C-X ", + "R": "RPLACE", + "Rv": "V·RPLC", + "Rc": "R·COMP", + "Rx": "R·C-X ", + "c": "COMMND", + "cv": "VIM·EX", + "ce": "NRM·EX", + "r": "PROMPT", + "rm": "-MORE-", + "r?": "CNFIRM", + "!": "!SHELL", + "t": "TERM " + } + } + }, + "powerline.segments.vim.visual_range": { + "args": { + "CTRL_V_text": "↕{rows} ↔{vcols}", + "v_text_oneline": "↔{vcols}", + "v_text_multiline": "↕{rows}", + "V_text": "⇕{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "⊗" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } + } + } +} diff --git a/powerline/config_files/themes/unicode_terminus.json b/powerline/config_files/themes/unicode_terminus.json new file mode 100644 index 0000000..9c76985 --- /dev/null +++ b/powerline/config_files/themes/unicode_terminus.json @@ -0,0 +1,151 @@ +{ + "dividers": { + "left": { + "hard": "▌ ", + "soft": "│ " + }, + "right": { + "hard": " ▐", + "soft": " │" + } + }, + "spaces": 1, + "segment_data": { + "branch": { + "before": "BR " + }, + "stash": { + "before": "ST " + }, + "cwd": { + "args": { + "ellipsis": "…" + } + }, + + "line_current_symbol": { + "contents": "␤ " + }, + "player": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + + "time": { + "before": "" + }, + + "powerline.segments.common.net.network_load": { + "args": { + "recv_format": "⇓ {value:>8}", + "sent_format": "⇑ {value:>8}" + } + }, + "powerline.segments.common.net.hostname": { + "before": "⌂ " + }, + "powerline.segments.common.bat.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥", + "online": "⚡︎", + "offline": " " + } + }, + "powerline.segments.common.sys.uptime": { + "before": "↑ " + }, + "powerline.segments.common.mail.email_imap_alert": { + "before": "MAIL " + }, + "powerline.segments.common.env.virtualenv": { + "before": "(e) " + }, + "powerline.segments.common.wthr.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.time.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", + "ic": "I·COMP", + "ix": "I·C-X ", + "R": "RPLACE", + "Rv": "V·RPLC", + "Rc": "R·COMP", + "Rx": "R·C-X ", + "c": "COMMND", + "cv": "VIM·EX", + "ce": "NRM·EX", + "r": "PROMPT", + "rm": "-MORE-", + "r?": "CNFIRM", + "!": "!SHELL", + "t": "TERM " + } + } + }, + "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": "+" + } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } + } + } +} diff --git a/powerline/config_files/themes/unicode_terminus_condensed.json b/powerline/config_files/themes/unicode_terminus_condensed.json new file mode 100644 index 0000000..421f5c8 --- /dev/null +++ b/powerline/config_files/themes/unicode_terminus_condensed.json @@ -0,0 +1,151 @@ +{ + "dividers": { + "left": { + "hard": "▌", + "soft": "│" + }, + "right": { + "hard": "▐", + "soft": "│" + } + }, + "spaces": 0, + "segment_data": { + "branch": { + "before": "B " + }, + "stash": { + "before": "S " + }, + "cwd": { + "args": { + "use_path_separator": true, + "ellipsis": "…" + } + }, + + "line_current_symbol": { + "contents": "␤" + }, + "player": { + "args": { + "state_symbols": { + "fallback": "♫", + "play": "▶", + "pause": "▮▮", + "stop": "■" + } + } + }, + + "time": { + "before": "" + }, + + "powerline.segments.common.net.network_load": { + "args": { + "recv_format": "⇓{value:>8}", + "sent_format": "⇑{value:>8}" + } + }, + "powerline.segments.common.net.hostname": { + "before": "⌂" + }, + "powerline.segments.common.bat.battery": { + "args": { + "full_heart": "♥", + "empty_heart": "♥", + "online": "⚡︎", + "offline": " " + } + }, + "powerline.segments.common.sys.uptime": { + "before": "↑" + }, + "powerline.segments.common.mail.email_imap_alert": { + "before": "M " + }, + "powerline.segments.common.env.virtualenv": { + "before": "E " + }, + "powerline.segments.common.wthr.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.time.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", + "ic": "I-C", + "ix": "I^X", + "R": "REP", + "Rv": "VRP", + "Rc": "R-C", + "Rx": "R^X", + "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": "↔{vcols}", + "v_text_multiline": "↕{rows}", + "V_text": "⇕{rows}" + } + }, + "powerline.segments.vim.readonly_indicator": { + "args": { + "text": "RO" + } + }, + "powerline.segments.vim.modified_indicator": { + "args": { + "text": "+" + } + }, + + "powerline.segments.i3wm.scratchpad": { + "args": { + "icons": { + "fresh": "●", + "changed": "○" + } + } + } + } +} diff --git a/powerline/config_files/themes/vim/__main__.json b/powerline/config_files/themes/vim/__main__.json new file mode 100644 index 0000000..7cd3305 --- /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 0000000..e6a05b0 --- /dev/null +++ b/powerline/config_files/themes/vim/cmdwin.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "contents": "Command Line", + "highlight_groups": ["file_name"] + }, + { + "type": "string", + "highlight_groups": ["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 0000000..9b0c744 --- /dev/null +++ b/powerline/config_files/themes/vim/default.json @@ -0,0 +1,128 @@ +{ + "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": "powerline.segments.vim.plugin.capslock.capslock_indicator", + "include_modes": ["i", "R", "Rv"], + "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_groups": ["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" + }, + { + "function": "csv_col_current", + "priority": 30 + }, + { + "type": "string", + "name": "line_current_symbol", + "highlight_groups": ["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 0000000..45c9458 --- /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_groups": ["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_groups": ["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_commandt.json b/powerline/config_files/themes/vim/plugin_commandt.json new file mode 100644 index 0000000..dd6748f --- /dev/null +++ b/powerline/config_files/themes/vim/plugin_commandt.json @@ -0,0 +1,26 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "contents": "Command-T", + "highlight_groups": ["commandt:label"] + }, + { + "function": "powerline.segments.vim.plugin.commandt.finder" + }, + { + "function": "powerline.segments.vim.plugin.commandt.path" + }, + { + "type": "string", + "highlight_groups": ["commandt:background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ], + "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 0000000..ad8432c --- /dev/null +++ b/powerline/config_files/themes/vim/plugin_gundo-preview.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_groups": ["gundo:name", "file_name"], + "contents": "Undo diff" + }, + { + "type": "string", + "highlight_groups": ["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 0000000..a03b256 --- /dev/null +++ b/powerline/config_files/themes/vim/plugin_gundo.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_groups": ["gundo:name", "file_name"], + "contents": "Undo tree" + }, + { + "type": "string", + "highlight_groups": ["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 0000000..896d393 --- /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_groups": ["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 0000000..ae4d5a5 --- /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_groups": ["file_name"] + }, + { + "function": "window_title", + "draw_soft_divider": false + }, + { + "type": "string", + "highlight_groups": ["background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "type": "string", + "name": "line_current_symbol", + "highlight_groups": ["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 0000000..1e3130e --- /dev/null +++ b/powerline/config_files/themes/vim/tabline.json @@ -0,0 +1,93 @@ +{ + "default_module": "powerline.segments.vim", + "segments": { + "left": [ + { + "type": "segment_list", + "function": "powerline.listers.vim.tablister", + "exclude_function": "single_tab", + "segments": [ + { + "function": "tab" + }, + { + "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 + } + ] + }, + { + "function": "tab", + "args": { + "end": true + } + }, + { + "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_groups": ["tab:background"], + "draw_soft_divider": false, + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "type": "string", + "contents": "Bufs", + "name": "single_tab", + "highlight_groups": ["single_tab"], + "include_function": "single_tab" + }, + { + "type": "string", + "contents": "Tabs", + "name": "many_tabs", + "highlight_groups": ["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 0000000..3d468b4 --- /dev/null +++ b/powerline/config_files/themes/wm/default.json @@ -0,0 +1,17 @@ +{ + "segments": { + "right": [ + { + "function": "powerline.segments.common.time.date" + }, + { + "function": "powerline.segments.common.time.date", + "name": "time", + "args": { + "format": "%H:%M", + "istime": true + } + } + ] + } +} diff --git a/powerline/dist/systemd/powerline-daemon.service b/powerline/dist/systemd/powerline-daemon.service new file mode 100644 index 0000000..96b685d --- /dev/null +++ b/powerline/dist/systemd/powerline-daemon.service @@ -0,0 +1,10 @@ +[Unit] +Description=powerline-daemon - Daemon that improves powerline performance +Documentation=man:powerline-daemon(1) +Documentation=https://powerline.readthedocs.org/en/latest/ + +[Service] +ExecStart=/usr/bin/powerline-daemon --foreground + +[Install] +WantedBy=default.target diff --git a/powerline/ipython.py b/powerline/ipython.py new file mode 100644 index 0000000..cb84fc7 --- /dev/null +++ b/powerline/ipython.py @@ -0,0 +1,68 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline import Powerline +from powerline.lib.dict import mergedicts +from powerline.lib.unicode import string + + +class IPythonInfo(object): + def __init__(self, shell): + self._shell = shell + + @property + def prompt_count(self): + return self._shell.execution_count + + +# 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, **kwargs): + super(IPythonPowerline, self).init( + 'ipython', + use_daemon_threads=True, + **kwargs + ) + + def get_config_paths(self): + if self.config_paths: + return self.config_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/lemonbar.py b/powerline/lemonbar.py new file mode 100644 index 0000000..b49f86b --- /dev/null +++ b/powerline/lemonbar.py @@ -0,0 +1,21 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline import Powerline +from powerline.lib.dict import mergedicts + + +class LemonbarPowerline(Powerline): + def init(self): + super(LemonbarPowerline, self).init(ext='wm', renderer_module='lemonbar') + + get_encoding = staticmethod(lambda: 'utf-8') + + def get_local_themes(self, local_themes): + if not local_themes: + return {} + + return dict(( + (key, {'config': self.load_theme_config(val)}) + for key, val in local_themes.items() + )) diff --git a/powerline/lib/__init__.py b/powerline/lib/__init__.py new file mode 100644 index 0000000..2a5fbd0 --- /dev/null +++ b/powerline/lib/__init__.py @@ -0,0 +1,28 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from functools import wraps + + +def wraps_saveargs(wrapped): + def dec(wrapper): + r = wraps(wrapped)(wrapper) + r.powerline_origin = getattr(wrapped, 'powerline_origin', wrapped) + return r + return dec + + +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 diff --git a/powerline/lib/config.py b/powerline/lib/config.py new file mode 100644 index 0000000..0c95e47 --- /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 0000000..515e8c4 --- /dev/null +++ b/powerline/lib/debug.py @@ -0,0 +1,97 @@ +# 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/dict.py b/powerline/lib/dict.py new file mode 100644 index 0000000..c06ab30 --- /dev/null +++ b/powerline/lib/dict.py @@ -0,0 +1,88 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + + +REMOVE_THIS_KEY = object() + + +def mergeargs(argvalue, remove=False): + if not argvalue: + return None + r = {} + for subval in argvalue: + mergedicts(r, dict([subval]), remove=remove) + return r + + +def _clear_special_values(d): + '''Remove REMOVE_THIS_KEY values from dictionary + ''' + l = [d] + while l: + i = l.pop() + pops = [] + for k, v in i.items(): + if v is REMOVE_THIS_KEY: + pops.append(k) + elif isinstance(v, dict): + l.append(v) + for k in pops: + i.pop(k) + + +def mergedicts(d1, d2, remove=True): + '''Recursively merge two dictionaries + + First dictionary is modified in-place. + ''' + _setmerged(d1, d2) + for k in d2: + if k in d1 and isinstance(d1[k], dict) and isinstance(d2[k], dict): + mergedicts(d1[k], d2[k], remove) + elif remove and d2[k] is REMOVE_THIS_KEY: + d1.pop(k, None) + else: + if remove and isinstance(d2[k], dict): + _clear_special_values(d2[k]) + 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 _setmerged(d1, d2): + if hasattr(d1, 'setmerged'): + d1.setmerged(d2) + + +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() + _setmerged(ret, d2) + 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 updated(d, *args, **kwargs): + '''Copy dictionary and update it with provided arguments + ''' + d = d.copy() + d.update(*args, **kwargs) + return d diff --git a/powerline/lib/encoding.py b/powerline/lib/encoding.py new file mode 100644 index 0000000..76a51d8 --- /dev/null +++ b/powerline/lib/encoding.py @@ -0,0 +1,125 @@ +# vim:fileencoding=utf-8:noet + +'''Encodings support + +This is the only module from which functions obtaining encoding should be +exported. Note: you should always care about errors= argument since it is not +guaranteed that encoding returned by some function can encode/decode given +string. + +All functions in this module must always return a valid encoding. Most of them +are not thread-safe. +''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import locale + + +def get_preferred_file_name_encoding(): + '''Get preferred file name encoding + ''' + return ( + sys.getfilesystemencoding() + or locale.getpreferredencoding() + or 'utf-8' + ) + + +def get_preferred_file_contents_encoding(): + '''Get encoding preferred for file contents + ''' + return ( + locale.getpreferredencoding() + or 'utf-8' + ) + + +def get_preferred_output_encoding(): + '''Get encoding that should be used for printing strings + + .. warning:: + Falls back to ASCII, so that output is most likely to be displayed + correctly. + ''' + if hasattr(locale, 'LC_MESSAGES'): + return ( + locale.getlocale(locale.LC_MESSAGES)[1] + or locale.getdefaultlocale()[1] + or 'ascii' + ) + + return ( + locale.getdefaultlocale()[1] + or 'ascii' + ) + + +def get_preferred_input_encoding(): + '''Get encoding that should be used for reading shell command output + + .. warning:: + Falls back to latin1 so that function is less likely to throw as decoded + output is primary searched for ASCII values. + ''' + if hasattr(locale, 'LC_MESSAGES'): + return ( + locale.getlocale(locale.LC_MESSAGES)[1] + or locale.getdefaultlocale()[1] + or 'latin1' + ) + + return ( + locale.getdefaultlocale()[1] + or 'latin1' + ) + + +def get_preferred_arguments_encoding(): + '''Get encoding that should be used for command-line arguments + + .. warning:: + Falls back to latin1 so that function is less likely to throw as + non-ASCII command-line arguments most likely contain non-ASCII + filenames and screwing them up due to unidentified locale is not much of + a problem. + ''' + return ( + locale.getdefaultlocale()[1] + or 'latin1' + ) + + +def get_preferred_environment_encoding(): + '''Get encoding that should be used for decoding environment variables + ''' + return ( + locale.getpreferredencoding() + or 'utf-8' + ) + + +def get_unicode_writer(stream=sys.stdout, encoding=None, errors='replace'): + '''Get function which will write unicode string to the given stream + + Writing is done using encoding returned by + :py:func:`get_preferred_output_encoding`. + + :param file stream: + Stream to write to. Default value is :py:attr:`sys.stdout`. + :param str encoding: + Determines which encoding to use. If this argument is specified then + :py:func:`get_preferred_output_encoding` is not used. + :param str errors: + Determines what to do with characters which cannot be encoded. See + ``errors`` argument of :py:func:`codecs.encode`. + + :return: Callable which writes unicode string to the given stream using + the preferred output encoding. + ''' + encoding = encoding or get_preferred_output_encoding() + if sys.version_info < (3,) or not hasattr(stream, 'buffer'): + return lambda s: stream.write(s.encode(encoding, errors)) + else: + return lambda s: stream.buffer.write(s.encode(encoding, errors)) diff --git a/powerline/lib/humanize_bytes.py b/powerline/lib/humanize_bytes.py new file mode 100644 index 0000000..c98a117 --- /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 0000000..8b74a7f --- /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 + +from powerline.lib.encoding import get_preferred_file_name_encoding + + +__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 = get_preferred_file_name_encoding() + self.hdr = struct.Struct(b'iIII') + # 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/fs/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') + 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 0000000..cedbe45 --- /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 0000000..cd7c414 --- /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/overrides.py b/powerline/lib/overrides.py new file mode 100644 index 0000000..3257d98 --- /dev/null +++ b/powerline/lib/overrides.py @@ -0,0 +1,80 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import json + +from powerline.lib.dict import REMOVE_THIS_KEY + + +def parse_value(s): + '''Convert string to Python object + + Rules: + + * Empty string means that corresponding key should be removed from the + dictionary. + * Strings that start with a minus, digit or with some character that starts + JSON collection or string object are parsed as JSON. + * JSON special values ``null``, ``true``, ``false`` (case matters) are + parsed as JSON. + * All other values are considered to be raw strings. + + :param str s: Parsed string. + + :return: Python object. + ''' + if not s: + return REMOVE_THIS_KEY + elif s[0] in '"{[0123456789-' or s in ('null', 'true', 'false'): + return json.loads(s) + else: + return s + + +def keyvaluesplit(s): + '''Split K1.K2=VAL into K1.K2 and parsed VAL + ''' + 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] + val = parse_value(s[idx + 1:]) + return (o, val) + + +def parsedotval(s): + '''Parse K1.K2=VAL into {"K1":{"K2":VAL}} + + ``VAL`` is processed according to rules defined in :py:func:`parse_value`. + ''' + if type(s) is tuple: + o, val = s + val = parse_value(val) + 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) + + +def parse_override_var(s): + '''Parse a semicolon-separated list of strings into a sequence of values + + Emits the same items in sequence as :py:func:`parsedotval` does. + ''' + return ( + parsedotval(item) + for item in s.split(';') + if item + ) diff --git a/powerline/lib/path.py b/powerline/lib/path.py new file mode 100644 index 0000000..49ff433 --- /dev/null +++ b/powerline/lib/path.py @@ -0,0 +1,18 @@ +# 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)) + + +def join(*components): + if any((isinstance(p, bytes) for p in components)): + return os.path.join(*[ + p if isinstance(p, bytes) else p.encode('ascii') + for p in components + ]) + else: + return os.path.join(*components) diff --git a/powerline/lib/shell.py b/powerline/lib/shell.py new file mode 100644 index 0000000..2082e82 --- /dev/null +++ b/powerline/lib/shell.py @@ -0,0 +1,133 @@ +# 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 functools import partial + +from powerline.lib.encoding import get_preferred_input_encoding, get_preferred_output_encoding + + +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 run_cmd(pl, cmd, stdin=None, strip=True): + '''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. + :param bool strip: + True if the result should be stripped. + ''' + 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 if stdin is None else stdin.encode(get_preferred_output_encoding())) + stdout = stdout.decode(get_preferred_input_encoding()) + return stdout.strip() if strip else stdout + + +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_preferred_input_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 0000000..e5a6b3e --- /dev/null +++ b/powerline/lib/threaded.py @@ -0,0 +1,262 @@ +# 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),) + + _omitted_args = { + 'render': (0,), + 'set_state': ('shutdown_event',), + } + + def omitted_args(self, name, method): + ret = self._omitted_args.get(name, ()) + if isinstance(getattr(self, name, None), MethodType): + ret = tuple((i + 1 if isinstance(i, int) else i for i in ret)) + return ret + + +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 + + _omitted_args = { + 'render': ('update_value', 'key', 'after_update'), + 'set_state': ('shutdown_event',), + 'render_one': (0,), + } diff --git a/powerline/lib/unicode.py b/powerline/lib/unicode.py new file mode 100644 index 0000000..eeae387 --- /dev/null +++ b/powerline/lib/unicode.py @@ -0,0 +1,283 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import codecs + +from unicodedata import east_asian_width, combining + +from powerline.lib.encoding import get_preferred_output_encoding + + +try: + from __builtin__ import unicode +except ImportError: + unicode = str + + +try: + from __builtin__ import unichr +except ImportError: + unichr = chr + + +if sys.maxunicode < 0x10FFFF: + _unichr = unichr + + def unichr(ch): + if ch <= sys.maxunicode: + return _unichr(ch) + else: + ch -= 0x10000 + return _unichr((ch >> 10) + 0xD800) + _unichr((ch & ((1 << 10) - 1)) + 0xDC00) + + +def u(s): + '''Return unicode instance assuming UTF-8 encoded string. + ''' + if type(s) is unicode: + return s + else: + return unicode(s, 'utf-8') + + +if sys.version_info < (3,): + def tointiter(s): + '''Convert a byte string to the sequence of integers + ''' + return (ord(c) for c in s) +else: + def tointiter(s): + '''Convert a byte string to the sequence of integers + ''' + return iter(s) + + +def powerline_decode_error(e): + if not isinstance(e, UnicodeDecodeError): + raise NotImplementedError + return (''.join(( + '<{0:02X}>'.format(c) + for c in tointiter(e.object[e.start:e.end]) + )), e.end) + + +codecs.register_error('powerline_decode_error', powerline_decode_error) + + +last_swe_idx = 0 + + +def register_strwidth_error(strwidth): + '''Create new encode errors handling method similar to ``replace`` + + Like ``replace`` this method uses question marks in place of the characters + that cannot be represented in the requested encoding. Unlike ``replace`` the + amount of question marks is identical to the amount of display cells + offending character occupies. Thus encoding ``…`` (U+2026, HORIZONTAL + ELLIPSIS) to ``latin1`` will emit one question mark, but encoding ``A`` + (U+FF21, FULLWIDTH LATIN CAPITAL LETTER A) will emit two question marks. + + Since width of some characters depends on the terminal settings and + powerline knows how to respect them a single error handling method cannot be + used. Instead of it the generator function is used which takes ``strwidth`` + function (function that knows how to compute string width respecting all + needed settings) and emits new error handling method name. + + :param function strwidth: + Function that computs string width measured in display cells the string + occupies when displayed. + + :return: New error handling method name. + ''' + global last_swe_idx + last_swe_idx += 1 + + def powerline_encode_strwidth_error(e): + if not isinstance(e, UnicodeEncodeError): + raise NotImplementedError + return ('?' * strwidth(e.object[e.start:e.end]), e.end) + + ename = 'powerline_encode_strwidth_error_{0}'.format(last_swe_idx) + codecs.register_error(ename, powerline_encode_strwidth_error) + return ename + + +def out_u(s): + '''Return unicode string suitable for displaying + + Unlike other functions assumes get_preferred_output_encoding() first. Unlike + u() does not throw exceptions for invalid unicode strings. Unlike + safe_unicode() does throw an exception if object is not a string. + ''' + if isinstance(s, unicode): + return s + elif isinstance(s, bytes): + return unicode(s, get_preferred_output_encoding(), 'powerline_decode_error') + else: + raise TypeError('Expected unicode or bytes instance, got {0}'.format(repr(type(s)))) + + +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 powerline.lib.encoding.get_preferred_output_encoding() encoding + * If everything failed use safe_unicode on last exception with which + everything failed + ''' + try: + try: + if type(s) is bytes: + return unicode(s, 'ascii') + else: + return unicode(s) + except UnicodeDecodeError: + try: + return unicode(s, 'utf-8') + except TypeError: + return unicode(str(s), 'utf-8') + except UnicodeDecodeError: + return unicode(s, get_preferred_output_encoding()) + except Exception as e: + return safe_unicode(e) + + +class FailedUnicode(unicode): + '''Builtin ``unicode`` 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 + + +if sys.version_info < (3,): + def string(s): + if type(s) is not str: + return s.encode('utf-8') + else: + return s +else: + def string(s): + if type(s) is not str: + return s.decode('utf-8') + else: + return s + + +string.__doc__ = ( + '''Transform ``unicode`` or ``bytes`` object into ``str`` object + + On Python-2 this encodes ``unicode`` to ``bytes`` (which is ``str``) using + UTF-8 encoding; on Python-3 this decodes ``bytes`` to ``unicode`` (which is + ``str``) using UTF-8 encoding. + + Useful for functions that expect an ``str`` object in both unicode versions, + not caring about the semantic differences between them in Python-2 and + Python-3. + ''' +) + + +def surrogate_pair_to_character(high, low): + '''Transform a pair of surrogate codepoints to one codepoint + ''' + return 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00) + + +_strwidth_documentation = ( + '''Compute string width in display cells + + {0} + + :param dict width_data: + Dictionary which maps east_asian_width property values to strings + lengths. It is expected to contain the following keys and values (from + `East Asian Width annex `_): + + === ====== =========================================================== + Key Value Description + === ====== =========================================================== + F 2 Fullwidth: all characters that are defined as Fullwidth in + the Unicode Standard [Unicode] by having a compatibility + decomposition of type to characters elsewhere in the + Unicode Standard that are implicitly narrow but unmarked. + H 1 Halfwidth: all characters that are explicitly defined as + Halfwidth in the Unicode Standard by having a compatibility + decomposition of type to characters elsewhere in + the Unicode Standard that are implicitly wide but unmarked, + plus U+20A9 ₩ WON SIGN. + W 2 Wide: all other characters that are always wide. These + characters occur only in the context of East Asian + typography where they are wide characters (such as the + Unified Han Ideographs or Squared Katakana Symbols). This + category includes characters that have explicit halfwidth + counterparts. + Na 1 Narrow: characters that are always narrow and have explicit + fullwidth or wide counterparts. These characters are + implicitly narrow in East Asian typography and legacy + character sets because they have explicit fullwidth or wide + counterparts. All of ASCII is an example of East Asian + Narrow characters. + A 1 or 2 Ambiguous: characters that may sometimes be wide and + sometimes narrow. Ambiguous characters require additional + information not contained in the character code to further + resolve their width. This information is usually defined in + terminal setting that should in turn respect glyphs widths + in used fonts. Also see :ref:`ambiwidth configuration + option `. + N 1 Neutral characters: character that does not occur in legacy + East Asian character sets. + === ====== =========================================================== + + :param unicode string: + String whose width will be calculated. + + :return: unsigned integer.''') + + +def strwidth_ucs_4(width_data, string): + return sum((( + ( + 0 + ) if combining(symbol) else ( + width_data[east_asian_width(symbol)] + ) + ) for symbol in string)) + + +strwidth_ucs_4.__doc__ = _strwidth_documentation.format( + '''This version of function expects that characters above 0xFFFF are + represented using one symbol. This is only the case in UCS-4 Python builds. + + .. note: + Even in UCS-4 Python builds it is possible to represent characters above + 0xFFFF using surrogate pairs. Characters represented this way are not + supported.''') + + +def strwidth_ucs_2(width_data, string): + return sum((( + ( + width_data[east_asian_width(string[i - 1] + symbol)] + ) if 0xDC00 <= ord(symbol) <= 0xDFFF else ( + 0 + ) if combining(symbol) or 0xD800 <= ord(symbol) <= 0xDBFF else ( + width_data[east_asian_width(symbol)] + ) + ) for i, symbol in enumerate(string))) + + +strwidth_ucs_2.__doc__ = _strwidth_documentation.format( + '''This version of function expects that characters above 0xFFFF are + represented using two symbols forming a surrogate pair, which is the only + option in UCS-2 Python builds. It still works correctly in UCS-4 Python + builds, but is slower then its UCS-4 counterpart.''') diff --git a/powerline/lib/url.py b/powerline/lib/url.py new file mode 100644 index 0000000..f25919c --- /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 0000000..f862c6b --- /dev/null +++ b/powerline/lib/vcs/__init__.py @@ -0,0 +1,276 @@ +# 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 +from powerline.lib.unicode import out_u +from powerline.lib.path import join + + +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] = out_u(get_func(directory, config_file)) + if changed: + # Config file has changed or was not tracked + branch_name_cache[config_file] = out_u(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(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 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-existent 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), +) + + +vcs_props_bytes = [ + (vcs, vcs_dir.encode('ascii'), check) + for vcs, vcs_dir, check in vcs_props +] + + +def guess(path, create_watcher): + for directory in generate_directories(path): + for vcs, vcs_dir, check in (vcs_props_bytes if isinstance(path, bytes) else 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 0000000..e47d8b2 --- /dev/null +++ b/powerline/lib/vcs/bzr.py @@ -0,0 +1,108 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +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 +from powerline.lib.path import join +from powerline.lib.encoding import get_preferred_file_contents_encoding + + +class CoerceIO(StringIO): + def write(self, arg): + if isinstance(arg, bytes): + arg = arg.decode(get_preferred_file_contents_encoding(), '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(get_preferred_file_contents_encoding(), 'replace') + break + except Exception: + pass + return ans or os.path.basename(directory) + + +state = None + + +class Repository(object): + 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: + + :'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=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 = 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 0000000..bebc311 --- /dev/null +++ b/powerline/lib/vcs/git.py @@ -0,0 +1,208 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re + +from powerline.lib.vcs import get_branch_name, get_file_status +from powerline.lib.shell import readlines +from powerline.lib.path import join +from powerline.lib.encoding import (get_preferred_file_name_encoding, + get_preferred_file_contents_encoding) +from powerline.lib.shell import which + + +_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(get_preferred_file_contents_encoding(), 'replace') + return raw[:7] + + +def git_directory(directory): + 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:] + if raw[-1:] == b'\n': + raw = raw[:-1] + if not isinstance(path, bytes): + raw = raw.decode(get_preferred_file_name_encoding()) + 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=join(gitd, 'index'), + file_path=path, + ignore_file_name='.gitignore', + get_func=self.do_status, + create_watcher=self.create_watcher, + extra_ignore_files=tuple(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 = 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 stash(self): + try: + stashref = git.Repository(git_directory(self.directory)).lookup_reference('refs/stash') + except KeyError: + return 0 + return sum(1 for _ in stashref.log()) + + 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): + def __init__(self, *args, **kwargs): + if not which('git'): + raise OSError('git executable is not available') + super(Repository, self).__init__(*args, **kwargs) + + @staticmethod + def ignore_event(path, name): + # Ignore changes to the index.lock file, since they happen + # frequently and don't 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 stash(self): + return sum(1 for _ in self._gitcmd(self.directory, '--no-optional-locks', 'stash', 'list')) + + def do_status(self, directory, path): + if path: + try: + return next(self._gitcmd(directory, '--no-optional-locks', 'status', '--porcelain', '--ignored', '--', path))[:2] + except StopIteration: + return None + else: + wt_column = ' ' + index_column = ' ' + untracked_column = ' ' + for line in self._gitcmd(directory, '--no-optional-locks', '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 0000000..09b6e0b --- /dev/null +++ b/powerline/lib/vcs/mercurial.py @@ -0,0 +1,88 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +import hglib + +from powerline.lib.vcs import get_branch_name, get_file_status +from powerline.lib.path import join +from powerline.lib.encoding import get_preferred_file_contents_encoding + + +def branch_name_from_config_file(directory, config_file): + try: + with open(config_file, 'rb') as f: + raw = f.read() + return raw.decode(get_preferred_file_contents_encoding(), 'replace').strip() + except Exception: + return 'default' + + +class Repository(object): + __slots__ = ('directory', 'create_watcher') + + # hg status -> (powerline file status, repo status flag) + statuses = { + b'M': ('M', 1), b'A': ('A', 1), b'R': ('R', 1), b'!': ('D', 1), + b'?': ('U', 2), b'I': ('I', 0), b'C': ('', 0), + } + repo_statuses_str = (None, 'D ', ' U', 'DU') + + def __init__(self, directory, create_watcher): + self.directory = os.path.abspath(directory) + 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 hglib.open(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=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): + with self._repo(directory) as repo: + if path: + path = os.path.join(directory, path) + statuses = repo.status(include=path, all=True) + for status, paths in statuses: + if paths: + return self.statuses[status][0] + return None + else: + resulting_status = 0 + for status, paths in repo.status(all=True): + if paths: + resulting_status |= self.statuses[status][1] + return self.repo_statuses_str[resulting_status] + + def branch(self): + config_file = 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 0000000..4fe9896 --- /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 0000000..c4f1200 --- /dev/null +++ b/powerline/lib/watcher/inotify.py @@ -0,0 +1,268 @@ +# 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 don't + # 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 don't 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 don't 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 don't + # 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: + if not isinstance(path, bytes): + name = name.decode(self.fenc) + 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 0000000..0c08971 --- /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 0000000..7d2b83f --- /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 0000000..272db0f --- /dev/null +++ b/powerline/lib/watcher/uv.py @@ -0,0 +1,207 @@ +# 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 errno import ENOENT + +from powerline.lib.path import realpath +from powerline.lib.encoding import get_preferred_file_name_encoding + + +class UvNotFound(NotImplementedError): + pass + + +pyuv = None +pyuv_version_info = None + + +def import_pyuv(): + global pyuv + global pyuv_version_info + if not pyuv: + try: + pyuv = __import__('pyuv') + except ImportError: + raise UvNotFound + else: + pyuv_version_info = tuple((int(c) for c in pyuv.__version__.split('.'))) + + +class UvThread(Thread): + daemon = True + + def __init__(self, loop): + self.uv_loop = loop + self.async_handle = pyuv.Async(loop, self._async_cb) + super(UvThread, self).__init__() + + def _async_cb(self, handle): + self.uv_loop.stop() + self.async_handle.close() + + def run(self): + self.uv_loop.run() + + def join(self): + self.async_handle.send() + 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 + + +def normpath(path, fenc): + path = realpath(path) + if isinstance(path, bytes): + return path.decode(fenc) + else: + return path + + +class UvWatcher(object): + def __init__(self): + import_pyuv() + self.watches = {} + self.lock = RLock() + self.loop = start_uv_thread() + self.fenc = get_preferred_file_name_encoding() + if pyuv_version_info >= (1, 0): + self._start_watch = self._start_watch_1_x + else: + self._start_watch = self._start_watch_0_x + + def _start_watch_1_x(self, path): + handle = pyuv.fs.FSEvent(self.loop) + handle.start(path, 0, partial(self._record_event, path)) + self.watches[path] = handle + + def _start_watch_0_x(self, path): + self.watches[path] = pyuv.fs.FSEvent( + self.loop, + path, + partial(self._record_event, path), + pyuv.fs.UV_CHANGE | pyuv.fs.UV_RENAME + ) + + def watch(self, path): + path = normpath(path, self.fenc) + with self.lock: + if path not in self.watches: + try: + self._start_watch(path) + except pyuv.error.FSEventError as e: + code = e.args[0] + if code == pyuv.errno.UV_ENOENT: + raise OSError(ENOENT, 'No such file or directory: ' + path) + else: + raise + + def unwatch(self, path): + path = normpath(path, self.fenc) + 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 normpath(path, self.fenc) 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 = normpath(path, self.fenc) + 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 = normpath(basedir, self.fenc) + self.modified = True + self.watch_directory(self.basedir) + + def watch_directory(self, path): + for root, dirs, files in os.walk(normpath(path, self.fenc)): + self.watch_one_directory(root) + + def watch_one_directory(self, dirname): + 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 0000000..8c68271 --- /dev/null +++ b/powerline/lint/__init__.py @@ -0,0 +1,625 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import logging + +from collections import defaultdict +from itertools import chain +from functools import partial + +from powerline import generate_config_finder, get_config_paths, load_config +from powerline.segments.vim import vim_modes +from powerline.lib.dict import mergedicts_copy +from powerline.lib.config import ConfigLoader +from powerline.lib.unicode import unicode +from powerline.lib.path import join +from powerline.lint.markedjson import load +from powerline.lint.markedjson.error import echoerr, EchoErr, MarkedError +from powerline.lint.checks import (check_matcher_func, check_ext, check_config, check_top_theme, + check_color, check_translated_group_name, check_group, + check_segment_module, check_exinclude_function, type_keys, + check_segment_function, check_args, get_one_segment_function, + check_highlight_groups, check_highlight_group, check_full_segment_data, + get_all_possible_functions, check_segment_data_key, register_common_name, + highlight_group_spec, check_log_file_level, check_logging_handler) +from powerline.lint.spec import Spec +from powerline.lint.context import Context + + +def open_file(path): + return open(path, 'rb') + + +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 + + +function_name_re = '^(\w+\.)*[a-zA-Z_]\w*$' + + +divider_spec = Spec().printable().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)))) +log_level_spec = Spec().re('^[A-Z]+$').func( + (lambda value, *args: (True, True, not hasattr(logging, value))), + (lambda value: 'unknown debugging level {0}'.format(value)) +).copy +log_format_spec = Spec().type(unicode).copy +main_spec = (Spec( + common=Spec( + default_top_theme=top_theme_spec().optional(), + term_truecolor=Spec().type(bool).optional(), + term_escape_style=Spec().type(unicode).oneof(set(('auto', 'xterm', 'fbterm'))).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().either( + Spec().type(unicode).func( + ( + lambda value, *args: ( + True, + True, + not os.path.isdir(os.path.dirname(os.path.expanduser(value))) + ) + ), + (lambda value: 'directory does not exist: {0}'.format(os.path.dirname(value))) + ), + Spec().list(Spec().either( + Spec().type(unicode, type(None)), + Spec().tuple( + Spec().re(function_name_re).func(check_logging_handler), + Spec().tuple( + Spec().type(list).optional(), + Spec().type(dict).optional(), + ), + log_level_spec().func(check_log_file_level).optional(), + log_format_spec().optional(), + ), + )) + ).optional(), + log_level=log_level_spec().optional(), + log_format=log_format_spec().optional(), + interval=Spec().either(Spec().cmp('gt', 0.0), Spec().type(type(None))).optional(), + reload_config=Spec().type(bool).optional(), + watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(), + ).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(), + wm=ext_spec().update( + local_themes=Spec().unknown_spec( + Spec().re('^[0-9A-Za-z-]+$'), + ext_theme_spec() + ).optional(), + update_interval=Spec().cmp('gt', 0.0).optional(), + ).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')) + + +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(), + attrs=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')) + + +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 +segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy +exinclude_spec = Spec().re(function_name_re).func(check_exinclude_function).copy +segment_spec_base = Spec( + 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().printable().optional(), + before=Spec().printable().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().printable().optional(), + highlight_groups=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(), +).func(check_full_segment_data).copy +subsegment_spec = segment_spec_base().update( + type=Spec().oneof(set((key for key in type_keys if key != 'segment_list'))).optional(), +) +segment_spec = segment_spec_base().update( + type=Spec().oneof(type_keys).optional(), + segments=Spec().optional().list(subsegment_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().printable().optional(), + before=Spec().printable().optional(), + display=Spec().type(bool).optional(), + args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_functions, *args, **kwargs)), + contents=Spec().printable().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 register_common_names(): + register_common_name('player', 'powerline.segments.common.players', '_player') + + +def load_json_file(path): + with open_file(path) as F: + try: + config, hadproblem = load(F) + except MarkedError as e: + return True, None, str(e) + else: + return hadproblem, config, None + + +def updated_with_config(d): + hadproblem, config, error = load_json_file(d['path']) + d.update( + hadproblem=hadproblem, + config=config, + error=error, + ) + return d + + +def find_all_ext_config_files(search_paths, subdir): + for config_root in search_paths: + top_config_subpath = join(config_root, subdir) + if not os.path.isdir(top_config_subpath): + if os.path.exists(top_config_subpath): + yield { + 'error': 'Path {0} is not a directory'.format(top_config_subpath), + 'path': top_config_subpath, + } + continue + for ext_name in os.listdir(top_config_subpath): + ext_path = os.path.join(top_config_subpath, ext_name) + if not os.path.isdir(ext_path): + if ext_name.endswith('.json') and os.path.isfile(ext_path): + yield updated_with_config({ + 'error': False, + 'path': ext_path, + 'name': ext_name[:-5], + 'ext': None, + 'type': 'top_' + subdir, + }) + else: + yield { + 'error': 'Path {0} is not a directory or configuration file'.format(ext_path), + 'path': ext_path, + } + continue + for config_file_name in os.listdir(ext_path): + config_file_path = os.path.join(ext_path, config_file_name) + if config_file_name.endswith('.json') and os.path.isfile(config_file_path): + yield updated_with_config({ + 'error': False, + 'path': config_file_path, + 'name': config_file_name[:-5], + 'ext': ext_name, + 'type': subdir, + }) + else: + yield { + 'error': 'Path {0} is not a configuration file'.format(config_file_path), + 'path': config_file_path, + } + + +def dict2(d): + return defaultdict(dict, ((k, dict(v)) for k, v in d.items())) + + +def check(paths=None, debug=False, echoerr=echoerr, require_ext=None): + '''Check configuration sanity + + :param list paths: + Paths from which configuration should be loaded. + :param bool debug: + Determines whether some information useful for debugging linter should + be output. + :param function echoerr: + Function that will be used to echo the error(s). Should accept four + optional keyword parameters: ``problem`` and ``problem_mark``, and + ``context`` and ``context_mark``. + :param str require_ext: + Require configuration for some extension to be present. + + :return: + ``False`` if user configuration seems to be completely sane and ``True`` + if some problems were found. + ''' + hadproblem = False + + register_common_names() + 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) + + if require_ext: + used_main_spec = main_spec.copy() + try: + used_main_spec['ext'][require_ext].required() + except KeyError: + used_main_spec['ext'][require_ext] = ext_spec() + else: + used_main_spec = main_spec + + lhadproblem = [False] + load_json_config = generate_json_config_loader(lhadproblem) + + config_loader = ConfigLoader(run_once=True, load=load_json_config) + + lists = { + 'colorschemes': set(), + 'themes': set(), + 'exts': set(), + } + found_dir = { + 'themes': False, + 'colorschemes': False, + } + config_paths = defaultdict(lambda: defaultdict(dict)) + loaded_configs = defaultdict(lambda: defaultdict(dict)) + for d in chain( + find_all_ext_config_files(search_paths, 'colorschemes'), + find_all_ext_config_files(search_paths, 'themes'), + ): + if d['error']: + hadproblem = True + ee(problem=d['error']) + continue + if d['hadproblem']: + hadproblem = True + if d['ext']: + found_dir[d['type']] = True + lists['exts'].add(d['ext']) + if d['name'] == '__main__': + pass + elif d['name'].startswith('__') or d['name'].endswith('__'): + hadproblem = True + ee(problem='File name is not supposed to start or end with “__”: {0}'.format( + d['path'])) + else: + lists[d['type']].add(d['name']) + config_paths[d['type']][d['ext']][d['name']] = d['path'] + loaded_configs[d['type']][d['ext']][d['name']] = d['config'] + else: + config_paths[d['type']][d['name']] = d['path'] + loaded_configs[d['type']][d['name']] = d['config'] + + for typ in ('themes', 'colorschemes'): + if not found_dir[typ]: + hadproblem = True + ee(problem='Subdirectory {0} was not found in paths {1}'.format(typ, ', '.join(search_paths))) + + diff = set(config_paths['colorschemes']) - set(config_paths['themes']) + if diff: + hadproblem = True + for ext in diff: + typ = 'colorschemes' if ext in config_paths['themes'] else 'themes' + if not config_paths['top_' + typ] or typ == 'themes': + ee(problem='{0} extension {1} not present in {2}'.format( + ext, + 'configuration' if ( + ext in loaded_configs['themes'] and ext in loaded_configs['colorschemes'] + ) else 'directory', + typ, + )) + + try: + main_config = load_config('config', find_config_files, config_loader) + except IOError: + main_config = {} + ee(problem='Configuration file not found: config.json') + hadproblem = True + except MarkedError as e: + main_config = {} + ee(problem=str(e)) + hadproblem = True + else: + if used_main_spec.match( + main_config, + data={'configs': config_paths, 'lists': lists}, + context=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 = {} + ee(problem='Configuration file not found: colors.json') + hadproblem = True + except MarkedError as e: + colors_config = {} + ee(problem=str(e)) + hadproblem = True + else: + if colors_spec.match(colors_config, context=Context(colors_config), echoerr=ee)[1]: + hadproblem = True + + if lhadproblem[0]: + hadproblem = True + + top_colorscheme_configs = dict(loaded_configs['top_colorschemes']) + data = { + 'ext': None, + 'top_colorscheme_configs': top_colorscheme_configs, + 'ext_colorscheme_configs': {}, + 'colors_config': colors_config + } + for colorscheme, config in loaded_configs['top_colorschemes'].items(): + data['colorscheme'] = colorscheme + if top_colorscheme_spec.match(config, context=Context(config), data=data, echoerr=ee)[1]: + hadproblem = True + + ext_colorscheme_configs = dict2(loaded_configs['colorschemes']) + 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=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, + ) + if not (mconfigs[0] or mconfigs[2]): + continue + config = None + for mconfig in mconfigs: + if not mconfig: + continue + if config: + config = mergedicts_copy(config, mconfig) + else: + config = mconfig + colorscheme_configs[ext][colorscheme] = config + + theme_configs = dict2(loaded_configs['themes']) + top_theme_configs = dict(loaded_configs['top_themes']) + 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=Context(config), data=data, echoerr=ee)[1]: + hadproblem = True + + for top_theme, config in top_theme_configs.items(): + data = { + 'ext': None, + 'colorscheme_configs': colorscheme_configs, + 'import_paths': import_paths, + 'main_config': main_config, + 'theme_configs': theme_configs, + 'ext_theme_configs': None, + 'colors_config': colors_config + } + data['theme_type'] = 'top' + data['theme'] = top_theme + if top_theme_spec.match(config, context=Context(config), data=data, echoerr=ee)[1]: + hadproblem = True + + return hadproblem diff --git a/powerline/lint/checks.py b/powerline/lint/checks.py new file mode 100644 index 0000000..8d9cb12 --- /dev/null +++ b/powerline/lint/checks.py @@ -0,0 +1,866 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re +import logging + +from collections import defaultdict + +from powerline.lib.threaded import ThreadedSegment +from powerline.lib.unicode import unicode +from powerline.lint.markedjson.markedvalue import MarkedUnicode +from powerline.lint.markedjson.error import DelayedEchoErr, Mark +from powerline.lint.selfcheck import havemarks +from powerline.lint.context import JStr, list_themes +from powerline.lint.imp import WithPath, import_function, import_segment +from powerline.lint.spec import Spec +from powerline.lint.inspect import getconfigargspec + + +list_sep = JStr(', ') + + +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_groups', '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_groups', 'name')) + + +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_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): + 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): + havemarks(theme) + if theme not in data['configs']['top_themes']: + echoerr(context='Error while checking extension configuration (key {key})'.format(key=context.key), + 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 + + +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), + 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 = None + if ext: + def listed_key(d, k): + try: + return [d[k]] + except KeyError: + return [] + + if colorscheme == '__main__': + colorscheme_names = set(data['ext_colorscheme_configs'][ext]) + colorscheme_names.update(data['top_colorscheme_configs']) + colorscheme_names.discard('__main__') + configs = [ + ( + name, + listed_key(data['ext_colorscheme_configs'][ext], name) + + listed_key(data['ext_colorscheme_configs'][ext], '__main__') + + listed_key(data['top_colorscheme_configs'], name) + ) + for name in colorscheme_names + ] + else: + configs = [ + ( + colorscheme, + listed_key(data['ext_colorscheme_configs'][ext], colorscheme) + + listed_key(data['ext_colorscheme_configs'][ext], '__main__') + + listed_key(data['top_colorscheme_configs'], colorscheme) + ) + ] + else: + try: + configs = [(colorscheme, [data['top_colorscheme_configs'][colorscheme]])] + except KeyError: + pass + hadproblem = False + for new_colorscheme, config_lst in configs: + not_found = [] + new_data = data.copy() + new_data['colorscheme'] = new_colorscheme + for config in config_lst: + 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 + if not proceed: + break + if not_found and len(not_found) == len(config_lst): + echoerr( + context='Error while checking group definition in colorscheme (key {key})'.format( + key=context.key), + problem='name {0} is not present anywhere in {1} {2} {3} colorschemes: {4}'.format( + group, len(not_found), ext, new_colorscheme, ', '.join(not_found)), + problem_mark=group.mark + ) + hadproblem = True + return True, False, hadproblem + + +def check_key_compatibility(segment, data, context, echoerr): + 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), + 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_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_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_mark=context[-1][1].mark, + problem=( + 'found missing keys required to determine highlight group. ' + 'Either highlight_groups 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), + problem='failed to import module {0}'.format(module), + problem_mark=module.mark) + return True, False, True + return True, False, False + + +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) + + +highlight_group_spec = Spec().ident().copy +_highlight_group_spec = highlight_group_spec().context_message( + 'Error while checking function documentation while checking theme (key {key})') + + +def check_hl_group_name(hl_group, context_mark, context, echoerr): + '''Check highlight group name: it should match naming conventions + + :param str hl_group: + Checked group. + :param Mark context_mark: + Context mark. May be ``None``. + :param Context context: + Current context. + :param func echoerr: + Function used for error reporting. + + :return: ``False`` if check succeeded and ``True`` if it failed. + ''' + return _highlight_group_spec.match(hl_group, context_mark=context_mark, context=context, echoerr=echoerr)[1] + + +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 + + hadproblem = False + + if func.__doc__: + NO_H_G_USED_STR = 'No highlight groups are used (literal segment).' + 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 + if hl_groups is None: + idx -= LHGUS + mark = Mark(mark_name, i + 1, idx + 1, func.__doc__, pointer + idx) + echoerr( + context='Error while checking theme (key {key})'.format(key=context.key), + context_mark=function_name.mark, + problem=( + 'found highlight group definition in addition to sentence stating that ' + 'no highlight groups are used' + ), + problem_mark=mark, + ) + hadproblem = True + continue + 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) + elif NO_H_G_USED_STR in line: + idx = line.index(NO_H_G_USED_STR) + if hl_groups: + mark = Mark(mark_name, i + 1, idx + 1, func.__doc__, pointer + idx) + echoerr( + context='Error while checking theme (key {key})'.format(key=context.key), + context_mark=function_name.mark, + problem=( + 'found sentence stating that no highlight groups are used ' + 'in addition to highlight group definition' + ), + problem_mark=mark, + ) + hadproblem = True + continue + hl_groups = None + pointer += len(line) + len('\n') + + 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_mark=function_name.mark, + 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=divider_hl_group.mark, + ) + hadproblem = True + if check_hl_group_name(divider_hl_group, function_name.mark, context, echoerr): + 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)) + ) + if check_hl_group_name(hl_group, function_name.mark, context, echoerr): + hadproblem = True + 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), + 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), + problem='found highlight group {0} not defined in the following colorschemes: {1}'.format( + h[0], list_sep.join(r)) + ) + hadproblem = True + elif hl_groups is not None: + 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), + 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), + 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_group_in_colorscheme(hl_group, cconfig, allow_gradients, data, context, echoerr): + havemarks(hl_group, cconfig) + if hl_group not in cconfig.get('groups', {}): + return False + elif not allow_gradients or allow_gradients == 'force': + group_config = cconfig['groups'][hl_group] + while isinstance(group_config, unicode): + try: + group_config = cconfig['groups'][group_config] + except KeyError: + # No such group. Error was already reported when checking + # colorschemes. + return True + havemarks(group_config) + hadgradient = False + for ckey in ('fg', 'bg'): + color = group_config.get(ckey) + if not color: + # No color. Error was already reported when checking + # colorschemes. + return True + 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_mark=hl_group.mark, + problem='group {0} is using gradient {1} instead of a color'.format(hl_group, color), + problem_mark=color.mark + ) + return False + if allow_gradients == 'force' and not hadgradient: + echoerr( + context='Error while checking highlight group in theme (key {key})'.format( + key=context.key), + 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 + ) + return False + return True + + +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 = [] + found = False + for colorscheme, cconfig in data['colorscheme_configs'][ext].items(): + if hl_group_in_colorscheme(hl_group, cconfig, allow_gradients, data, context, echoerr): + found = True + else: + r.append(colorscheme) + if not found: + pass + 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), + 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), + 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), + 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_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_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_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.enter_key(args, key), + echoerr + ) + if khadproblem: + hadproblem = True + if not proceed: + return hadproblem + + return hadproblem + + +def check_args(get_functions, args, data, context, echoerr): + 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_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 + + +common_names = defaultdict(set) + + +def register_common_name(name, cmodule, cname): + s = cmodule + '.' + cname + cmodule_mark = Mark('', 1, 1, s, 1) + cname_mark = Mark('', 1, len(cmodule) + 1, s, len(cmodule) + 1) + common_names[name].add((MarkedUnicode(cmodule, cmodule_mark), MarkedUnicode(cname, cname_mark))) + + +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: + if name in common_names: + for cmodule, cname in common_names[name]: + cfunc = import_segment(cname, data, context, echoerr, module=MarkedUnicode(cmodule, None)) + if cfunc: + yield cfunc + 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 + + +def check_log_file_level(this_level, data, context, echoerr): + '''Check handler level specified in :ref:`log_file key ` + + This level must be greater or equal to the level in :ref:`log_level key + `. + ''' + havemarks(this_level) + hadproblem = False + top_level = context[0][1].get('common', {}).get('log_level', 'WARNING') + top_level_str = top_level + top_level_mark = getattr(top_level, 'mark', None) + if ( + not isinstance(top_level, unicode) or not hasattr(logging, top_level) + or not isinstance(this_level, unicode) or not hasattr(logging, this_level) + ): + return True, False, hadproblem + top_level = getattr(logging, top_level) + this_level_str = this_level + this_level_mark = this_level.mark + this_level = getattr(logging, this_level) + if this_level < top_level: + echoerr( + context='Error while checking log level index (key {key})'.format( + key=context.key), + context_mark=this_level_mark, + problem='found level that is less critical then top level ({0} < {0})'.format( + this_level_str, top_level_str), + problem_mark=top_level_mark, + ) + hadproblem = True + return True, False, hadproblem + + +def check_logging_handler(handler_name, data, context, echoerr): + havemarks(handler_name) + import_paths = [os.path.expanduser(path) for path in context[0][1].get('common', {}).get('paths', [])] + + handler_module, separator, handler_class = handler_name.rpartition('.') + if not separator: + handler_module = 'logging.handlers' + handler_class = handler_name + with WithPath(import_paths): + try: + handler = getattr(__import__(str(handler_module), fromlist=[str(handler_class)]), str(handler_class)) + except ImportError: + echoerr(context='Error while loading logger class (key {key})'.format(key=context.key), + problem='failed to load module {0}'.format(handler_module), + problem_mark=handler_name.mark) + return True, False, True + except AttributeError: + echoerr(context='Error while loading logger class (key {key})'.format(key=context.key), + problem='failed to load handler class {0}'.format(handler_class), + problem_mark=handler_name.mark) + return True, False, True + + if not issubclass(handler, logging.Handler): + echoerr(context='Error while loading logger class (key {key})'.format(key=context.key), + problem='loaded class {0} is not a logging.Handler subclass'.format(handler_class), + problem_mark=handler_name.mark) + return True, False, True + + return True, False, False diff --git a/powerline/lint/context.py b/powerline/lint/context.py new file mode 100644 index 0000000..a48a283 --- /dev/null +++ b/powerline/lint/context.py @@ -0,0 +1,68 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import itertools + +from powerline.lib.unicode import unicode +from powerline.lint.markedjson.markedvalue import MarkedUnicode +from powerline.lint.selfcheck import havemarks + + +class JStr(unicode): + def join(self, iterable): + return super(JStr, self).join((unicode(item) for item in iterable)) + + +key_sep = JStr('/') + + +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])] + + +class Context(tuple): + for func in dir(tuple): + if func in ('__getitem__', '__init__', '__getattribute__', '__len__', '__iter__'): + continue + exec(( + 'def {0}(self, *args, **kwargs):\n' + ' raise TypeError("{0} is not allowed for Context")' + ).format(func)) + del func + + __slots__ = () + + def __new__(cls, base, context_key=None, context_value=None): + if context_key is not None: + assert(context_value is not None) + assert(type(base) is Context) + havemarks(context_key, context_value) + return tuple.__new__(cls, tuple.__add__(base, ((context_key, context_value),))) + else: + havemarks(base) + return tuple.__new__(cls, ((MarkedUnicode('', base.mark), base),)) + + @property + def key(self): + return key_sep.join((c[0] for c in self)) + + def enter_key(self, value, key): + return self.enter(value.keydict[key], value[key]) + + def enter_item(self, name, item): + return self.enter(MarkedUnicode(name, item.mark), item) + + def enter(self, context_key, context_value): + return Context.__new__(Context, self, context_key, context_value) diff --git a/powerline/lint/imp.py b/powerline/lint/imp.py new file mode 100644 index 0000000..399654e --- /dev/null +++ b/powerline/lint/imp.py @@ -0,0 +1,56 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from powerline.lint.selfcheck import havemarks + + +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 import_function(function_type, name, data, context, echoerr, module): + havemarks(name, module) + + if module == 'powerline.segments.i3wm' and name == 'workspaces': + echoerr(context='Warning while checking segments (key {key})'.format(key=context.key), + context_mark=name.mark, + problem='segment {0} from {1} is deprecated'.format(name, module), + problem_mark=module.mark) + + with WithPath(data['import_paths']): + try: + func = getattr(__import__(str(module), fromlist=[str(name)]), str(name)) + except ImportError: + echoerr(context='Error while checking segments (key {key})'.format(key=context.key), + 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), + 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_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) diff --git a/powerline/lint/inspect.py b/powerline/lint/inspect.py new file mode 100644 index 0000000..15bb610 --- /dev/null +++ b/powerline/lint/inspect.py @@ -0,0 +1,63 @@ +# 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 in omitted_args + or arg == 'pl' + or arg == 'self' + 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 0000000..dea5faf --- /dev/null +++ b/powerline/lint/markedjson/__init__.py @@ -0,0 +1,19 @@ +# 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 JSON value and produce the corresponding Python object + + :return: + (hadproblem, object) where first argument is true if there were errors + during loading JSON stream and second is the corresponding JSON 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 0000000..bd5620d --- /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 0000000..372d84b --- /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.abc.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 0000000..732120b --- /dev/null +++ b/powerline/lint/markedjson/error.py @@ -0,0 +1,241 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import re + +from powerline.lib.encoding import get_preferred_output_encoding + + +NON_PRINTABLE_STR = ( + '[^' + # ASCII control characters: 0x00-0x19 + + '\t\n' # Tab, newline: allowed ASCII control characters + + '\x20-\x7E' # ASCII printable characters + # Unicode control characters: 0x7F-0x9F + + '\u0085' # Allowed unicode control character: next line character + + '\u00A0-\uD7FF' + # Surrogate escapes: 0xD800-0xDFFF + + '\uE000-\uFFFD' + + (( + '\uD800-\uDFFF' + ) if sys.maxunicode < 0x10FFFF else ( + '\U00010000-\U0010FFFF' + )) + + ']' + + (( + # Paired surrogate escapes: allowed in UCS-2 builds as the only way to + # represent characters above 0xFFFF. Only paired variant is allowed. + '|(?' % ord(s.group()) + + +def strtrans(s): + return NON_PRINTABLE_RE.sub(repl, s.replace('\t', '>---')) + + +class Mark: + def __init__(self, name, line, column, buffer, pointer, old_mark=None, merged_marks=None): + self.name = name + self.line = line + self.column = column + self.buffer = buffer + self.pointer = pointer + self.old_mark = old_mark + self.merged_marks = merged_marks or [] + + def copy(self): + return Mark(self.name, self.line, self.column, self.buffer, self.pointer, self.old_mark, self.merged_marks[:]) + + 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 advance_string(self, diff): + ret = self.copy() + # FIXME Currently does not work properly with escaped strings. + ret.column += diff + ret.pointer += diff + return ret + + def set_old_mark(self, old_mark): + if self is old_mark: + return + checked_marks = set([id(self)]) + older_mark = old_mark + while True: + if id(older_mark) in checked_marks: + raise ValueError('Trying to set recursive marks') + checked_marks.add(id(older_mark)) + older_mark = older_mark.old_mark + if not older_mark: + break + self.old_mark = old_mark + + def set_merged_mark(self, merged_mark): + self.merged_marks.append(merged_mark) + + def to_string(self, indent=0, head_text='in ', add_snippet=True): + mark = self + where = '' + processed_marks = set() + while mark: + indentstr = ' ' * indent + where += ('%s %s"%s", line %d, column %d' % ( + indentstr, head_text, mark.name, mark.line + 1, mark.column + 1)) + if add_snippet: + snippet = mark.get_snippet(indent=(indent + 4)) + if snippet: + where += ':\n' + snippet + if mark.merged_marks: + where += '\n' + indentstr + ' with additionally merged\n' + where += mark.merged_marks[0].to_string(indent + 4, head_text='', add_snippet=False) + for mmark in mark.merged_marks[1:]: + where += '\n' + indentstr + ' and\n' + where += mmark.to_string(indent + 4, head_text='', add_snippet=False) + if add_snippet: + processed_marks.add(id(mark)) + if mark.old_mark: + where += '\n' + indentstr + ' which replaced value\n' + indent += 4 + mark = mark.old_mark + if id(mark) in processed_marks: + raise ValueError('Trying to dump recursive mark') + return where + + if sys.version_info < (3,): + def __str__(self): + return self.to_string().encode('utf-8') + + def __unicode__(self): + return self.to_string() + else: + def __str__(self): + return self.to_string() + + def __eq__(self, other): + return self is other or ( + self.name == other.name + and self.line == other.line + and self.column == other.column + ) + + +if sys.version_info < (3,): + def echoerr(**kwargs): + stream = kwargs.pop('stream', sys.stderr) + stream.write('\n') + stream.write((format_error(**kwargs) + '\n').encode(get_preferred_output_encoding())) +else: + def echoerr(**kwargs): + stream = kwargs.pop('stream', sys.stderr) + stream.write('\n') + stream.write(format_error(**kwargs) + '\n') + + +def format_error(context=None, context_mark=None, problem=None, problem_mark=None, note=None, indent=0): + lines = [] + indentstr = ' ' * indent + if context is not None: + lines.append(indentstr + context) + if ( + context_mark is not None + and ( + problem is None or problem_mark is None + or context_mark != problem_mark + ) + ): + lines.append(context_mark.to_string(indent=indent)) + if problem is not None: + lines.append(indentstr + problem) + if problem_mark is not None: + lines.append(problem_mark.to_string(indent=indent)) + if note is not None: + lines.append(indentstr + 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)) + + +class EchoErr(object): + __slots__ = ('echoerr', 'logger', 'indent') + + def __init__(self, echoerr, logger, indent=0): + self.echoerr = echoerr + self.logger = logger + self.indent = indent + + def __call__(self, **kwargs): + kwargs = kwargs.copy() + kwargs.setdefault('indent', self.indent) + self.echoerr(**kwargs) + + +class DelayedEchoErr(EchoErr): + __slots__ = ('echoerr', 'logger', 'errs', 'message', 'separator_message', 'indent', 'indent_shift') + + def __init__(self, echoerr, message='', separator_message=''): + super(DelayedEchoErr, self).__init__(echoerr, echoerr.logger) + self.errs = [[]] + self.message = message + self.separator_message = separator_message + self.indent_shift = (4 if message or separator_message else 0) + self.indent = echoerr.indent + self.indent_shift + + def __call__(self, **kwargs): + kwargs = kwargs.copy() + kwargs['indent'] = kwargs.get('indent', 0) + self.indent + self.errs[-1].append(kwargs) + + def next_variant(self): + self.errs.append([]) + + def echo_all(self): + if self.message: + self.echoerr(problem=self.message, indent=(self.indent - self.indent_shift)) + for variant in self.errs: + if not variant: + continue + if self.separator_message and variant is not self.errs[0]: + self.echoerr(problem=self.separator_message, indent=(self.indent - self.indent_shift)) + for kwargs in variant: + self.echoerr(**kwargs) + + def __nonzero__(self): + return not not self.errs + + __bool__ = __nonzero__ diff --git a/powerline/lint/markedjson/events.py b/powerline/lint/markedjson/events.py new file mode 100644 index 0000000..ef8a70e --- /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 0000000..3ee5686 --- /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 0000000..3b8db3e --- /dev/null +++ b/powerline/lint/markedjson/markedvalue.py @@ -0,0 +1,151 @@ +# 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: + r.append(MarkedUnicode(s, self.mark.advance_string(pointdiff))) + 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 setmerged(self, d): + try: + self.mark.set_merged_mark(d.mark) + except AttributeError: + pass + + def __setitem__(self, key, value): + try: + old_value = self[key] + except KeyError: + pass + else: + try: + key.mark.set_old_mark(self.keydict[key].mark) + except AttributeError: + pass + except KeyError: + pass + try: + value.mark.set_old_mark(old_value.mark) + except AttributeError: + pass + 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 0000000..66ad843 --- /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 0000000..336a2a2 --- /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 0000000..0ca4516 --- /dev/null +++ b/powerline/lint/markedjson/reader.py @@ -0,0 +1,141 @@ +# 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_RE +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_RE.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=-1): + # Was size=4096 + assert(size < 0) + # WARNING: reading the whole stream at once. To change this behaviour to + # former reading N characters at once one must make sure that reading + # never ends at partial unicode character. + 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 0000000..fa8ceaa --- /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 0000000..b0bddf3 --- /dev/null +++ b/powerline/lint/markedjson/scanner.py @@ -0,0 +1,499 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from string import hexdigits + +from powerline.lint.markedjson.error import MarkedError +from powerline.lint.markedjson import tokens +from powerline.lib.unicode import unicode, unichr, surrogate_pair_to_character + + +hexdigits_set = set(hexdigits) + + +# 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 hexdigits: + 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) + self.forward(length) + if 0xD800 <= code <= 0xDC00: + # Start of the surrogate pair + next_char = self.prefix(6) + if ( + next_char[0] != '\\' + or next_char[1] != 'u' + or not (set(next_char[2:]) < hexdigits_set) + or not (0xDC00 <= int(next_char[2:], 16) <= 0xDFFF) + ): + raise ScannerError( + 'while scanning a double-quoted scalar', start_mark, + 'expected escape sequence with the next character in surrogate pair, but found %r' % ( + next_char + ), + self.get_mark() + ) + code = surrogate_pair_to_character(code, int(next_char[2:], 16)) + self.forward(6) + chunks.append(unichr(code)) + 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 0000000..6fa8bf1 --- /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/lint/selfcheck.py b/powerline/lint/selfcheck.py new file mode 100644 index 0000000..06d1fbe --- /dev/null +++ b/powerline/lint/selfcheck.py @@ -0,0 +1,16 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lib.unicode import unicode + + +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) + ']')) diff --git a/powerline/lint/spec.py b/powerline/lint/spec.py new file mode 100644 index 0000000..6a54441 --- /dev/null +++ b/powerline/lint/spec.py @@ -0,0 +1,759 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import itertools +import re + +from copy import copy + +from powerline.lib.unicode import unicode +from powerline.lint.markedjson.error import echoerr, DelayedEchoErr, NON_PRINTABLE_STR +from powerline.lint.selfcheck import havemarks + + +NON_PRINTABLE_RE = re.compile( + NON_PRINTABLE_STR.translate({ + ord('\t'): None, + ord('\n'): None, + 0x0085: None, + }) +) + + +class Spec(object): + '''Class that describes some JSON value + + In powerline it is only used to describe JSON values stored in powerline + configuration. + + :param dict keys: + Dictionary that maps keys that may be present in the given JSON + dictionary to their descriptions. If this parameter is not empty it + implies that described value has dictionary type. Non-dictionary types + must be described using ``Spec()``: without arguments. + + .. note:: + Methods that create the specifications return ``self``, so calls to them + may be chained: ``Spec().type(unicode).re('^\w+$')``. This does not + apply to functions that *apply* specification like :py:meth`Spec.match`. + + .. note:: + Methods starting with ``check_`` return two values: first determines + whether caller should proceed on running other checks, second + determines whether there were any problems (i.e. whether error was + reported). One should not call these methods directly: there is + :py:meth:`Spec.match` method for checking values. + + .. note:: + In ``check_`` and ``match`` methods specifications are identified by + their indexes for the purpose of simplyfying :py:meth:`Spec.copy` + method. + + Some common parameters: + + ``data``: + Whatever data supplied by the first caller for checker functions. Is not + processed by :py:class:`Spec` methods in any fashion. + ``context``: + :py:class:`powerline.lint.context.Context` instance, describes context + of the value. :py:class:`Spec` methods only use its ``.key`` methods for + error messages. + ``echoerr``: + Callable that should be used to echo errors. Is supposed to take four + optional keyword arguments: ``problem``, ``problem_mark``, ``context``, + ``context_mark``. + ``value``: + Checked value. + ''' + + 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): + '''Describe additional keys that may be present in given JSON value + + If called with some keyword arguments implies that described value is + a dictionary. If called without keyword parameters it is no-op. + + :return: self. + ''' + 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): + '''Deep copy the spec + + :param dict copied: + Internal dictionary used for storing already copied values. This + parameter should not be used. + + :return: New :py:class:`Spec` object that is a deep copy of ``self``. + ''' + 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): + '''Helper for the :py:meth:`Spec.copy` function + + Populates new instance with values taken from the old one. + + :param dict d: + ``__dict__`` of the old instance. + :param dict copied: + Storage for already copied values. + ''' + 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): + '''Define specification for non-static keys + + This method should be used if key names cannot be determined at runtime + or if a number of keys share identical spec (in order to not repeat it). + :py:meth:`Spec.match` method processes dictionary in the given order: + + * First it tries to use specifications provided at the initialization or + by the :py:meth:`Spec.update` method. + * If no specification for given key was provided it processes + specifications from ``keyfunc`` argument in order they were supplied. + Once some key matches specification supplied second ``spec`` argument + is used to determine correctness of the value. + + :param Spec keyfunc: + :py:class:`Spec` instance or a regular function that returns two + values (the same :py:meth:`Spec.match` returns). This argument is + used to match keys that were not provided at initialization or via + :py:meth:`Spec.update`. + :param Spec spec: + :py:class:`Spec` instance that will be used to check keys matched by + ``keyfunc``. + + :return: self. + ''' + 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): + '''Define message which will be used when unknown key was found + + “Unknown” is a key that was not provided at the initialization and via + :py:meth:`Spec.update` and did not match any ``keyfunc`` provided via + :py:meth:`Spec.unknown_spec`. + + :param msgfunc: + Function that takes that unknown key as an argument and returns the + message text. Text will appear at the top (start of the sentence). + + :return: self. + ''' + self.ufailmsg = msgfunc + return self + + def context_message(self, msg): + '''Define message that describes context + + :param str msg: + Message that describes context. Is written using the + :py:meth:`str.format` syntax and is expected to display keyword + parameter ``key``. + + :return: self. + ''' + 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): + '''Check that given value matches given type(s) + + :param tuple types: + List of accepted types. Since :py:class:`Spec` is supposed to + describe JSON values only ``dict``, ``list``, ``unicode``, ``bool``, + ``float`` and ``NoneType`` types make any sense. + + :return: proceed, hadproblem. + ''' + havemarks(value) + if type(value.value) not in types: + echoerr( + context=self.cmsg.format(key=context.key), + context_mark=context_mark, + problem='{0!r} must be a {1} instance, not {2}'.format( + value, + ', '.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): + '''Check value using given function + + :param function func: + Callable that should accept four positional parameters: + + #. checked value, + #. ``data`` parameter with arbitrary data (supplied by top-level + caller), + #. current context and + #. function used for echoing errors. + + This callable should return three values: + + #. determines whether ``check_func`` caller should proceed + calling other checks, + #. determines whether ``check_func`` should echo error on its own + (it should be set to False if ``func`` echoes error itself) and + #. determines whether function has found some errors in the checked + value. + + :param function msg_func: + Callable that takes checked value as the only positional parameter + and returns a string that describes the problem. Only useful for + small checker functions since it is ignored when second returned + value is false. + + :return: proceed, hadproblem. + ''' + havemarks(value) + proceed, echo, hadproblem = func(value, data, context, echoerr) + if echo and hadproblem: + echoerr(context=self.cmsg.format(key=context.key), + 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): + '''Check that each value in the list matches given specification + + :param function item_func: + Callable like ``func`` from :py:meth:`Spec.check_func`. Unlike + ``func`` this callable is called for each value in the list and may + be a :py:class:`Spec` object index. + :param func msg_func: + Callable like ``msg_func`` from :py:meth:`Spec.check_func`. Should + accept one problematic item and is not used for :py:class:`Spec` + object indices in ``item_func`` method. + + :return: proceed, hadproblem. + ''' + 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.enter_item('list item ' + unicode(i), item), + echoerr + ) + else: + proceed, echo, fhadproblem = item_func(item, data, context, echoerr) + if echo and fhadproblem: + echoerr(context=self.cmsg.format(key=context.key + '/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): + '''Check that given value matches one of the given specifications + + :param int start: + First specification index. + :param int end: + Specification index that is greater by 1 then last specification + index. + + This method does not give an error if any specification from + ``self.specs[start:end]`` is matched by the given value. + ''' + havemarks(value) + new_echoerr = DelayedEchoErr( + echoerr, + 'One of the either variants failed. Messages from the first variant:', + 'messages from the next variant:' + ) + + hadproblem = False + for spec in self.specs[start:end]: + proceed, hadproblem = spec.match(value, value.mark, data, context, new_echoerr) + new_echoerr.next_variant() + 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): + '''Check that given value is a list with items matching specifications + + :param int start: + First specification index. + :param int end: + Specification index that is greater by 1 then last specification + index. + + This method checks that each item in the value list matches + specification with index ``start + item_number``. + ''' + 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.enter_item('tuple item ' + unicode(i), item), + echoerr + ) + if ihadproblem: + hadproblem = True + if not proceed: + return False, hadproblem + return True, hadproblem + + def check_printable(self, value, context_mark, data, context, echoerr, _): + '''Check that given unicode string contains only printable characters + ''' + hadproblem = False + for match in NON_PRINTABLE_RE.finditer(value): + hadproblem = True + echoerr( + context=self.cmsg.format(key=context.key), + context_mark=value.mark, + problem='found not printable character U+{0:04x} in a configuration string'.format( + ord(match.group(0))), + problem_mark=value.mark.advance_string(match.start() + 1) + ) + return True, hadproblem + + def printable(self, *args): + self.type(unicode) + self.checks.append(('check_printable', args)) + return self + + def type(self, *args): + '''Describe value that has one of the types given in arguments + + :param args: + List of accepted types. Since :py:class:`Spec` is supposed to + describe JSON values only ``dict``, ``list``, ``unicode``, ``bool``, + ``float`` and ``NoneType`` types make any sense. + + :return: self. + ''' + 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): + '''Describe value that has given length + + :param str comparison: + Type of the comparison. Valid values: ``le``, ``lt``, ``ge``, + ``gt``, ``eq``. + :param int cint: + Integer with which length is compared. + :param function msg_func: + Function that should accept checked value and return message that + describes the problem with this value. Default value will emit + something like “length of ['foo', 'bar'] is not greater then 10”. + + :return: self. + ''' + 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): + '''Describe value that is a number or string that has given property + + :param str comparison: + Type of the comparison. Valid values: ``le``, ``lt``, ``ge``, + ``gt``, ``eq``. This argument will restrict the number or string to + emit True on the given comparison. + :param cint: + Number or string with which value is compared. Type of this + parameter affects required type of the checked value: ``str`` and + ``unicode`` types imply ``unicode`` values, ``float`` type implies + that value can be either ``int`` or ``float``, ``int`` type implies + ``int`` value and for any other type the behavior is undefined. + :param function msg_func: + Function that should accept checked value and return message that + describes the problem with this value. Default value will emit + something like “10 is not greater then 10”. + + :return: self. + ''' + 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): + '''Describe unsigned integer value + + :param function msg_func: + Function that should accept checked value and return message that + describes the problem with this value. + + :return: self. + ''' + 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): + '''Describe list with any number of elements, each matching given spec + + :param item_func: + :py:class:`Spec` instance or a callable. Check out + :py:meth:`Spec.check_list` documentation for more details. Note that + in :py:meth:`Spec.check_list` description :py:class:`Spec` instance + is replaced with its index in ``self.specs``. + :param function msg_func: + Function that should accept checked value and return message that + describes the problem with this value. Default value will emit just + “failed check”, which is rather indescriptive. + + :return: self. + ''' + 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): + '''Describe list with the given number of elements, each matching corresponding spec + + :param (Spec,) specs: + List of specifications. Last element(s) in this list may be + optional. Each element in this list describes element with the same + index in the checked value. Check out :py:meth:`Spec.check_tuple` + for more details, but note that there list of specifications is + replaced with start and end indices in ``self.specs``. + + :return: self. + ''' + 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: + if min_len > 0: + 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): + '''Describe value that is checked by the given function + + Check out :py:meth:`Spec.check_func` documentation for more details. + ''' + self.checks.append(('check_func', func, msg_func or (lambda value: 'failed check'))) + return self + + def re(self, regex, msg_func=None): + '''Describe value that is a string that matches given regular expression + + :param str regex: + Regular expression that should be matched by the value. + :param function msg_func: + Function that should accept checked value and return message that + describes the problem with this value. Default value will emit + something like “String "xyz" does not match "[a-f]+"”. + + :return: self. + ''' + 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): + '''Describe value that is an identifier like ``foo:bar`` or ``foo`` + + :param function msg_func: + Function that should accept checked value and return message that + describes the problem with this value. Default value will emit + something like “String "xyz" is not an … identifier”. + + :return: self. + ''' + 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): + '''Describe value that is equal to one of the value in the collection + + :param set collection: + A collection of possible values. + :param function msg_func: + Function that should accept checked value and return message that + describes the problem with this value. Default value will emit + something like “"xyz" must be one of {'abc', 'def', 'ghi'}”. + + :return: self. + ''' + 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): + '''Describe value that must not be there + + Useful for giving more descriptive errors for some specific keys then + just “found unknown key: shutdown_event” or for forbidding certain + values when :py:meth:`Spec.unknown_spec` was used. + + :param str msg: + Message given for the offending value. It is formatted using + :py:meth:`str.format` with the only positional parameter which is + the value itself. + + :return: self. + ''' + self.checks.append(( + 'check_func', + (lambda *args: (True, True, True)), + (lambda value: msg.format(value)) + )) + return self + + def either(self, *specs): + '''Describes value that matches one of the given specs + + Check out :py:meth:`Spec.check_either` method documentation for more + details, but note that there a list of specs was replaced by start and + end indices in ``self.specs``. + + :return: self. + ''' + start = len(self.specs) + self.specs.extend(specs) + self.checks.append(('check_either', start, len(self.specs))) + return self + + def optional(self): + '''Mark value as optional + + Only useful for key specs in :py:meth:`Spec.__init__` and + :py:meth:`Spec.update` and some last supplied to :py:meth:`Spec.tuple`. + + :return: self. + ''' + self.isoptional = True + return self + + def required(self): + '''Mark value as required + + Only useful for key specs in :py:meth:`Spec.__init__` and + :py:meth:`Spec.update` and some last supplied to :py:meth:`Spec.tuple`. + + .. note:: + Value is required by default. This method is only useful for + altering existing specification (or rather its copy). + + :return: self. + ''' + self.isoptional = False + return self + + def match_checks(self, *args): + '''Process checks registered for the given value + + Processes only “top-level” checks: key specifications given using at the + initialization or via :py:meth:`Spec.unknown_spec` are processed by + :py:meth:`Spec.match`. + + :return: proceed, hadproblem. + ''' + 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=(), echoerr=echoerr): + '''Check that given value matches this specification + + :return: proceed, hadproblem. + ''' + 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.enter_key(value, key), + 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_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.enter_key(value, key), + echoerr + ) + if vhadproblem: + hadproblem = True + break + else: + hadproblem = True + if self.ufailmsg: + echoerr(context=self.cmsg.format(key=context.key), + context_mark=None, + problem=self.ufailmsg(key), + problem_mark=key.mark) + return True, hadproblem + + def __getitem__(self, key): + '''Get specification for the given key + ''' + return self.specs[self.keys[key]] + + def __setitem__(self, key, value): + '''Set specification for the given key + ''' + self.update(**{key: value}) diff --git a/powerline/listers/__init__.py b/powerline/listers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/powerline/listers/i3wm.py b/powerline/listers/i3wm.py new file mode 100644 index 0000000..9224d16 --- /dev/null +++ b/powerline/listers/i3wm.py @@ -0,0 +1,63 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info +from powerline.lib.dict import updated +from powerline.bindings.wm import get_i3_connection, get_connected_xrandr_outputs + + +@requires_segment_info +def output_lister(pl, segment_info): + '''List all outputs in segment_info format + ''' + + return ( + ( + updated(segment_info, output=output['name']), + { + 'draw_inner_divider': None + } + ) + for output in get_connected_xrandr_outputs(pl) + ) + + +@requires_segment_info +def workspace_lister(pl, segment_info, only_show=None, output=None): + '''List all workspaces in segment_info format + + Sets the segment info values of ``workspace`` and ``output`` to the name of + the i3 workspace and the ``xrandr`` output respectively and the keys + ``"visible"``, ``"urgent"`` and ``"focused"`` to a boolean indicating these + states. + + :param list only_show: + Specifies which workspaces to list. Valid entries are ``"visible"``, + ``"urgent"`` and ``"focused"``. If omitted or ``null`` all workspaces + are listed. + + :param str output: + May be set to the name of an X output. If specified, only workspaces + on that output are listed. Overrides automatic output detection by + the lemonbar renderer and bindings. Set to ``false`` to force + all workspaces to be shown. + ''' + + if output == None: + output = output or segment_info.get('output') + + return ( + ( + updated( + segment_info, + output=w.output, + workspace=w, + ), + { + 'draw_inner_divider': None + } + ) + for w in get_i3_connection().get_workspaces() + if (((not only_show or any(getattr(w, typ) for typ in only_show)) + and (not output or w.output == output))) + ) diff --git a/powerline/listers/pdb.py b/powerline/listers/pdb.py new file mode 100644 index 0000000..24e11ea --- /dev/null +++ b/powerline/listers/pdb.py @@ -0,0 +1,37 @@ +# 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 frame_lister(pl, segment_info, full_stack=False, maxframes=3): + '''List all frames in segment_info format + + :param bool full_stack: + If true, then all frames in the stack are listed. Normally N first + frames are discarded where N is a number of frames present at the first + invocation of the prompt minus one. + :param int maxframes: + Maximum number of frames to display. + ''' + if full_stack: + initial_stack_length = 0 + frames = segment_info['pdb'].stack + else: + initial_stack_length = segment_info['initial_stack_length'] + frames = segment_info['pdb'].stack[initial_stack_length:] + + if len(frames) > maxframes: + frames = frames[-maxframes:] + + return ( + ( + { + 'curframe': frame[0], + 'initial_stack_length': initial_stack_length, + }, + {} + ) + for frame in frames + ) diff --git a/powerline/listers/vim.py b/powerline/listers/vim.py new file mode 100644 index 0000000..583e0d3 --- /dev/null +++ b/powerline/listers/vim.py @@ -0,0 +1,123 @@ +# 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) + +try: + import vim +except ImportError: + vim = object() + + +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, + 'divider_highlight_group': 'tab:divider' + }) + ))(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 one of ``buf:``, ``buf_nc:``, ``buf_mod:``, or ``buf_nc_mod`` + prefix to all segment highlight groups. + + :param bool show_unlisted: + True if unlisted buffers should be shown as well. Current buffer is + 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 ( + (lambda buffer, current, modified: ( + buffer_updated_segment_info(segment_info, buffer), + add_multiplier(buffer, { + 'highlight_group_prefix': '{0}{1}'.format(current, modified), + 'divider_highlight_group': 'tab:divider' + }) + ))( + buffer, + 'buf' if buffer is cur_buffer else 'buf_nc', + '_mod' if int(vim.eval('getbufvar({0}, \'&modified\')'.format(buffer.number))) > 0 else '' + ) + for buffer in vim.buffers if ( + buffer is cur_buffer + or show_unlisted + # We can't use vim_getbufoption(segment_info, 'buflisted') + # here for performance reasons. Querying the buffer options + # through the vim python module's option attribute caused + # vim to think it needed to update the tabline for every + # keystroke after any event that changed the buffer's + # options. + # + # Using the vim module's eval method to directly use the + # buflisted(nr) vim method instead does not cause vim to + # update the tabline after every keystroke, but rather after + # events that would change that status. Fixes #1281 + or int(vim.eval('buflisted(%s)' % buffer.number)) > 0 + ) + ) diff --git a/powerline/matchers/__init__.py b/powerline/matchers/__init__.py new file mode 100644 index 0000000..b2b9f10 --- /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 0000000..f6de45e --- /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, buffer_name + + +def help(matcher_info): + return vim_getbufoption(matcher_info, 'buftype') == 'help' + + +def cmdwin(matcher_info): + name = buffer_name(matcher_info) + return name and os.path.basename(name) == b'[Command Line]' + + +def quickfix(matcher_info): + return 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 0000000..b2b9f10 --- /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/commandt.py b/powerline/matchers/vim/plugin/commandt.py new file mode 100644 index 0000000..7eefe9b --- /dev/null +++ b/powerline/matchers/vim/plugin/commandt.py @@ -0,0 +1,14 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from powerline.bindings.vim import vim_getbufoption, buffer_name + + +def commandt(matcher_info): + name = buffer_name(matcher_info) + return ( + vim_getbufoption(matcher_info, 'filetype') == 'command-t' + or (name and os.path.basename(name) == b'GoToFile') + ) diff --git a/powerline/matchers/vim/plugin/gundo.py b/powerline/matchers/vim/plugin/gundo.py new file mode 100644 index 0000000..e0fe377 --- /dev/null +++ b/powerline/matchers/vim/plugin/gundo.py @@ -0,0 +1,16 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from powerline.bindings.vim import buffer_name + + +def gundo(matcher_info): + name = buffer_name(matcher_info) + return name and os.path.basename(name) == b'__Gundo__' + + +def gundo_preview(matcher_info): + name = buffer_name(matcher_info) + return name and os.path.basename(name) == b'__Gundo_Preview__' diff --git a/powerline/matchers/vim/plugin/nerdtree.py b/powerline/matchers/vim/plugin/nerdtree.py new file mode 100644 index 0000000..d6e9f69 --- /dev/null +++ b/powerline/matchers/vim/plugin/nerdtree.py @@ -0,0 +1,15 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re + +from powerline.bindings.vim import buffer_name + + +NERD_TREE_RE = re.compile(b'NERD_tree_\\d+') + + +def nerdtree(matcher_info): + name = buffer_name(matcher_info) + return name and NERD_TREE_RE.match(os.path.basename(name)) diff --git a/powerline/pdb.py b/powerline/pdb.py new file mode 100644 index 0000000..b1e13ce --- /dev/null +++ b/powerline/pdb.py @@ -0,0 +1,48 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import platform +import os + +from powerline import Powerline +from powerline.lib.overrides import parse_override_var +from powerline.lib.dict import mergeargs, mergedicts + + +class PDBPowerline(Powerline): + '''PDB-specific powerline bindings + ''' + def init(self, **kwargs): + return super(PDBPowerline, self).init( + ext='pdb', + renderer_module='pdb', + **kwargs + ) + + def do_setup(self, pdb): + self.update_renderer() + self.renderer.set_pdb(pdb) + + def load_main_config(self): + r = super(PDBPowerline, self).load_main_config() + config_overrides = os.environ.get('POWERLINE_CONFIG_OVERRIDES') + if config_overrides: + mergedicts(r, mergeargs(parse_override_var(config_overrides))) + return r + + def load_theme_config(self, name): + r = super(PDBPowerline, self).load_theme_config(name) + theme_overrides = os.environ.get('POWERLINE_THEME_OVERRIDES') + if theme_overrides: + theme_overrides_dict = mergeargs(parse_override_var(theme_overrides)) + if name in theme_overrides_dict: + mergedicts(r, theme_overrides_dict[name]) + return r + + def get_config_paths(self): + paths = [path for path in os.environ.get('POWERLINE_CONFIG_PATHS', '').split(':') if path] + return paths or super(PDBPowerline, self).get_config_paths() + + if sys.version_info < (3,) and platform.python_implementation() == 'PyPy': + get_encoding = staticmethod(lambda: 'ascii') diff --git a/powerline/renderer.py b/powerline/renderer.py new file mode 100644 index 0000000..31aca80 --- /dev/null +++ b/powerline/renderer.py @@ -0,0 +1,606 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os +import re +import operator + +from itertools import chain + +from powerline.theme import Theme +from powerline.lib.unicode import unichr, strwidth_ucs_2, strwidth_ucs_4 + + +NBSP = ' ' + + +np_control_character_translations = dict(( + # Control characters: ^@ … ^Y + (i1, '^' + unichr(i1 + 0x40)) for i1 in range(0x20) +)) +'''Control character translations + +Dictionary that maps characters in range 0x00–0x1F (inclusive) to strings +``'^@'``, ``'^A'`` and so on. + +.. note: maps tab to ``^I`` and newline to ``^J``. +''' + +np_invalid_character_translations = dict(( + # Invalid unicode characters obtained using 'surrogateescape' error + # handler. + (i2, '<{0:02x}>'.format(i2 - 0xDC00)) for i2 in range(0xDC80, 0xDD00) +)) +'''Invalid unicode character translations + +When using ``surrogateescape`` encoding error handling method characters in +range 0x80–0xFF (inclusive) are transformed into unpaired surrogate escape +unicode codepoints 0xDC80–0xDD00. This dictionary maps such characters to +``<80>``, ``<81>``, and so on: in Python-3 they cannot be printed or +converted to UTF-8 because UTF-8 standard does not allow surrogate escape +characters, not even paired ones. Python-2 contains a bug that allows such +action, but printing them in any case makes no sense. +''' + +# XXX: not using `r` because it makes no sense. +np_invalid_character_re = re.compile('(? 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 + # algorithm 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 = self.hl_join([ + segment['_rendered_hl'] + for segment in self._render_segments(theme, segments, hl_args) + ]) + if rendered_highlighted: + rendered_highlighted += self.hlstyle(**hl_args) + + return construct_returned_value(rendered_highlighted, segments, current_width, output_raw, output_width) + + def _prepare_segments(self, segments, calculate_contents_len): + '''Translate non-printable characters and calculate segment width + ''' + for segment in segments: + segment['contents'] = translate_np(segment['contents']) + if calculate_contents_len: + for segment in segments: + if segment['literal_contents'][1]: + segment['_contents_len'] = segment['literal_contents'][0] + else: + segment['_contents_len'] = self.strwidth(segment['contents']) + + 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() + prev_segment = theme.EMPTY_SEGMENT + try: + first_segment = next(iter(( + segment + for segment in segments + if not segment['literal_contents'][1] + ))) + except StopIteration: + first_segment = None + try: + last_segment = next(iter(( + segment + for segment in reversed(segments) + if not segment['literal_contents'][1] + ))) + except StopIteration: + last_segment = None + for index, segment in enumerate(segments): + side = segment['side'] + segment_len = segment['_contents_len'] + if not segment['literal_contents'][1]: + if side == 'left': + if segment is not last_segment: + compare_segment = next(iter(( + segment + for segment in segments[index + 1:] + if not segment['literal_contents'][1] + ))) + else: + compare_segment = theme.EMPTY_SEGMENT + else: + compare_segment = prev_segment + + divider_type = 'soft' if compare_segment['highlight']['bg'] == segment['highlight']['bg'] else 'hard' + + outer_padding = int(bool( + segment is first_segment + if side == 'left' else + segment is last_segment + )) * theme.outer_padding + + draw_divider = segment['draw_' + divider_type + '_divider'] + segment_len += outer_padding + if draw_divider: + segment_len += divider_widths[side][divider_type] + divider_spaces + prev_segment = segment + + segment['_len'] = segment_len + ret += segment_len + return ret + + def _render_segments(self, theme, segments, hl_args, 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() + prev_segment = theme.EMPTY_SEGMENT + try: + first_segment = next(iter(( + segment + for segment in segments + if not segment['literal_contents'][1] + ))) + except StopIteration: + first_segment = None + try: + last_segment = next(iter(( + segment + for segment in reversed(segments) + if not segment['literal_contents'][1] + ))) + except StopIteration: + last_segment = None + + for index, segment in enumerate(segments): + side = segment['side'] + if not segment['literal_contents'][1]: + if side == 'left': + if segment is not last_segment: + compare_segment = next(iter(( + segment + for segment in segments[index + 1:] + if not segment['literal_contents'][1] + ))) + else: + compare_segment = theme.EMPTY_SEGMENT + else: + compare_segment = prev_segment + outer_padding = int(bool( + segment is first_segment + if side == 'left' else + segment is last_segment + )) * theme.outer_padding * ' ' + 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'] + + segment_hl_args = {} + segment_hl_args.update(segment['highlight']) + segment_hl_args.update(hl_args) + + # 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_hl_args) + divider_highlighted = self.hl(divider_raw, divider_fg, divider_bg, False, **hl_args) + 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, **hl_args) + contents_highlighted = self.hl(self.escape(contents_raw), **segment_hl_args) + 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_hl_args) + segment['_rendered_raw'] = contents_raw + segment['_rendered_hl'] = contents_highlighted + prev_segment = segment + else: + segment['_rendered_raw'] = ' ' * segment['literal_contents'][0] + segment['_rendered_hl'] = segment['literal_contents'][1] + yield segment + + def escape(self, string): + '''Method that escapes segment contents. + ''' + return string.translate(self.character_translations) + + def hlstyle(fg=None, bg=None, attrs=None, **kwargs): + '''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, attrs=None, **kwargs): + '''Output highlighted chunk. + + This implementation just outputs :py:meth:`hlstyle` joined with + ``contents``. + ''' + return self.hlstyle(fg, bg, attrs, **kwargs) + (contents or '') diff --git a/powerline/renderers/__init__.py b/powerline/renderers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/powerline/renderers/i3bar.py b/powerline/renderers/i3bar.py new file mode 100644 index 0000000..3eab61f --- /dev/null +++ b/powerline/renderers/i3bar.py @@ -0,0 +1,36 @@ +# 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, attrs=None, **kwargs): + segment = { + 'full_text': contents, + 'separator': False, + 'separator_block_width': 0, # no separators + } + + 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'] = '#{0:06x}'.format(bg[1]) + return json.dumps(segment) + ',' + + +renderer = I3barRenderer diff --git a/powerline/renderers/ipython/__init__.py b/powerline/renderers/ipython/__init__.py new file mode 100644 index 0000000..8f463b5 --- /dev/null +++ b/powerline/renderers/ipython/__init__.py @@ -0,0 +1,34 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import Theme +from powerline.renderers.shell import PromptRenderer + + +class IPythonRenderer(PromptRenderer): + '''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() diff --git a/powerline/renderers/ipython/pre_5.py b/powerline/renderers/ipython/pre_5.py new file mode 100644 index 0000000..9fc8c21 --- /dev/null +++ b/powerline/renderers/ipython/pre_5.py @@ -0,0 +1,56 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell import ShellRenderer +from powerline.renderers.shell.readline import ReadlineRenderer +from powerline.renderers.ipython import IPythonRenderer + + +class IPythonPre50Renderer(IPythonRenderer, ShellRenderer): + '''Powerline ipython segment renderer for pre-5.0 IPython versions.''' + def render(self, **kwargs): + # XXX super(ShellRenderer), *not* super(IPythonPre50Renderer) + return super(ShellRenderer, self).render(**kwargs) + + def do_render(self, segment_info, **kwargs): + segment_info.update(client_id='ipython') + return super(IPythonPre50Renderer, self).do_render( + segment_info=segment_info, + **kwargs + ) + + +class IPythonPromptRenderer(IPythonPre50Renderer, ReadlineRenderer): + '''Powerline ipython prompt (in and in2) renderer''' + pass + + +class IPythonNonPromptRenderer(IPythonPre50Renderer): + '''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/ipython/since_5.py b/powerline/renderers/ipython/since_5.py new file mode 100644 index 0000000..88c7625 --- /dev/null +++ b/powerline/renderers/ipython/since_5.py @@ -0,0 +1,130 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import operator + +from collections import defaultdict + +try: + from __builtin__ import reduce +except ImportError: + from functools import reduce + +from pygments.token import Token +from prompt_toolkit.styles import DynamicStyle, Attrs + +from powerline.renderers.ipython import IPythonRenderer +from powerline.ipython import IPythonInfo +from powerline.colorscheme import ATTR_BOLD, ATTR_ITALIC, ATTR_UNDERLINE + + +PowerlinePromptToken = Token.Generic.Prompt.Powerline + + +# Note: since 2.7 there is dict.__missing__ with same purpose. But in 2.6 one +# must use defaultdict to get __missing__ working. +class PowerlineStyleDict(defaultdict): + '''Dictionary used for getting pygments style for Powerline groups + ''' + def __new__(cls, missing_func): + return defaultdict.__new__(cls) + + def __init__(self, missing_func): + super(PowerlineStyleDict, self).__init__() + self.missing_func = missing_func + + def __missing__(self, key): + return self.missing_func(key) + + +class PowerlinePromptStyle(DynamicStyle): + def get_attrs_for_token(self, token): + if ( + token not in PowerlinePromptToken + or len(token) != len(PowerlinePromptToken) + 1 + or not token[-1].startswith('Pl') + or token[-1] == 'Pl' + ): + return super(PowerlinePromptStyle, self).get_attrs_for_token(token) + ret = { + 'color': None, + 'bgcolor': None, + 'bold': None, + 'underline': None, + 'italic': None, + 'reverse': False, + 'blink': False, + } + for prop in token[-1][3:].split('_'): + if prop[0] == 'a': + ret[prop[1:]] = True + elif prop[0] == 'f': + ret['color'] = prop[1:] + elif prop[0] == 'b': + ret['bgcolor'] = prop[1:] + return Attrs(**ret) + + def get_token_to_attributes_dict(self): + dct = super(PowerlinePromptStyle, self).get_token_to_attributes_dict() + + def fallback(key): + try: + return dct[key] + except KeyError: + return self.get_attrs_for_token(key) + + return PowerlineStyleDict(fallback) + + def invalidation_hash(self): + return super(PowerlinePromptStyle, self).invalidation_hash() + 1 + + +class IPythonPygmentsRenderer(IPythonRenderer): + reduce_initial = [] + + def get_segment_info(self, segment_info, mode): + return super(IPythonPygmentsRenderer, self).get_segment_info( + IPythonInfo(segment_info), mode) + + @staticmethod + def hl_join(segments): + return reduce(operator.iadd, segments, []) + + def hl(self, contents, fg=None, bg=None, attrs=None, **kwargs): + '''Output highlighted chunk. + + This implementation outputs a list containing a single pair + (:py:class:`pygments.token.Token`, + :py:class:`powerline.lib.unicode.unicode`). + ''' + guifg = None + guibg = None + attrs = [] + if fg is not None and fg is not False: + guifg = fg[1] + if bg is not None and bg is not False: + guibg = bg[1] + if attrs: + attrs = [] + if attrs & ATTR_BOLD: + attrs.append('bold') + if attrs & ATTR_ITALIC: + attrs.append('italic') + if attrs & ATTR_UNDERLINE: + attrs.append('underline') + name = ( + 'Pl' + + ''.join(('_a' + attr for attr in attrs)) + + (('_f%6x' % guifg) if guifg is not None else '') + + (('_b%6x' % guibg) if guibg is not None else '') + ) + return [(getattr(Token.Generic.Prompt.Powerline, name), contents)] + + def hlstyle(self, **kwargs): + return [] + + def get_client_id(self, segment_info): + return id(self) + + +renderer = IPythonPygmentsRenderer diff --git a/powerline/renderers/ipython/since_7.py b/powerline/renderers/ipython/since_7.py new file mode 100644 index 0000000..ca2ea0e --- /dev/null +++ b/powerline/renderers/ipython/since_7.py @@ -0,0 +1,91 @@ +# vim:fileencoding=utf-8:noet +import operator + +try: + from __builtin__ import reduce +except ImportError: + from functools import reduce + +from pygments.token import Token +from prompt_toolkit.styles import DynamicStyle + +from powerline.renderers.ipython import IPythonRenderer +from powerline.ipython import IPythonInfo +from powerline.colorscheme import ATTR_BOLD, ATTR_ITALIC, ATTR_UNDERLINE + +used_styles = [] +seen = set() + +class PowerlinePromptStyle(DynamicStyle): + @property + def style_rules(self): + return (self.get_style() or self._dummy).style_rules + used_styles + + def invalidation_hash(self): + return (h + 1 for h in tuple(super(PowerlinePromptStyle, self).invalidation_hash())) + + +class IPythonPygmentsRenderer(IPythonRenderer): + reduce_initial = [] + + def __init__(self, **kwargs): + super(IPythonPygmentsRenderer, self).__init__(**kwargs) + self.character_translations[ord(' ')] = ' ' + + def get_segment_info(self, segment_info, mode): + return super(IPythonPygmentsRenderer, self).get_segment_info( + IPythonInfo(segment_info), mode) + + @staticmethod + def hl_join(segments): + return reduce(operator.iadd, segments, []) + + def hl(self, escaped_contents, fg=None, bg=None, attrs=None, *args, **kwargs): + '''Output highlighted chunk. + + This implementation outputs a list containing a single pair + (:py:class:`string`, + :py:class:`powerline.lib.unicode.unicode`). + ''' + guifg = None + guibg = None + att = [] + if fg is not None and fg is not False: + guifg = fg[1] + if bg is not None and bg is not False: + guibg = bg[1] + if attrs: + att = [] + if attrs & ATTR_BOLD: + att.append('bold') + if attrs & ATTR_ITALIC: + att.append('italic') + if attrs & ATTR_UNDERLINE: + att.append('underline') + + fg = (('%06x' % guifg) if guifg is not None else '') + bg = (('%06x' % guibg) if guibg is not None else '') + name = ( + 'pl' + + ''.join(('_a' + attr for attr in att)) + + '_f' + fg + '_b' + bg + ) + + global seen + if not (name in seen): + global used_styles + used_styles += [('pygments.' + name, + ''.join((' ' + attr for attr in att)) + + (' fg:#' + fg if fg != '' else ' fg:') + + (' bg:#' + bg if bg != '' else ' bg:'))] + seen.add(name) + return [((name,), escaped_contents)] + + def hlstyle(self, *args, **kwargs): + return [] + + def get_client_id(self, segment_info): + return id(self) + + +renderer = IPythonPygmentsRenderer diff --git a/powerline/renderers/lemonbar.py b/powerline/renderers/lemonbar.py new file mode 100644 index 0000000..8156807 --- /dev/null +++ b/powerline/renderers/lemonbar.py @@ -0,0 +1,61 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderer import Renderer +from powerline.theme import Theme +from powerline.colorscheme import ATTR_UNDERLINE + + +class LemonbarRenderer(Renderer): + '''lemonbar (formerly bar/bar ain't recursive) renderer + + + See documentation of `lemonbar `_ and :ref:`the usage instructions ` + ''' + + character_translations = Renderer.character_translations.copy() + character_translations[ord('%')] = '%%{}' + + @staticmethod + def hlstyle(*args, **kwargs): + # We don’t need to explicitly reset attributes, so skip those calls + return '' + + def hl(self, contents, fg=None, bg=None, attrs=None, **kwargs): + text = '' + + if fg is not None: + if fg is not False and fg[1] is not False: + text += '%{{F#ff{0:06x}}}'.format(fg[1]) + if bg is not None: + if bg is not False and bg[1] is not False: + text += '%{{B#ff{0:06x}}}'.format(bg[1]) + + if attrs & ATTR_UNDERLINE: + text += '%{+u}' + + return text + contents + '%{F-B--u}' + + def render(self, *args, **kwargs): + return '%{{l}}{0}%{{r}}{1}'.format( + super(LemonbarRenderer, self).render(side='left', segment_info={'output': kwargs.get('matcher_info')}, *args, **kwargs), + super(LemonbarRenderer, self).render(side='right', segment_info={'output': kwargs.get('matcher_info')}, *args, **kwargs), + ) + + def get_theme(self, matcher_info): + if not matcher_info or matcher_info not in self.local_themes: + return self.theme + match = self.local_themes[matcher_info] + + try: + return match['theme'] + except KeyError: + match['theme'] = Theme( + theme_config=match['config'], + main_theme_config=self.theme_config, + **self.theme_kwargs + ) + return match['theme'] + + +renderer = LemonbarRenderer diff --git a/powerline/renderers/pango_markup.py b/powerline/renderers/pango_markup.py new file mode 100644 index 0000000..3c1a675 --- /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, attrs=None, **kwargs): + '''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 attrs is not None and attrs is not False: + if attrs & ATTR_BOLD: + awesome_attr += ['font_weight="bold"'] + if attrs & ATTR_ITALIC: + awesome_attr += ['font_style="italic"'] + if attrs & ATTR_UNDERLINE: + awesome_attr += ['underline="single"'] + return '' + contents + '' + + escape = staticmethod(_escape) + + +renderer = PangoMarkupRenderer diff --git a/powerline/renderers/pdb.py b/powerline/renderers/pdb.py new file mode 100644 index 0000000..040f0e1 --- /dev/null +++ b/powerline/renderers/pdb.py @@ -0,0 +1,50 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import platform + +from powerline.renderers.shell.readline import ReadlineRenderer +from powerline.renderer import Renderer + + +class PDBRenderer(ReadlineRenderer): + '''PDB-specific powerline renderer + ''' + pdb = None + initial_stack_length = None + + def get_segment_info(self, segment_info, mode): + r = self.segment_info.copy() + r['pdb'] = self.pdb + r['initial_stack_length'] = self.initial_stack_length + r['curframe'] = self.pdb.curframe + return r + + def set_pdb(self, pdb): + '''Record currently used :py:class:`pdb.Pdb` instance + + Must be called before first calling :py:meth:`render` method. + + :param pdb.Pdb pdb: + Used :py:class:`pdb.Pdb` instance. This instance will later be used + by :py:meth:`get_segment_info` for patching :ref:`segment_info + ` dictionary. + ''' + self.pdb = pdb + + def render(self, **kwargs): + if self.initial_stack_length is None: + self.initial_stack_length = len(self.pdb.stack) - 1 + return Renderer.render(self, **kwargs) + + if sys.version_info < (3,) and platform.python_implementation() == 'PyPy': + def do_render(self, **kwargs): + # Make sure that only ASCII characters survive + ret = super(PDBRenderer, self).do_render(**kwargs) + ret = ret.encode('ascii', 'replace') + ret = ret.decode('ascii') + return ret + + +renderer = PDBRenderer diff --git a/powerline/renderers/shell/__init__.py b/powerline/renderers/shell/__init__.py new file mode 100644 index 0000000..d7dbf96 --- /dev/null +++ b/powerline/renderers/shell/__init__.py @@ -0,0 +1,182 @@ +# 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 PromptRenderer(Renderer): + '''Powerline generic prompt segment renderer''' + + def __init__(self, old_widths=None, **kwargs): + super(PromptRenderer, self).__init__(**kwargs) + self.old_widths = old_widths if old_widths is not None else {} + + def get_client_id(self, segment_info): + '''Get client ID given segment info + + This is used by daemon to correctly cache widths for different clients + using a single renderer instance. + + :param dict segment_info: + :ref:`Segment info dictionary `. Out of it only + ``client_id`` key is used. It is OK for this dictionary to not + contain this key. + + :return: Any hashable value or ``None``. + ''' + return segment_info.get('client_id') if isinstance(segment_info, dict) else None + + def do_render(self, output_width, segment_info, side, theme, width=None, **kwargs): + client_id = self.get_client_id(segment_info) + if client_id is not 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 and 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(PromptRenderer, self).do_render( + output_width=True, + width=width, + theme=theme, + segment_info=segment_info, + side=side, + **kwargs + ) + if client_id is not None: + 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 + + +class ShellRenderer(PromptRenderer): + '''Powerline shell segment renderer.''' + escape_hl_start = '' + escape_hl_end = '' + term_truecolor = False + term_escape_style = 'auto' + tmux_escape = False + screen_escape = False + + character_translations = Renderer.character_translations.copy() + + 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, segment_info, **kwargs): + if self.term_escape_style == 'auto': + if segment_info['environ'].get('TERM') == 'fbterm': + self.used_term_escape_style = 'fbterm' + else: + self.used_term_escape_style = 'xterm' + else: + self.used_term_escape_style = self.term_escape_style + return super(ShellRenderer, self).do_render(segment_info=segment_info, **kwargs) + + def hlstyle(self, fg=None, bg=None, attrs=None, escape=True, **kwargs): + '''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] + is_fbterm = self.used_term_escape_style == 'fbterm' + term_truecolor = not is_fbterm and self.term_truecolor + if fg is not None: + if fg is False or fg[0] is False: + ansi += [39] + else: + if 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 term_truecolor: + ansi += [48, 2] + list(int_to_rgb(bg[1])) + else: + ansi += [48, 5, bg[0]] + if attrs is not None: + if attrs is False: + ansi += [22] + else: + if attrs & ATTR_BOLD: + ansi += [1] + elif attrs & ATTR_ITALIC: + # Note: is likely not to work or even be inverse in place of + # italic. Omit using this in colorschemes. + ansi += [3] + elif attrs & ATTR_UNDERLINE: + ansi += [4] + if is_fbterm: + r = [] + while ansi: + cur_ansi = ansi.pop(0) + if cur_ansi == 38: + ansi.pop(0) + r.append('\033[1;{0}}}'.format(ansi.pop(0))) + elif cur_ansi == 48: + ansi.pop(0) + r.append('\033[2;{0}}}'.format(ansi.pop(0))) + else: + r.append('\033[{0}m'.format(cur_ansi)) + r = ''.join(r) + else: + 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 if escape else r + + 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 0000000..5ccf206 --- /dev/null +++ b/powerline/renderers/shell/bash.py @@ -0,0 +1,96 @@ +# 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('\\')] = '\\\\' + + def do_render(self, side, line, width, output_width, output_raw, hl_args, **kwargs): + + # we are rendering the normal left prompt + if side == 'left' and line == 0 and width is not None: + + # we need left prompt's width to render the raw spacer + output_width = output_width or output_raw + + left = super(BashPromptRenderer, self).do_render( + side=side, + line=line, + output_width=output_width, + width=width, + output_raw=output_raw, + hl_args=hl_args, + **kwargs + ) + left_rendered = left[0] if output_width else left + + # we don't escape color sequences in the right prompt so we can do escaping as a whole + if hl_args: + hl_args = hl_args.copy() + hl_args.update({'escape': False}) + else: + hl_args = {'escape': False} + + right = super(BashPromptRenderer, self).do_render( + side='right', + line=line, + output_width=True, + width=width, + output_raw=output_raw, + hl_args=hl_args, + **kwargs + ) + + ret = [] + if right[-1] > 0: + # if the right prompt is not empty we embed it in the left prompt + # it must be escaped as a whole so readline doesn't see it + ret.append(''.join(( + left_rendered, + self.escape_hl_start, + '\033[s', # save the cursor position + '\033[{0}C'.format(width), # move to the right edge of the terminal + '\033[{0}D'.format(right[-1] - 1), # move back to the right prompt position + right[0], + '\033[u', # restore the cursor position + self.escape_hl_end + ))) + if output_raw: + ret.append(''.join(( + left[1], + ' ' * (width - left[-1] - right[-1]), + right[1] + ))) + else: + ret.append(left_rendered) + if output_raw: + ret.append(left[1]) + if output_width: + ret.append(left[-1]) + if len(ret) == 1: + return ret[0] + else: + return ret + + else: + return super(BashPromptRenderer, self).do_render( + side=side, + line=line, + width=width, + output_width=output_width, + output_raw=output_raw, + hl_args=hl_args, + **kwargs + ) + + +renderer = BashPromptRenderer diff --git a/powerline/renderers/shell/ksh.py b/powerline/renderers/shell/ksh.py new file mode 100644 index 0000000..0828e57 --- /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/rcsh.py b/powerline/renderers/shell/rcsh.py new file mode 100644 index 0000000..75ccb22 --- /dev/null +++ b/powerline/renderers/shell/rcsh.py @@ -0,0 +1,7 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell.readline import ReadlineRenderer + + +renderer = ReadlineRenderer diff --git a/powerline/renderers/shell/readline.py b/powerline/renderers/shell/readline.py new file mode 100644 index 0000000..a72dff0 --- /dev/null +++ b/powerline/renderers/shell/readline.py @@ -0,0 +1,14 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.renderers.shell import ShellRenderer + + +class ReadlineRenderer(ShellRenderer): + '''Renderer useful for some applications that use readline + ''' + escape_hl_start = '\x01' + escape_hl_end = '\x02' + + +renderer = ReadlineRenderer diff --git a/powerline/renderers/shell/tcsh.py b/powerline/renderers/shell/tcsh.py new file mode 100644 index 0000000..bf0697d --- /dev/null +++ b/powerline/renderers/shell/tcsh.py @@ -0,0 +1,31 @@ +# 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('^')] = '\\^' + character_translations[ord('!')] = '\\!' + + def do_render(self, **kwargs): + ret = super(TcshPromptRenderer, self).do_render(**kwargs) + nbsp = self.character_translations.get(ord(' '), ' ') + end = self.hlstyle() + assert not ret or ret.endswith(end) + if ret.endswith(nbsp + end): + # Exchange nbsp and highlight end because tcsh removes trailing + # %{%} part of the prompt for whatever reason + ret = ret[:-(len(nbsp) + len(end))] + end + nbsp + else: + # We *must* end prompt with non-%{%} sequence for the reasons + # explained above. So add nbsp if it is not already there. + ret += nbsp + return ret + + +renderer = TcshPromptRenderer diff --git a/powerline/renderers/shell/zsh.py b/powerline/renderers/shell/zsh.py new file mode 100644 index 0000000..a231512 --- /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 0000000..fc3282a --- /dev/null +++ b/powerline/renderers/tmux.py @@ -0,0 +1,81 @@ +# 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 + + +def attrs_to_tmux_attrs(attrs): + if attrs is False: + return ['nobold', 'noitalics', 'nounderscore'] + else: + ret = [] + if attrs & ATTR_BOLD: + ret += ['bold'] + else: + ret += ['nobold'] + if attrs & ATTR_ITALIC: + ret += ['italics'] + else: + ret += ['noitalics'] + if attrs & ATTR_UNDERLINE: + ret += ['underscore'] + else: + ret += ['nounderscore'] + return ret + + +class TmuxRenderer(Renderer): + '''Powerline tmux segment renderer.''' + + character_translations = Renderer.character_translations.copy() + character_translations[ord('#')] = '##[]' + + def render(self, width=None, segment_info={}, **kwargs): + if width and segment_info: + width -= segment_info.get('width_adjust', 0) + if width < 10: + width = 10 + return super(TmuxRenderer, self).render(width=width, segment_info=segment_info, **kwargs) + + def hlstyle(self, fg=None, bg=None, attrs=None, **kwargs): + '''Highlight a segment.''' + # We don’t need to explicitly reset attributes, so skip those calls + if not attrs and not bg and not fg: + return '' + tmux_attrs = [] + if fg is not None: + if fg is False or fg[0] is False: + tmux_attrs += ['fg=default'] + else: + if self.term_truecolor and fg[1]: + tmux_attrs += ['fg=#{0:06x}'.format(int(fg[1]))] + else: + tmux_attrs += ['fg=colour' + str(fg[0])] + if bg is not None: + if bg is False or bg[0] is False: + tmux_attrs += ['bg=default'] + else: + if self.term_truecolor and bg[1]: + tmux_attrs += ['bg=#{0:06x}'.format(int(bg[1]))] + else: + tmux_attrs += ['bg=colour' + str(bg[0])] + if attrs is not None: + tmux_attrs += attrs_to_tmux_attrs(attrs) + return '#[' + ','.join(tmux_attrs) + ']' + + def get_segment_info(self, segment_info, mode): + r = self.segment_info.copy() + if segment_info: + r.update(segment_info) + if 'pane_current_path' in r: + r['getcwd'] = lambda: r['pane_current_path'] + elif 'pane_id' in r: + varname = 'TMUX_PWD_' + str(r['pane_id']) + 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 0000000..a92d51c --- /dev/null +++ b/powerline/renderers/vim.py @@ -0,0 +1,188 @@ +# 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, get_vim_encoding +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, register_strwidth_error + + +vim_mode = vim_get_func('mode', rettype='unicode') +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 + self.strwidth_error_name = register_strwidth_error(self.strwidth) + self.encoding = get_vim_encoding() + + 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,): + def strwidth(self, string): + # Does not work with tabs, but neither is strwidth from default + # renderer + return vim.strwidth(string.encode(self.encoding, 'replace')) + 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(), + encoding=self.encoding, + ) + 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), + ) + statusline = statusline.encode(self.encoding, self.strwidth_error_name) + return statusline + + def reset_highlight(self): + self.hl_groups.clear() + + def hlstyle(self, fg=None, bg=None, attrs=None, **kwargs): + '''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. + attrs = attrs or 0 # Normalize `attrs` + if (fg, bg, attrs) == self.prev_highlight: + return '' + self.prev_highlight = (fg, bg, attrs) + + # We don’t need to explicitly reset attributes in vim, so skip those + # calls + if not attrs and not bg and not fg: + return '' + + if not (fg, bg, attrs) in self.hl_groups: + hl_group = { + 'ctermfg': 'NONE', + 'guifg': None, + 'ctermbg': 'NONE', + 'guibg': None, + 'attrs': ['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 attrs: + hl_group['attrs'] = [] + if attrs & ATTR_BOLD: + hl_group['attrs'].append('bold') + if attrs & ATTR_ITALIC: + hl_group['attrs'].append('italic') + if attrs & ATTR_UNDERLINE: + hl_group['attrs'].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['attrs']) + ) + self.hl_groups[(fg, bg, attrs)] = hl_group + vim.command('hi {group} ctermfg={ctermfg} guifg={guifg} guibg={guibg} ctermbg={ctermbg} cterm={attrs} gui={attrs}'.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', + attrs=','.join(hl_group['attrs']), + )) + return '%#' + self.hl_groups[(fg, bg, attrs)]['name'] + '#' + + +renderer = VimRenderer diff --git a/powerline/segment.py b/powerline/segment.py new file mode 100644 index 0000000..c83bf6f --- /dev/null +++ b/powerline/segment.py @@ -0,0 +1,450 @@ +# 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) + while parsed_segments[new_pslen - 1]['literal_contents'][1]: + new_pslen -= 1 + 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): + if segment['literal_contents'][1]: + return True + 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_groups']), + 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 + +get_fallback_segment = { + 'name': 'fallback', + 'type': 'string', + 'highlight_groups': ['background'], + 'divider_highlight_group': None, + 'before': None, + 'after': None, + 'contents': '', + 'literal_contents': (0, ''), + 'priority': None, + 'draw_soft_divider': True, + 'draw_hard_divider': True, + 'draw_inner_divider': True, + 'display_condition': always_true, + 'width': None, + 'align': None, + 'expand': None, + 'truncate': None, + 'startup': None, + 'shutdown': None, + '_rendered_raw': '', + '_rendered_hl': '', + '_len': None, + '_contents_len': None, +}.copy + + +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_groups = [function_name] + else: + highlight_groups = segment.get('highlight_groups') 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_groups': 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, + 'literal_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_groups': highlight_groups, + '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, + 'literal_contents': (0, ''), + '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 0000000..fa09e58 --- /dev/null +++ b/powerline/segments/__init__.py @@ -0,0 +1,63 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from pkgutil import extend_path +from types import MethodType + + +__path__ = extend_path(__path__, __name__) + + +class Segment(object): + '''Base class for any segment that is not a function + + Required for powerline.lint.inspect to work properly: it defines methods for + omitting existing or adding new arguments. + + .. note:: + Until python-3.4 ``inspect.getargspec`` does not support querying + callable classes for arguments of their ``__call__`` method, requiring + to use this method directly (i.e. before 3.4 you should write + ``getargspec(obj.__call__)`` in place of ``getargspec(obj)``). + ''' + if sys.version_info < (3, 4): + def argspecobjs(self): + yield '__call__', self.__call__ + else: + def argspecobjs(self): + yield '__call__', self + + argspecobjs.__doc__ = ( + '''Return a list of valid arguments for inspect.getargspec + + Used to determine function arguments. + ''' + ) + + def omitted_args(self, name, method): + '''List arguments which should be omitted + + Returns a tuple with indexes of omitted arguments. + + .. note::``segment_info``, ``create_watcher`` and ``pl`` will be omitted + regardless of the below return (for ``segment_info`` and + ``create_watcher``: only if object was marked to require segment + info or filesystem watcher). + ''' + if isinstance(self.__call__, MethodType): + return (0,) + else: + return () + + @staticmethod + def additional_args(): + '''Returns a list of (additional argument name[, default value]) tuples. + ''' + return () + + +def with_docstring(instance, doc): + instance.__doc__ = doc + return instance diff --git a/powerline/segments/common/__init__.py b/powerline/segments/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/powerline/segments/common/bat.py b/powerline/segments/common/bat.py new file mode 100644 index 0000000..c892f62 --- /dev/null +++ b/powerline/segments/common/bat.py @@ -0,0 +1,302 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import re + +from powerline.lib.shell import run_cmd + + +def _fetch_battery_info(pl): + try: + import dbus + except ImportError: + pl.debug('Not using DBUS+UPower as dbus is not available') + else: + try: + bus = dbus.SystemBus() + except Exception as e: + pl.exception('Failed to connect to system bus: {0}', str(e)) + else: + interface = 'org.freedesktop.UPower' + try: + up = bus.get_object(interface, '/org/freedesktop/UPower') + except dbus.exceptions.DBusException as e: + if getattr(e, '_dbus_error_name', '').endswith('ServiceUnknown'): + pl.debug('Not using DBUS+UPower as UPower is not available via dbus') + else: + pl.exception('Failed to get UPower service with dbus: {0}', str(e)) + else: + devinterface = 'org.freedesktop.DBus.Properties' + devtype_name = interface + '.Device' + devices = [] + for devpath in up.EnumerateDevices(dbus_interface=interface): + dev = bus.get_object(interface, devpath) + devget = lambda what: dev.Get( + devtype_name, + what, + dbus_interface=devinterface + ) + if int(devget('Type')) != 2: + pl.debug('Not using DBUS+UPower with {0}: invalid type', devpath) + continue + if not bool(devget('IsPresent')): + pl.debug('Not using DBUS+UPower with {0}: not present', devpath) + continue + if not bool(devget('PowerSupply')): + pl.debug('Not using DBUS+UPower with {0}: not a power supply', devpath) + continue + devices.append(devpath) + pl.debug('Using DBUS+UPower with {0}', devpath) + if devices: + def _flatten_battery(pl): + energy = 0.0 + energy_full = 0.0 + state = True + for devpath in devices: + dev = bus.get_object(interface, devpath) + energy_full += float( + dbus.Interface(dev, dbus_interface=devinterface).Get( + devtype_name, + 'EnergyFull' + ), + ) + energy += float( + dbus.Interface(dev, dbus_interface=devinterface).Get( + devtype_name, + 'Energy' + ), + ) + state &= dbus.Interface(dev, dbus_interface=devinterface).Get( + devtype_name, + 'State' + ) != 2 + if energy_full > 0: + return (energy * 100.0 / energy_full), state + else: + return 0.0, state + return _flatten_battery + pl.debug('Not using DBUS+UPower as no batteries were found') + + if os.path.isdir('/sys/class/power_supply'): + # ENERGY_* attributes represents capacity in µWh only. + # CHARGE_* attributes represents capacity in µAh only. + linux_capacity_units = ('energy', 'charge') + linux_energy_full_fmt = '/sys/class/power_supply/{0}/{1}_full' + linux_energy_fmt = '/sys/class/power_supply/{0}/{1}_now' + linux_status_fmt = '/sys/class/power_supply/{0}/status' + devices = [] + for linux_supplier in os.listdir('/sys/class/power_supply'): + for unit in linux_capacity_units: + energy_path = linux_energy_fmt.format(linux_supplier, unit) + if not os.path.exists(energy_path): + continue + pl.debug('Using /sys/class/power_supply with battery {0} and unit {1}', + linux_supplier, unit) + devices.append((linux_supplier, unit)) + break # energy or charge, not both + if devices: + def _get_battery_status(pl): + energy = 0.0 + energy_full = 0.0 + state = True + for device, unit in devices: + with open(linux_energy_full_fmt.format(device, unit), 'r') as f: + energy_full += int(float(f.readline().split()[0])) + with open(linux_energy_fmt.format(device, unit), 'r') as f: + energy += int(float(f.readline().split()[0])) + try: + with open(linux_status_fmt.format(device), 'r') as f: + state &= (f.readline().strip() != 'Discharging') + except IOError: + state = None + return (energy * 100.0 / energy_full), state + return _get_battery_status + pl.debug('Not using /sys/class/power_supply as no batteries were found') + else: + pl.debug("Checking for first capacity battery percentage") + for batt in os.listdir('/sys/class/power_supply'): + if os.path.exists('/sys/class/power_supply/{0}/capacity'.format(batt)): + def _get_battery_perc(pl): + state = True + with open('/sys/class/power_supply/{0}/capacity'.format(batt), 'r') as f: + perc = int(f.readline().split()[0]) + try: + with open(linux_status_fmt.format(batt), 'r') as f: + state &= (f.readline().strip() != 'Discharging') + except IOError: + state = None + return perc, state + return _get_battery_perc + else: + pl.debug('Not using /sys/class/power_supply: no directory') + + try: + from shutil import which # Python-3.3 and later + except ImportError: + pl.info('Using dumb “which” which only checks for file in /usr/bin') + which = lambda f: (lambda fp: os.path.exists(fp) and fp)(os.path.join('/usr/bin', f)) + + if which('pmset'): + pl.debug('Using pmset') + + BATTERY_PERCENT_RE = re.compile(r'(\d+)%') + + def _get_battery_status(pl): + battery_summary = run_cmd(pl, ['pmset', '-g', 'batt']) + battery_percent = BATTERY_PERCENT_RE.search(battery_summary).group(1) + ac_charging = 'AC' in battery_summary + return int(battery_percent), ac_charging + return _get_battery_status + else: + pl.debug('Not using pmset: executable not found') + + if sys.platform.startswith('win') or sys.platform == 'cygwin': + # From http://stackoverflow.com/a/21083571/273566, reworked + try: + from win32com.client import GetObject + except ImportError: + pl.debug('Not using win32com.client as it is not available') + else: + try: + wmi = GetObject('winmgmts:') + except Exception as e: + pl.exception('Failed to run GetObject from win32com.client: {0}', str(e)) + else: + for battery in wmi.InstancesOf('Win32_Battery'): + pl.debug('Using win32com.client with Win32_Battery') + + def _get_battery_status(pl): + # http://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx + return battery.EstimatedChargeRemaining, battery.BatteryStatus == 6 + + return _get_battery_status + pl.debug('Not using win32com.client as no batteries were found') + from ctypes import Structure, c_byte, c_ulong, byref + if sys.platform == 'cygwin': + pl.debug('Using cdll to communicate with kernel32 (Cygwin)') + from ctypes import cdll + library_loader = cdll + else: + pl.debug('Using windll to communicate with kernel32 (Windows)') + from ctypes import windll + library_loader = windll + + class PowerClass(Structure): + _fields_ = [ + ('ACLineStatus', c_byte), + ('BatteryFlag', c_byte), + ('BatteryLifePercent', c_byte), + ('Reserved1', c_byte), + ('BatteryLifeTime', c_ulong), + ('BatteryFullLifeTime', c_ulong) + ] + + def _get_battery_status(pl): + powerclass = PowerClass() + result = library_loader.kernel32.GetSystemPowerStatus(byref(powerclass)) + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa372693(v=vs.85).aspx + if result: + return None + return powerclass.BatteryLifePercent, powerclass.ACLineStatus == 1 + + if _get_battery_status() is None: + pl.debug('Not using GetSystemPowerStatus because it failed') + else: + pl.debug('Using GetSystemPowerStatus') + + return _get_battery_status + + raise NotImplementedError + + +def _get_battery_status(pl): + global _get_battery_status + + def _failing_get_status(pl): + raise NotImplementedError + + try: + _get_battery_status = _fetch_battery_info(pl) + except NotImplementedError: + _get_battery_status = _failing_get_status + except Exception as e: + pl.exception('Exception while obtaining battery status: {0}', str(e)) + _get_battery_status = _failing_get_status + return _get_battery_status(pl) + + +def battery(pl, format='{ac_state} {capacity:3.0%}', steps=5, gamify=False, full_heart='O', empty_heart='O', online='C', offline=' '): + '''Return battery charge status. + + :param str format: + Percent format in case gamify is False. Format arguments: ``ac_state`` + which is equal to either ``online`` or ``offline`` string arguments and + ``capacity`` which is equal to current battery capacity in interval [0, + 100]. + :param int steps: + Number of discrete steps to show between 0% and 100% capacity if gamify + is True. + :param bool gamify: + Measure in hearts (♥) instead of percentages. For full hearts + ``battery_full`` highlighting group is preferred, for empty hearts there + is ``battery_empty``. ``battery_online`` or ``battery_offline`` group + will be used for leading segment containing ``online`` or ``offline`` + argument contents. + :param str full_heart: + Heart displayed for “full” part of battery. + :param str empty_heart: + Heart displayed for “used” part of battery. It is also displayed using + another gradient level and highlighting group, so it is OK for it to be + the same as full_heart as long as necessary highlighting groups are + defined. + :param str online: + Symbol used if computer is connected to a power supply. + :param str offline: + Symbol used if computer is not connected to a power supply. + + ``battery_gradient`` and ``battery`` groups are used in any case, first is + preferred. + + Highlight groups used: ``battery_full`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_empty`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_online`` or ``battery_ac_state`` or ``battery_gradient`` (gradient) or ``battery``, ``battery_offline`` or ``battery_ac_state`` or ``battery_gradient`` (gradient) or ``battery``. + ''' + try: + capacity, ac_powered = _get_battery_status(pl) + except NotImplementedError: + pl.info('Unable to get battery status.') + return None + + ret = [] + if gamify: + denom = int(steps) + numer = int(denom * capacity / 100) + ret.append({ + 'contents': online if ac_powered else offline, + 'draw_inner_divider': False, + 'highlight_groups': ['battery_online' if ac_powered else 'battery_offline', 'battery_ac_state', 'battery_gradient', 'battery'], + 'gradient_level': 0, + }) + ret.append({ + 'contents': full_heart * numer, + 'draw_inner_divider': False, + 'highlight_groups': ['battery_full', 'battery_gradient', 'battery'], + # Using zero as “nothing to worry about”: it is least alert color. + 'gradient_level': 0, + }) + ret.append({ + 'contents': empty_heart * (denom - numer), + 'draw_inner_divider': False, + 'highlight_groups': ['battery_empty', 'battery_gradient', 'battery'], + # Using a hundred as it is most alert color. + 'gradient_level': 100, + }) + else: + ret.append({ + 'contents': format.format(ac_state=(online if ac_powered else offline), capacity=(capacity / 100.0)), + 'highlight_groups': ['battery_gradient', 'battery'], + # Gradients are “least alert – most alert” by default, capacity has + # the opposite semantics. + 'gradient_level': 100 - capacity, + }) + return ret diff --git a/powerline/segments/common/env.py b/powerline/segments/common/env.py new file mode 100644 index 0000000..bbfe3e2 --- /dev/null +++ b/powerline/segments/common/env.py @@ -0,0 +1,201 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from powerline.lib.unicode import out_u +from powerline.theme import requires_segment_info +from powerline.segments import Segment, with_docstring + + +@requires_segment_info +def environment(pl, segment_info, variable=None): + '''Return the value of any defined environment variable + + :param string variable: + The environment variable to return if found + ''' + return segment_info['environ'].get(variable, None) + + +@requires_segment_info +def virtualenv(pl, segment_info, ignore_venv=False, ignore_conda=False, ignored_names=("venv", ".venv")): + '''Return the name of the current Python or conda virtualenv. + :param list ignored_names: + Names of venvs to ignore. Will then get the name of the venv by ascending to the parent directory + :param bool ignore_venv: + Whether to ignore virtual environments. Default is False. + :param bool ignore_conda: + Whether to ignore conda environments. Default is False. + ''' + if not ignore_venv: + for candidate in reversed(segment_info['environ'].get('VIRTUAL_ENV', '').split("/")): + if candidate and candidate not in ignored_names: + return candidate + if not ignore_conda: + for candidate in reversed(segment_info['environ'].get('CONDA_DEFAULT_ENV', '').split("/")): + if candidate and candidate not in ignored_names: + return candidate + return None + + +@requires_segment_info +class CwdSegment(Segment): + def argspecobjs(self): + for obj in super(CwdSegment, self).argspecobjs(): + yield obj + yield 'get_shortened_path', self.get_shortened_path + + def omitted_args(self, name, method): + if method is self.get_shortened_path: + return () + else: + return super(CwdSegment, self).omitted_args(name, method) + + def get_shortened_path(self, pl, segment_info, shorten_home=True, **kwargs): + try: + path = out_u(segment_info['getcwd']()) + except OSError as e: + if e.errno == 2: + # user most probably deleted the directory + # this happens when removing files from Mercurial repos for example + pl.warn('Current directory not found') + return '[not found]' + else: + raise + if shorten_home: + home = segment_info['home'] + if home: + home = out_u(home) + if path.startswith(home): + path = '~' + path[len(home):] + return path + + def __call__(self, pl, segment_info, + dir_shorten_len=None, + dir_limit_depth=None, + use_path_separator=False, + ellipsis='...', + **kwargs): + cwd = self.get_shortened_path(pl, segment_info, **kwargs) + cwd_split = cwd.split(os.sep) + cwd_split_len = len(cwd_split) + cwd = [i[0:dir_shorten_len] if dir_shorten_len and i else i for i in cwd_split[:-1]] + [cwd_split[-1]] + if dir_limit_depth and cwd_split_len > dir_limit_depth + 1: + del(cwd[0:-dir_limit_depth]) + if ellipsis is not None: + cwd.insert(0, ellipsis) + ret = [] + if not cwd[0]: + cwd[0] = '/' + draw_inner_divider = not use_path_separator + for part in cwd: + if not part: + continue + if use_path_separator: + part += os.sep + ret.append({ + 'contents': part, + 'divider_highlight_group': 'cwd:divider', + 'draw_inner_divider': draw_inner_divider, + }) + ret[-1]['highlight_groups'] = ['cwd:current_folder', 'cwd'] + if use_path_separator: + ret[-1]['contents'] = ret[-1]['contents'][:-1] + if len(ret) > 1 and ret[0]['contents'][0] == os.sep: + ret[0]['contents'] = ret[0]['contents'][1:] + return ret + + +cwd = with_docstring(CwdSegment(), +'''Return the current working directory. + +Returns a segment list to create a breadcrumb-like effect. + +:param int dir_shorten_len: + shorten parent directory names to this length (e.g. + :file:`/long/path/to/powerline` → :file:`/l/p/t/powerline`) +:param int dir_limit_depth: + limit directory depth to this number (e.g. + :file:`/long/path/to/powerline` → :file:`⋯/to/powerline`) +:param bool use_path_separator: + Use path separator in place of soft divider. +:param bool shorten_home: + Shorten home directory to ``~``. +:param str ellipsis: + Specifies what to use in place of omitted directories. Use None to not + show this subsegment at all. + +Divider highlight group used: ``cwd:divider``. + +Highlight groups used: ``cwd:current_folder`` or ``cwd``. It is recommended to define all highlight groups. +''') + + +try: + import psutil + + # psutil-2.0.0: psutil.Process.username is unbound method + if callable(psutil.Process.username): + def _get_user(): + return psutil.Process(os.getpid()).username() + # pre psutil-2.0.0: psutil.Process.username has type property + else: + def _get_user(): + return psutil.Process(os.getpid()).username +except ImportError: + try: + import pwd + except ImportError: + from getpass import getuser as _get_user + else: + try: + from os import geteuid as getuid + except ImportError: + from os import getuid + + def _get_user(): + return pwd.getpwuid(getuid()).pw_name + + +username = False +# os.geteuid is not available on windows +_geteuid = getattr(os, 'geteuid', lambda: 1) + + +@requires_segment_info +def user(pl, segment_info, hide_user=None, hide_domain=False): + '''Return the current user. + + :param str hide_user: + Omit showing segment for users with names equal to this string. + :param bool hide_domain: + Drop domain component if it exists in a username (delimited by '@'). + + Highlights the user with the ``superuser`` if the effective user ID is 0. + + Highlight groups used: ``superuser`` or ``user``. It is recommended to define all highlight groups. + ''' + global username + if ( + segment_info['environ'].get('_POWERLINE_RUNNING_SHELL_TESTS') + == 'ee5bcdc6-b749-11e7-9456-50465d597777' + ): + return 'user' + if username is False: + username = _get_user() + if username is None: + pl.warn('Failed to get username') + return None + if username == hide_user: + return None + if hide_domain: + try: + username = username[:username.index('@')] + except ValueError: + pass + euid = _geteuid() + return [{ + 'contents': username, + 'highlight_groups': ['user'] if euid != 0 else ['superuser', 'user'], + }] diff --git a/powerline/segments/common/mail.py b/powerline/segments/common/mail.py new file mode 100644 index 0000000..8202492 --- /dev/null +++ b/powerline/segments/common/mail.py @@ -0,0 +1,78 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re + +from imaplib import IMAP4_SSL_PORT, IMAP4_SSL, IMAP4 +from collections import namedtuple + +from powerline.lib.threaded import KwThreadedSegment +from powerline.segments import with_docstring + + +_IMAPKey = namedtuple('Key', 'username password server port folder use_ssl') + + +class EmailIMAPSegment(KwThreadedSegment): + interval = 60 + + @staticmethod + def key(username, password, server='imap.gmail.com', port=IMAP4_SSL_PORT, folder='INBOX', use_ssl=None, **kwargs): + if use_ssl is None: + use_ssl = (port == IMAP4_SSL_PORT) + return _IMAPKey(username, password, server, port, folder, use_ssl) + + def compute_state(self, key): + if not key.username or not key.password: + self.warn('Username and password are not configured') + return None + if key.use_ssl: + mail = IMAP4_SSL(key.server, key.port) + else: + mail = IMAP4(key.server, key.port) + mail.login(key.username, key.password) + rc, message = mail.status(key.folder, '(UNSEEN)') + unread_str = message[0].decode('utf-8') + unread_count = int(re.search('UNSEEN (\d+)', unread_str).group(1)) + return unread_count + + @staticmethod + def render_one(unread_count, max_msgs=None, **kwargs): + if not unread_count: + return None + elif type(unread_count) != int or not max_msgs: + return [{ + 'contents': str(unread_count), + 'highlight_groups': ['email_alert'], + }] + else: + return [{ + 'contents': str(unread_count), + 'highlight_groups': ['email_alert_gradient', 'email_alert'], + 'gradient_level': min(unread_count * 100.0 / max_msgs, 100), + }] + + +email_imap_alert = with_docstring(EmailIMAPSegment(), +('''Return unread e-mail count for IMAP servers. + +:param str username: + login username +:param str password: + login password +:param str server: + e-mail server +:param int port: + e-mail server port +:param str folder: + folder to check for e-mails +:param int max_msgs: + Maximum number of messages. If there are more messages then max_msgs then it + will use gradient level equal to 100, otherwise gradient level is equal to + ``100 * msgs_num / max_msgs``. If not present gradient is not computed. +:param bool use_ssl: + If ``True`` then use SSL connection. If ``False`` then do not use it. + Default is ``True`` if port is equal to {ssl_port} and ``False`` otherwise. + +Highlight groups used: ``email_alert_gradient`` (gradient), ``email_alert``. +''').format(ssl_port=IMAP4_SSL_PORT)) diff --git a/powerline/segments/common/net.py b/powerline/segments/common/net.py new file mode 100644 index 0000000..b5d9062 --- /dev/null +++ b/powerline/segments/common/net.py @@ -0,0 +1,315 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re +import os +import socket + +from powerline.lib.url import urllib_read +from powerline.lib.threaded import ThreadedSegment, KwThreadedSegment +from powerline.lib.monotonic import monotonic +from powerline.lib.humanize_bytes import humanize_bytes +from powerline.segments import with_docstring +from powerline.theme import requires_segment_info + + +@requires_segment_info +def hostname(pl, segment_info, only_if_ssh=False, exclude_domain=False): + '''Return the current hostname. + + :param bool only_if_ssh: + only return the hostname if currently in an SSH session + :param bool exclude_domain: + return the hostname without domain if there is one + ''' + if ( + segment_info['environ'].get('_POWERLINE_RUNNING_SHELL_TESTS') + == 'ee5bcdc6-b749-11e7-9456-50465d597777' + ): + return 'hostname' + if only_if_ssh and not segment_info['environ'].get('SSH_CLIENT'): + return None + if exclude_domain: + return socket.gethostname().split('.')[0] + return socket.gethostname() + + +def _external_ip(query_url='http://ipv4.icanhazip.com/'): + return urllib_read(query_url).strip() + + +class ExternalIpSegment(ThreadedSegment): + interval = 300 + + def set_state(self, query_url='http://ipv4.icanhazip.com/', **kwargs): + self.query_url = query_url + super(ExternalIpSegment, self).set_state(**kwargs) + + def update(self, old_ip): + return _external_ip(query_url=self.query_url) + + def render(self, ip, **kwargs): + if not ip: + return None + return [{'contents': ip, 'divider_highlight_group': 'background:divider'}] + + +external_ip = with_docstring(ExternalIpSegment(), +'''Return external IP address. + +:param str query_url: + URI to query for IP address, should return only the IP address as a text string + + Suggested URIs: + + * http://ipv4.icanhazip.com/ + * http://ipv6.icanhazip.com/ + * http://icanhazip.com/ (returns IPv6 address if available, else IPv4) + +Divider highlight group used: ``background:divider``. +''') + + +try: + import netifaces +except ImportError: + def internal_ip(pl, interface='auto', ipv=4): + return None +else: + _interface_starts = { + 'eth': 10, # Regular ethernet adapters : eth1 + 'enp': 10, # Regular ethernet adapters, Gentoo : enp2s0 + 'en': 10, # OS X : en0 + 'ath': 9, # Atheros WiFi adapters : ath0 + 'wlan': 9, # Other WiFi adapters : wlan1 + 'wlp': 9, # Other WiFi adapters, Gentoo : wlp5s0 + 'teredo': 1, # miredo interface : teredo + 'lo': -10, # Loopback interface : lo + 'docker': -5, # Docker bridge interface : docker0 + 'vmnet': -5, # VMWare bridge interface : vmnet1 + 'vboxnet': -5, # VirtualBox bridge interface : vboxnet0 + } + + _interface_start_re = re.compile(r'^([a-z]+?)(\d|$)') + + def _interface_key(interface): + match = _interface_start_re.match(interface) + if match: + try: + base = _interface_starts[match.group(1)] * 100 + except KeyError: + base = 500 + if match.group(2): + return base - int(match.group(2)) + else: + return base + else: + return 0 + + def internal_ip(pl, interface='auto', ipv=4): + family = netifaces.AF_INET6 if ipv == 6 else netifaces.AF_INET + if interface == 'auto': + try: + interface = next(iter(sorted(netifaces.interfaces(), key=_interface_key, reverse=True))) + except StopIteration: + pl.info('No network interfaces found') + return None + elif interface == 'default_gateway': + try: + interface = netifaces.gateways()['default'][family][1] + except KeyError: + pl.info('No default gateway found for IPv{0}', ipv) + return None + addrs = netifaces.ifaddresses(interface) + try: + return addrs[family][0]['addr'] + except (KeyError, IndexError): + pl.info("No IPv{0} address found for interface {1}", ipv, interface) + return None + + +internal_ip = with_docstring(internal_ip, +'''Return internal IP address + +Requires ``netifaces`` module to work properly. + +:param str interface: + Interface on which IP will be checked. Use ``auto`` to automatically + detect interface. In this case interfaces with lower numbers will be + preferred over interfaces with similar names. Order of preference based on + names: + + #. ``eth`` and ``enp`` followed by number or the end of string. + #. ``ath``, ``wlan`` and ``wlp`` followed by number or the end of string. + #. ``teredo`` followed by number or the end of string. + #. Any other interface that is not ``lo*``. + #. ``lo`` followed by number or the end of string. + + Use ``default_gateway`` to detect the interface based on the machine's + `default gateway `_ (i.e., + the router to which it is connected). + +:param int ipv: + 4 or 6 for ipv4 and ipv6 respectively, depending on which IP address you + need exactly. +''') + + +try: + import psutil + + def _get_bytes(interface): + try: + io_counters = psutil.net_io_counters(pernic=True) + except AttributeError: + io_counters = psutil.network_io_counters(pernic=True) + if_io = io_counters.get(interface) + if not if_io: + return None + return if_io.bytes_recv, if_io.bytes_sent + + def _get_interfaces(): + try: + io_counters = psutil.net_io_counters(pernic=True) + except AttributeError: + io_counters = psutil.network_io_counters(pernic=True) + for interface, data in io_counters.items(): + if data: + yield interface, data.bytes_recv, data.bytes_sent +except ImportError: + def _get_bytes(interface): + with open('/sys/class/net/{interface}/statistics/rx_bytes'.format(interface=interface), 'rb') as file_obj: + rx = int(file_obj.read()) + with open('/sys/class/net/{interface}/statistics/tx_bytes'.format(interface=interface), 'rb') as file_obj: + tx = int(file_obj.read()) + return (rx, tx) + + def _get_interfaces(): + for interface in os.listdir('/sys/class/net'): + x = _get_bytes(interface) + if x is not None: + yield interface, x[0], x[1] + + +class NetworkLoadSegment(KwThreadedSegment): + interfaces = {} + replace_num_pat = re.compile(r'[a-zA-Z]+') + + @staticmethod + def key(interface='auto', **kwargs): + return interface + + def compute_state(self, interface): + if interface == 'auto': + proc_exists = getattr(self, 'proc_exists', None) + if proc_exists is None: + proc_exists = self.proc_exists = os.path.exists('/proc/net/route') + if proc_exists: + # Look for default interface in routing table + with open('/proc/net/route', 'rb') as f: + for line in f.readlines(): + parts = line.split() + if len(parts) > 1: + iface, destination = parts[:2] + if not destination.replace(b'0', b''): + interface = iface.decode('utf-8') + break + if interface == 'auto': + # Choose interface with most total activity, excluding some + # well known interface names + interface, total = 'eth0', -1 + for name, rx, tx in _get_interfaces(): + base = self.replace_num_pat.match(name) + if None in (base, rx, tx) or base.group() in ('lo', 'vmnet', 'sit'): + continue + activity = rx + tx + if activity > total: + total = activity + interface = name + + try: + idata = self.interfaces[interface] + try: + idata['prev'] = idata['last'] + except KeyError: + pass + except KeyError: + idata = {} + if self.run_once: + idata['prev'] = (monotonic(), _get_bytes(interface)) + self.shutdown_event.wait(self.interval) + self.interfaces[interface] = idata + + idata['last'] = (monotonic(), _get_bytes(interface)) + return idata.copy() + + def render_one(self, idata, recv_format='DL {value:>8}', sent_format='UL {value:>8}', suffix='B/s', si_prefix=False, **kwargs): + if not idata or 'prev' not in idata: + return None + + t1, b1 = idata['prev'] + t2, b2 = idata['last'] + measure_interval = t2 - t1 + + if None in (b1, b2): + return None + + r = [] + for i, key in zip((0, 1), ('recv', 'sent')): + format = locals()[key + '_format'] + try: + value = (b2[i] - b1[i]) / measure_interval + except ZeroDivisionError: + self.warn('Measure interval zero.') + value = 0 + max_key = key + '_max' + is_gradient = max_key in kwargs + hl_groups = ['network_load_' + key, 'network_load'] + if is_gradient: + hl_groups[:0] = (group + '_gradient' for group in hl_groups) + r.append({ + 'contents': format.format(value=humanize_bytes(value, suffix, si_prefix)), + 'divider_highlight_group': 'network_load:divider', + 'highlight_groups': hl_groups, + }) + if is_gradient: + max = kwargs[max_key] + if value >= max: + r[-1]['gradient_level'] = 100 + else: + r[-1]['gradient_level'] = value * 100.0 / max + + return r + + +network_load = with_docstring(NetworkLoadSegment(), +'''Return the network load. + +Uses the ``psutil`` module if available for multi-platform compatibility, +falls back to reading +:file:`/sys/class/net/{interface}/statistics/{rx,tx}_bytes`. + +:param str interface: + Network interface to measure (use the special value "auto" to have powerline + try to auto-detect the network interface). +:param str suffix: + String appended to each load string. +:param bool si_prefix: + Use SI prefix, e.g. MB instead of MiB. +:param str recv_format: + Format string that determines how download speed should look like. Receives + ``value`` as argument. +:param str sent_format: + Format string that determines how upload speed should look like. Receives + ``value`` as argument. +:param float recv_max: + Maximum number of received bytes per second. Is only used to compute + gradient level. +:param float sent_max: + Maximum number of sent bytes per second. Is only used to compute gradient + level. + +Divider highlight group used: ``network_load:divider``. + +Highlight groups used: ``network_load_sent_gradient`` (gradient) or ``network_load_recv_gradient`` (gradient) or ``network_load_gradient`` (gradient), ``network_load_sent`` or ``network_load_recv`` or ``network_load``. +''') diff --git a/powerline/segments/common/players.py b/powerline/segments/common/players.py new file mode 100644 index 0000000..f43db0c --- /dev/null +++ b/powerline/segments/common/players.py @@ -0,0 +1,636 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import re + +from powerline.lib.shell import asrun, run_cmd +from powerline.lib.unicode import out_u +from powerline.segments import Segment, with_docstring + + +STATE_SYMBOLS = { + 'fallback': '', + 'play': '>', + 'pause': '~', + 'stop': 'X', +} + + +def _convert_state(state): + '''Guess player state''' + state = state.lower() + if 'play' in state: + return 'play' + if 'pause' in state: + return 'pause' + if 'stop' in state: + return 'stop' + return 'fallback' + + +def _convert_seconds(seconds): + '''Convert seconds to minutes:seconds format''' + if isinstance(seconds, str): + seconds = seconds.replace(",",".") + return '{0:.0f}:{1:02.0f}'.format(*divmod(float(seconds), 60)) + + +class PlayerSegment(Segment): + def __call__(self, format='{state_symbol} {artist} - {title} ({total})', state_symbols=STATE_SYMBOLS, **kwargs): + stats = { + 'state': 'fallback', + 'album': None, + 'artist': None, + 'title': None, + 'elapsed': None, + 'total': None, + } + func_stats = self.get_player_status(**kwargs) + if not func_stats: + return None + stats.update(func_stats) + stats['state_symbol'] = state_symbols.get(stats['state']) + return [{ + 'contents': format.format(**stats), + 'highlight_groups': ['player_' + (stats['state'] or 'fallback'), 'player'], + }] + + def get_player_status(self, pl): + pass + + def argspecobjs(self): + for ret in super(PlayerSegment, self).argspecobjs(): + yield ret + yield 'get_player_status', self.get_player_status + + def omitted_args(self, name, method): + return () + + +_common_args = ''' +This player segment should be added like this: + +.. code-block:: json + + {{ + "function": "powerline.segments.common.players.{0}", + "name": "player" + }} + +(with additional ``"args": {{…}}`` if needed). + +Highlight groups used: ``player_fallback`` or ``player``, ``player_play`` or ``player``, ``player_pause`` or ``player``, ``player_stop`` or ``player``. + +:param str format: + Format used for displaying data from player. Should be a str.format-like + string with the following keyword parameters: + + +------------+-------------------------------------------------------------+ + |Parameter |Description | + +============+=============================================================+ + |state_symbol|Symbol displayed for play/pause/stop states. There is also | + | |“fallback” state used in case function failed to get player | + | |state. For this state symbol is by default empty. All | + | |symbols are defined in ``state_symbols`` argument. | + +------------+-------------------------------------------------------------+ + |album |Album that is currently played. | + +------------+-------------------------------------------------------------+ + |artist |Artist whose song is currently played | + +------------+-------------------------------------------------------------+ + |title |Currently played composition. | + +------------+-------------------------------------------------------------+ + |elapsed |Composition duration in format M:SS (minutes:seconds). | + +------------+-------------------------------------------------------------+ + |total |Composition length in format M:SS. | + +------------+-------------------------------------------------------------+ +:param dict state_symbols: + Symbols used for displaying state. Must contain all of the following keys: + + ======== ======================================================== + Key Description + ======== ======================================================== + play Displayed when player is playing. + pause Displayed when player is paused. + stop Displayed when player is not playing anything. + fallback Displayed if state is not one of the above or not known. + ======== ======================================================== +''' + + +_player = with_docstring(PlayerSegment(), _common_args.format('_player')) + + +class CmusPlayerSegment(PlayerSegment): + def get_player_status(self, pl): + '''Return cmus player information. + + cmus-remote -Q returns data with multi-level information i.e. + status playing + file + 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 = _convert_state(now_playing.get('status')) + return { + 'state': state, + 'album': now_playing.get('album'), + 'artist': now_playing.get('artist'), + 'title': now_playing.get('title'), + 'elapsed': _convert_seconds(now_playing.get('position', 0)), + 'total': _convert_seconds(now_playing.get('duration', 0)), + } + + +cmus = with_docstring(CmusPlayerSegment(), +('''Return CMUS player information + +Requires cmus-remote command be accessible from $PATH. + +{0} +''').format(_common_args.format('cmus'))) + + +class MpdPlayerSegment(PlayerSegment): + def get_player_status(self, pl, host='localhost', password=None, port=6600): + try: + import mpd + except ImportError: + if password: + host = password + '@' + host + now_playing = run_cmd(pl, [ + 'mpc', + '-h', host, + '-p', str(port) + ], strip=False) + album = run_cmd(pl, [ + 'mpc', 'current', + '-f', '%album%', + '-h', host, + '-p', str(port) + ]) + if not now_playing or now_playing.count("\n") != 3: + return + now_playing = re.match( + r"(.*) - (.*)\n\[([a-z]+)\] +[#0-9\/]+ +([0-9\:]+)\/([0-9\:]+)", + now_playing + ) + return { + 'state': _convert_state(now_playing[3]), + 'album': album, + 'artist': now_playing[1], + 'title': now_playing[2], + 'elapsed': now_playing[4], + 'total': now_playing[5] + } + else: + try: + client = mpd.MPDClient(use_unicode=True) + except TypeError: + # python-mpd 1.x does not support use_unicode + client = mpd.MPDClient() + client.connect(host, port) + if password: + client.password(password) + now_playing = client.currentsong() + if not now_playing: + return + status = client.status() + client.close() + client.disconnect() + return { + 'state': status.get('state'), + 'album': now_playing.get('album'), + 'artist': now_playing.get('artist'), + 'title': now_playing.get('title'), + 'elapsed': _convert_seconds(status.get('elapsed', 0)), + 'total': _convert_seconds(now_playing.get('time', 0)), + } + + +mpd = with_docstring(MpdPlayerSegment(), +('''Return Music Player Daemon information + +Requires ``mpd`` Python module (e.g. |python-mpd2|_ or |python-mpd|_ Python +package) or alternatively the ``mpc`` command to be accessible from $PATH. + +.. |python-mpd| replace:: ``python-mpd`` +.. _python-mpd: https://pypi.python.org/pypi/python-mpd + +.. |python-mpd2| replace:: ``python-mpd2`` +.. _python-mpd2: https://pypi.python.org/pypi/python-mpd2 + +{0} +:param str host: + Host on which mpd runs. +:param str password: + Password used for connecting to daemon. +:param int port: + Port which should be connected to. +''').format(_common_args.format('mpd'))) + + +try: + import dbus +except ImportError: + def _get_dbus_player_status(pl, player_name, **kwargs): + pl.error('Could not add {0} segment: requires dbus module', player_name) + return +else: + def _get_dbus_player_status(pl, + bus_name=None, + iface_prop='org.freedesktop.DBus.Properties', + iface_player='org.mpris.MediaPlayer2.Player', + player_path='/org/mpris/MediaPlayer2', + player_name='player'): + bus = dbus.SessionBus() + + if bus_name is None: + for service in bus.list_names(): + if re.match('org.mpris.MediaPlayer2.', service): + bus_name = service + break + + try: + player = bus.get_object(bus_name, player_path) + iface = dbus.Interface(player, iface_prop) + info = iface.Get(iface_player, 'Metadata') + status = iface.Get(iface_player, 'PlaybackStatus') + except dbus.exceptions.DBusException: + return + if not info: + return + + try: + elapsed = iface.Get(iface_player, 'Position') + except dbus.exceptions.DBusException: + pl.warning('Missing player elapsed time') + elapsed = None + else: + elapsed = _convert_seconds(elapsed / 1e6) + album = info.get('xesam:album') + title = info.get('xesam:title') + artist = info.get('xesam:artist') + state = _convert_state(status) + if album: + album = out_u(album) + if title: + title = out_u(title) + if artist: + artist = out_u(artist[0]) + + length = info.get('mpris:length') + # avoid parsing `None` length values, that would + # raise an error otherwise + parsed_length = length and _convert_seconds(length / 1e6) + + return { + 'state': state, + 'album': album, + 'artist': artist, + 'title': title, + 'elapsed': elapsed, + 'total': parsed_length, + } + + +class DbusPlayerSegment(PlayerSegment): + get_player_status = staticmethod(_get_dbus_player_status) + + +dbus_player = with_docstring(DbusPlayerSegment(), +('''Return generic dbus player state + +Requires ``dbus`` python module. Only for players that support specific protocol + (e.g. like :py:func:`spotify` and :py:func:`clementine`). + +{0} +:param str player_name: + Player name. Used in error messages only. +:param str bus_name: + Dbus bus name. +:param str player_path: + Path to the player on the given bus. +:param str iface_prop: + Interface properties name for use with dbus.Interface. +:param str iface_player: + Player name. +''').format(_common_args.format('dbus_player'))) + + +class SpotifyDbusPlayerSegment(PlayerSegment): + def get_player_status(self, pl): + player_status = _get_dbus_player_status( + pl=pl, + player_name='Spotify', + bus_name='org.mpris.MediaPlayer2.spotify', + player_path='/org/mpris/MediaPlayer2', + iface_prop='org.freedesktop.DBus.Properties', + iface_player='org.mpris.MediaPlayer2.Player', + ) + if player_status is not None: + return player_status + # Fallback for legacy spotify client with different DBus protocol + return _get_dbus_player_status( + pl=pl, + player_name='Spotify', + bus_name='com.spotify.qt', + player_path='/', + iface_prop='org.freedesktop.DBus.Properties', + iface_player='org.freedesktop.MediaPlayer2', + ) + + +spotify_dbus = with_docstring(SpotifyDbusPlayerSegment(), +('''Return spotify player information + +Requires ``dbus`` python module. + +{0} +''').format(_common_args.format('spotify_dbus'))) + + +class SpotifyAppleScriptPlayerSegment(PlayerSegment): + def get_player_status(self, pl): + status_delimiter = '-~`/=' + ascript = ''' + tell application "System Events" + set process_list to (name of every process) + end tell + + if process_list contains "Spotify" then + tell application "Spotify" + if player state is playing or player state is paused then + set track_name to name of current track + set artist_name to artist of current track + set album_name to album of current track + set track_length to duration of current track + set now_playing to "" & player state & "{0}" & album_name & "{0}" & artist_name & "{0}" & track_name & "{0}" & track_length & "{0}" & player position + return now_playing + else + return player state + end if + + end tell + else + return "stopped" + end if + '''.format(status_delimiter) + + spotify = asrun(pl, ascript) + if not asrun: + return None + + spotify_status = spotify.split(status_delimiter) + state = _convert_state(spotify_status[0]) + if state == 'stop': + return None + return { + 'state': state, + 'album': spotify_status[1], + 'artist': spotify_status[2], + 'title': spotify_status[3], + 'total': _convert_seconds(int(spotify_status[4])/1000), + 'elapsed': _convert_seconds(spotify_status[5]), + } + + +spotify_apple_script = with_docstring(SpotifyAppleScriptPlayerSegment(), +('''Return spotify player information + +Requires ``osascript`` available in $PATH. + +{0} +''').format(_common_args.format('spotify_apple_script'))) + + +if not sys.platform.startswith('darwin'): + spotify = spotify_dbus + _old_name = 'spotify_dbus' +else: + spotify = spotify_apple_script + _old_name = 'spotify_apple_script' + + +spotify = with_docstring(spotify, spotify.__doc__.replace(_old_name, 'spotify')) + + +class ClementinePlayerSegment(PlayerSegment): + def get_player_status(self, pl): + return _get_dbus_player_status( + pl=pl, + player_name='Clementine', + bus_name='org.mpris.MediaPlayer2.clementine', + player_path='/org/mpris/MediaPlayer2', + iface_prop='org.freedesktop.DBus.Properties', + iface_player='org.mpris.MediaPlayer2.Player', + ) + + +clementine = with_docstring(ClementinePlayerSegment(), +('''Return clementine player information + +Requires ``dbus`` python module. + +{0} +''').format(_common_args.format('clementine'))) + + +class RhythmboxPlayerSegment(PlayerSegment): + def get_player_status(self, pl): + now_playing = run_cmd(pl, [ + 'rhythmbox-client', + '--no-start', '--no-present', + '--print-playing-format', '%at\n%aa\n%tt\n%te\n%td' + ], strip=False) + if not now_playing: + return + now_playing = now_playing.split('\n') + return { + 'album': now_playing[0], + 'artist': now_playing[1], + 'title': now_playing[2], + 'elapsed': now_playing[3], + 'total': now_playing[4], + } + + +rhythmbox = with_docstring(RhythmboxPlayerSegment(), +('''Return rhythmbox player information + +Requires ``rhythmbox-client`` available in $PATH. + +{0} +''').format(_common_args.format('rhythmbox'))) + + +class RDIOPlayerSegment(PlayerSegment): + def get_player_status(self, pl): + status_delimiter = '-~`/=' + ascript = ''' + tell application "System Events" + set rdio_active to the count(every process whose name is "Rdio") + if rdio_active is 0 then + return + end if + end tell + tell application "Rdio" + set rdio_name to the name of the current track + set rdio_artist to the artist of the current track + set rdio_album to the album of the current track + set rdio_duration to the duration of the current track + set rdio_state to the player state + set rdio_elapsed to the player position + return rdio_name & "{0}" & rdio_artist & "{0}" & rdio_album & "{0}" & rdio_elapsed & "{0}" & rdio_duration & "{0}" & rdio_state + end tell + '''.format(status_delimiter) + now_playing = asrun(pl, ascript) + if not now_playing: + return + now_playing = now_playing.split(status_delimiter) + if len(now_playing) != 6: + return + state = _convert_state(now_playing[5]) + total = _convert_seconds(now_playing[4]) + elapsed = _convert_seconds(float(now_playing[3]) * float(now_playing[4]) / 100) + return { + 'title': now_playing[0], + 'artist': now_playing[1], + 'album': now_playing[2], + 'elapsed': elapsed, + 'total': total, + 'state': state, + } + + +rdio = with_docstring(RDIOPlayerSegment(), +('''Return rdio player information + +Requires ``osascript`` available in $PATH. + +{0} +''').format(_common_args.format('rdio'))) + + +class ITunesPlayerSegment(PlayerSegment): + def get_player_status(self, pl): + status_delimiter = '-~`/=' + ascript = ''' + tell application "System Events" + set process_list to (name of every process) + end tell + + if process_list contains "iTunes" then + tell application "iTunes" + if player state is playing then + set t_title to name of current track + set t_artist to artist of current track + set t_album to album of current track + set t_duration to duration of current track + set t_elapsed to player position + set t_state to player state + return t_title & "{0}" & t_artist & "{0}" & t_album & "{0}" & t_elapsed & "{0}" & t_duration & "{0}" & t_state + end if + end tell + end if + '''.format(status_delimiter) + now_playing = asrun(pl, ascript) + if not now_playing: + return + now_playing = now_playing.split(status_delimiter) + if len(now_playing) != 6: + return + title, artist, album = now_playing[0], now_playing[1], now_playing[2] + state = _convert_state(now_playing[5]) + total = _convert_seconds(now_playing[4]) + elapsed = _convert_seconds(now_playing[3]) + return { + 'title': title, + 'artist': artist, + 'album': album, + 'total': total, + 'elapsed': elapsed, + 'state': state + } + + +itunes = with_docstring(ITunesPlayerSegment(), +('''Return iTunes now playing information + +Requires ``osascript``. + +{0} +''').format(_common_args.format('itunes'))) + + +class MocPlayerSegment(PlayerSegment): + def get_player_status(self, pl): + '''Return Music On Console (mocp) player information. + + ``mocp -i`` returns current information i.e. + + .. code-block:: + + File: filename.format + Title: full title + Artist: artist name + SongTitle: song title + Album: album name + TotalTime: 00:00 + TimeLeft: 00:00 + TotalSec: 000 + CurrentTime: 00:00 + CurrentSec: 000 + Bitrate: 000kbps + AvgBitrate: 000kbps + Rate: 00kHz + + For the information we are looking for we don’t really care if we have + extra-timing information or bit rate level. The dictionary comprehension + in this method takes anything in ignore_info and brings the key inside + that to the right info of the dictionary. + ''' + now_playing_str = run_cmd(pl, ['mocp', '-i']) + if not now_playing_str: + return + + now_playing = dict(( + line.split(': ', 1) + for line in now_playing_str.split('\n')[:-1] + )) + state = _convert_state(now_playing.get('State', 'stop')) + return { + 'state': state, + 'album': now_playing.get('Album', ''), + 'artist': now_playing.get('Artist', ''), + 'title': now_playing.get('SongTitle', ''), + 'elapsed': _convert_seconds(now_playing.get('CurrentSec', 0)), + 'total': _convert_seconds(now_playing.get('TotalSec', 0)), + } + + +mocp = with_docstring(MocPlayerSegment(), +('''Return MOC (Music On Console) player information + +Requires version >= 2.3.0 and ``mocp`` executable in ``$PATH``. + +{0} +''').format(_common_args.format('mocp'))) diff --git a/powerline/segments/common/sys.py b/powerline/segments/common/sys.py new file mode 100644 index 0000000..29a2459 --- /dev/null +++ b/powerline/segments/common/sys.py @@ -0,0 +1,184 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from multiprocessing import cpu_count as _cpu_count + +from powerline.lib.threaded import ThreadedSegment +from powerline.lib import add_divider_highlight_group +from powerline.segments import with_docstring + + +cpu_count = None + + +def system_load(pl, format='{avg:.1f}', threshold_good=1, threshold_bad=2, + track_cpu_count=False, short=False): + '''Return system load average. + + Highlights using ``system_load_good``, ``system_load_bad`` and + ``system_load_ugly`` highlighting groups, depending on the thresholds + passed to the function. + + :param str format: + format string, receives ``avg`` as an argument + :param float threshold_good: + threshold for gradient level 0: any normalized load average below this + value will have this gradient level. + :param float threshold_bad: + threshold for gradient level 100: any normalized load average above this + value will have this gradient level. Load averages between + ``threshold_good`` and ``threshold_bad`` receive gradient level that + indicates relative position in this interval: + (``100 * (cur-good) / (bad-good)``). + Note: both parameters are checked against normalized load averages. + :param bool track_cpu_count: + if True powerline will continuously poll the system to detect changes + in the number of CPUs. + :param bool short: + if True only the sys load over last 1 minute will be displayed. + + Divider highlight group used: ``background:divider``. + + Highlight groups used: ``system_load_gradient`` (gradient) or ``system_load``. + ''' + global cpu_count + try: + cpu_num = cpu_count = _cpu_count() if cpu_count is None or track_cpu_count else cpu_count + except NotImplementedError: + pl.warn('Unable to get CPU count: method is not implemented') + return None + ret = [] + for avg in os.getloadavg(): + normalized = avg / cpu_num + if normalized < threshold_good: + gradient_level = 0 + elif normalized < threshold_bad: + gradient_level = (normalized - threshold_good) * 100.0 / (threshold_bad - threshold_good) + else: + gradient_level = 100 + ret.append({ + 'contents': format.format(avg=avg), + 'highlight_groups': ['system_load_gradient', 'system_load'], + 'divider_highlight_group': 'background:divider', + 'gradient_level': gradient_level, + }) + + if short: + return ret + + ret[0]['contents'] += ' ' + ret[1]['contents'] += ' ' + return ret + + +try: + import psutil + + class CPULoadPercentSegment(ThreadedSegment): + interval = 1 + + def update(self, old_cpu): + return psutil.cpu_percent(interval=None) + + def run(self): + while not self.shutdown_event.is_set(): + try: + self.update_value = psutil.cpu_percent(interval=self.interval) + except Exception as e: + self.exception('Exception while calculating cpu_percent: {0}', str(e)) + + def render(self, cpu_percent, format='{0:.0f}%', **kwargs): + return [{ + 'contents': format.format(cpu_percent), + 'gradient_level': cpu_percent, + 'highlight_groups': ['cpu_load_percent_gradient', 'cpu_load_percent'], + }] +except ImportError: + class CPULoadPercentSegment(ThreadedSegment): + interval = 1 + + @staticmethod + def startup(**kwargs): + pass + + @staticmethod + def start(): + pass + + @staticmethod + def shutdown(): + pass + + @staticmethod + def render(cpu_percent, pl, format='{0:.0f}%', **kwargs): + pl.warn('Module “psutil” is not installed, thus CPU load is not available') + return None + + +cpu_load_percent = with_docstring(CPULoadPercentSegment(), +'''Return the average CPU load as a percentage. + +Requires the ``psutil`` module. + +:param str format: + Output format. Accepts measured CPU load as the first argument. + +Highlight groups used: ``cpu_load_percent_gradient`` (gradient) or ``cpu_load_percent``. +''') + + +if os.path.exists('/proc/uptime'): + def _get_uptime(): + with open('/proc/uptime', 'r') as f: + return int(float(f.readline().split()[0])) +elif 'psutil' in globals(): + from time import time + + if hasattr(psutil, 'boot_time'): + def _get_uptime(): + return int(time() - psutil.boot_time()) + else: + def _get_uptime(): + return int(time() - psutil.BOOT_TIME) +else: + def _get_uptime(): + raise NotImplementedError + + +@add_divider_highlight_group('background:divider') +def uptime(pl, days_format='{days:d}d', hours_format=' {hours:d}h', minutes_format=' {minutes:02d}m', + seconds_format=' {seconds:02d}s', shorten_len=3): + '''Return system uptime. + + :param str days_format: + day format string, will be passed ``days`` as the argument + :param str hours_format: + hour format string, will be passed ``hours`` as the argument + :param str minutes_format: + minute format string, will be passed ``minutes`` as the argument + :param str seconds_format: + second format string, will be passed ``seconds`` as the argument + :param int shorten_len: + shorten the amount of units (days, hours, etc.) displayed + + Divider highlight group used: ``background:divider``. + ''' + try: + seconds = _get_uptime() + except NotImplementedError: + pl.warn('Unable to get uptime. You should install psutil module') + return None + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + days, hours = divmod(hours, 24) + time_formatted = list(filter(None, [ + days_format.format(days=days) if days_format else None, + hours_format.format(hours=hours) if hours_format else None, + minutes_format.format(minutes=minutes) if minutes_format else None, + seconds_format.format(seconds=seconds) if seconds_format else None, + ])) + first_non_zero = next((i for i, x in enumerate([days, hours, minutes, seconds]) if x != 0)) + time_formatted = time_formatted[first_non_zero:first_non_zero + shorten_len] + return ''.join(time_formatted).strip() diff --git a/powerline/segments/common/time.py b/powerline/segments/common/time.py new file mode 100644 index 0000000..be727c9 --- /dev/null +++ b/powerline/segments/common/time.py @@ -0,0 +1,123 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from datetime import datetime + + +def date(pl, format='%Y-%m-%d', istime=False, timezone=None): + '''Return the current date. + + :param str format: + strftime-style date format string + :param bool istime: + If true then segment uses ``time`` highlight group. + :param string timezone: + Specify a timezone to use as ``+HHMM`` or ``-HHMM``. + (Defaults to system defaults.) + + Divider highlight group used: ``time:divider``. + + Highlight groups used: ``time`` or ``date``. + ''' + + try: + tz = datetime.strptime(timezone, '%z').tzinfo if timezone else None + except ValueError: + tz = None + + nw = datetime.now(tz) + + try: + contents = nw.strftime(format) + except UnicodeEncodeError: + contents = nw.strftime(format.encode('utf-8')).decode('utf-8') + + return [{ + 'contents': contents, + 'highlight_groups': (['time'] if istime else []) + ['date'], + 'divider_highlight_group': 'time:divider' if istime else None, + }] + + +UNICODE_TEXT_TRANSLATION = { + ord('\''): '’', + ord('-'): '‐', +} + + +def fuzzy_time(pl, format='{minute_str} {hour_str}', unicode_text=False, timezone=None, hour_str=['twelve', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven'], minute_str = { + '0': 'o\'clock', '5': 'five past', '10': 'ten past','15': 'quarter past', + '20': 'twenty past', '25': 'twenty-five past', '30': 'half past', '35': 'twenty-five to', + '40': 'twenty to', '45': 'quarter to', '50': 'ten to', '55': 'five to' + }, special_case_str = { + '(23, 58)': 'round about midnight', + '(23, 59)': 'round about midnight', + '(0, 0)': 'midnight', + '(0, 1)': 'round about midnight', + '(0, 2)': 'round about midnight', + '(12, 0)': 'noon', + }): + + '''Display the current time as fuzzy time, e.g. "quarter past six". + + :param string format: + Format used to display the fuzzy time. (Ignored when a special time + is displayed.) + :param bool unicode_text: + If true then hyphenminuses (regular ASCII ``-``) and single quotes are + replaced with unicode dashes and apostrophes. + :param string timezone: + Specify a timezone to use as ``+HHMM`` or ``-HHMM``. + (Defaults to system defaults.) + :param string list hour_str: + Strings to be used to display the hour, starting with midnight. + (This list may contain 12 or 24 entries.) + :param dict minute_str: + Dictionary mapping minutes to strings to be used to display them. + :param dict special_case_str: + Special strings for special times. + + Highlight groups used: ``fuzzy_time``. + ''' + + try: + tz = datetime.strptime(timezone, '%z').tzinfo if timezone else None + except ValueError: + tz = None + + now = datetime.now(tz) + + try: + # We don't want to enforce a special type of spaces/ alignment in the input + from ast import literal_eval + special_case_str = {literal_eval(x):special_case_str[x] for x in special_case_str} + result = special_case_str[(now.hour, now.minute)] + if unicode_text: + result = result.translate(UNICODE_TEXT_TRANSLATION) + return result + except KeyError: + pass + + hour = now.hour + if now.minute >= 30: + hour = hour + 1 + hour = hour % len(hour_str) + + min_dis = 100 + min_pos = 0 + + for mn in minute_str: + mn = int(mn) + if now.minute >= mn and now.minute - mn < min_dis: + min_dis = now.minute - mn + min_pos = mn + elif now.minute < mn and mn - now.minute < min_dis: + min_dis = mn - now.minute + min_pos = mn + result = format.format(minute_str=minute_str[str(min_pos)], hour_str=hour_str[hour]) + + if unicode_text: + result = result.translate(UNICODE_TEXT_TRANSLATION) + + return result diff --git a/powerline/segments/common/vcs.py b/powerline/segments/common/vcs.py new file mode 100644 index 0000000..07679ae --- /dev/null +++ b/powerline/segments/common/vcs.py @@ -0,0 +1,89 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.lib.vcs import guess, tree_status +from powerline.segments import Segment, with_docstring +from powerline.theme import requires_segment_info, requires_filesystem_watcher + + +@requires_filesystem_watcher +@requires_segment_info +class BranchSegment(Segment): + divider_highlight_group = None + + @staticmethod + def get_directory(segment_info): + return segment_info['getcwd']() + + def __call__(self, pl, segment_info, create_watcher, status_colors=False, ignore_statuses=()): + name = self.get_directory(segment_info) + if name: + repo = guess(path=name, create_watcher=create_watcher) + if repo is not None: + branch = repo.branch() + scol = ['branch'] + if status_colors: + try: + status = tree_status(repo, pl) + except Exception as e: + pl.exception('Failed to compute tree status: {0}', str(e)) + status = '?' + else: + status = status and status.strip() + if status in ignore_statuses: + status = None + scol.insert(0, 'branch_dirty' if status else 'branch_clean') + return [{ + 'contents': branch, + 'highlight_groups': scol, + 'divider_highlight_group': self.divider_highlight_group, + }] + + +branch = with_docstring(BranchSegment(), +'''Return the current VCS branch. + +:param bool status_colors: + Determines whether repository status will be used to determine highlighting. + Default: False. +:param list ignore_statuses: + List of statuses which will not result in repo being marked as dirty. Most + useful is setting this option to ``["U"]``: this will ignore repository + which has just untracked files (i.e. repository with modified, deleted or + removed files will be marked as dirty, while just untracked files will make + segment show clean repository). Only applicable if ``status_colors`` option + is True. + +Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``. +''') + + +@requires_filesystem_watcher +@requires_segment_info +class StashSegment(Segment): + divider_highlight_group = None + + @staticmethod + def get_directory(segment_info): + return segment_info['getcwd']() + + def __call__(self, pl, segment_info, create_watcher): + name = self.get_directory(segment_info) + if name: + repo = guess(path=name, create_watcher=create_watcher) + if repo is not None: + stash = getattr(repo, 'stash', None) + if stash: + stashes = stash() + if stashes: + return [{ + 'contents': str(stashes), + 'highlight_groups': ['stash'], + 'divider_highlight_group': self.divider_highlight_group + }] + +stash = with_docstring(StashSegment(), +'''Return the number of current VCS stash entries, if any. + +Highlight groups used: ``stash``. +''') diff --git a/powerline/segments/common/wthr.py b/powerline/segments/common/wthr.py new file mode 100644 index 0000000..2c54cca --- /dev/null +++ b/powerline/segments/common/wthr.py @@ -0,0 +1,234 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import json +from collections import namedtuple + +from powerline.lib.url import urllib_read, urllib_urlencode +from powerline.lib.threaded import KwThreadedSegment +from powerline.segments import with_docstring + + +_WeatherKey = namedtuple('Key', 'location_query weather_api_key') + + +# XXX Warning: module name must not be equal to the segment name as long as this +# segment is imported into powerline.segments.common module. + + +# Weather condition code descriptions available at +# https://openweathermap.org/weather-conditions +weather_conditions_codes = { + 200: ('stormy',), + 201: ('stormy',), + 202: ('stormy',), + 210: ('stormy',), + 211: ('stormy',), + 212: ('stormy',), + 221: ('stormy',), + 230: ('stormy',), + 231: ('stormy',), + 232: ('stormy',), + 300: ('rainy',), + 301: ('rainy',), + 302: ('rainy',), + 310: ('rainy',), + 311: ('rainy',), + 312: ('rainy',), + 313: ('rainy',), + 314: ('rainy',), + 321: ('rainy',), + 500: ('rainy',), + 501: ('rainy',), + 502: ('rainy',), + 503: ('rainy',), + 504: ('rainy',), + 511: ('snowy',), + 520: ('rainy',), + 521: ('rainy',), + 522: ('rainy',), + 531: ('rainy',), + 600: ('snowy',), + 601: ('snowy',), + 602: ('snowy',), + 611: ('snowy',), + 612: ('snowy',), + 613: ('snowy',), + 615: ('snowy',), + 616: ('snowy',), + 620: ('snowy',), + 621: ('snowy',), + 622: ('snowy',), + 701: ('foggy',), + 711: ('foggy',), + 721: ('foggy',), + 731: ('foggy',), + 741: ('foggy',), + 751: ('foggy',), + 761: ('foggy',), + 762: ('foggy',), + 771: ('foggy',), + 781: ('foggy',), + 800: ('sunny',), + 801: ('cloudy',), + 802: ('cloudy',), + 803: ('cloudy',), + 804: ('cloudy',), +} + +weather_conditions_icons = { + 'day': 'DAY', + 'blustery': 'WIND', + 'rainy': 'RAIN', + 'cloudy': 'CLOUDS', + 'snowy': 'SNOW', + 'stormy': 'STORM', + 'foggy': 'FOG', + 'sunny': 'SUN', + 'night': 'NIGHT', + 'windy': 'WINDY', + 'not_available': 'NA', + 'unknown': 'UKN', +} + +temp_conversions = { + 'C': lambda temp: temp - 273.15, + 'F': lambda temp: (temp * 9 / 5) - 459.67, + 'K': lambda temp: temp, +} + +# Note: there are also unicode characters for units: ℃, ℉ and K +temp_units = { + 'C': '°C', + 'F': '°F', + 'K': 'K', +} + + +class WeatherSegment(KwThreadedSegment): + interval = 600 + default_location = None + location_urls = {} + weather_api_key = "fbc9549d91a5e4b26c15be0dbdac3460" + + @staticmethod + def key(location_query=None, **kwargs): + try: + weather_api_key = kwargs["weather_api_key"] + except KeyError: + weather_api_key = WeatherSegment.weather_api_key + return _WeatherKey(location_query, weather_api_key) + + def get_request_url(self, weather_key): + try: + return self.location_urls[weather_key] + except KeyError: + query_data = { + "appid": weather_key.weather_api_key + } + location_query = weather_key.location_query + if location_query is None: + location_data = json.loads(urllib_read('https://freegeoip.app/json/')) + query_data["lat"] = location_data["latitude"] + query_data["lon"] = location_data["longitude"] + else: + query_data["q"] = location_query + self.location_urls[location_query] = url = ( + "https://api.openweathermap.org/data/2.5/weather?" + + urllib_urlencode(query_data)) + return url + + def compute_state(self, weather_key): + url = self.get_request_url(weather_key) + raw_response = urllib_read(url) + if not raw_response: + self.error('Failed to get response') + return None + + response = json.loads(raw_response) + try: + condition = response['weather'][0] + condition_code = int(condition['id']) + temp = float(response['main']['temp']) + except (KeyError, ValueError): + self.exception('OpenWeatherMap returned malformed or unexpected response: {0}', repr(raw_response)) + return None + + try: + icon_names = weather_conditions_codes[condition_code] + except IndexError: + icon_names = ('unknown',) + self.error('Unknown condition code: {0}', condition_code) + + return (temp, icon_names) + + def render_one(self, weather, icons=None, unit='C', temp_format=None, temp_coldest=-30, temp_hottest=40, **kwargs): + if not weather: + return None + + temp, icon_names = weather + + for icon_name in icon_names: + if icons: + if icon_name in icons: + icon = icons[icon_name] + break + else: + icon = weather_conditions_icons[icon_names[-1]] + + temp_format = temp_format or ('{temp:.0f}' + temp_units[unit]) + converted_temp = temp_conversions[unit](temp) + if converted_temp <= temp_coldest: + gradient_level = 0 + elif converted_temp >= temp_hottest: + gradient_level = 100 + else: + gradient_level = (converted_temp - temp_coldest) * 100.0 / (temp_hottest - temp_coldest) + groups = ['weather_condition_' + icon_name for icon_name in icon_names] + ['weather_conditions', 'weather'] + return [ + { + 'contents': icon + ' ', + 'highlight_groups': groups, + 'divider_highlight_group': 'background:divider', + }, + { + 'contents': temp_format.format(temp=converted_temp), + 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], + 'divider_highlight_group': 'background:divider', + 'gradient_level': gradient_level, + }, + ] + + +weather = with_docstring(WeatherSegment(), +'''Return weather from OpenWeatherMaps. + +Uses GeoIP lookup from https://freegeoip.app to automatically determine +your current location. This should be changed if you’re in a VPN or if your +IP address is registered at another location. + +Returns a list of colorized icon and temperature segments depending on +weather conditions. + +:param str unit: + temperature unit, can be one of ``F``, ``C`` or ``K`` +:param str location_query: + location query for your current location, e.g. ``oslo, norway`` +:param dict icons: + dict for overriding default icons, e.g. ``{'heavy_snow' : u'❆'}`` +:param str temp_format: + format string, receives ``temp`` as an argument. Should also hold unit. +:param float temp_coldest: + coldest temperature. Any temperature below it will have gradient level equal + to zero. +:param float temp_hottest: + hottest temperature. Any temperature above it will have gradient level equal + to 100. Temperatures between ``temp_coldest`` and ``temp_hottest`` receive + gradient level that indicates relative position in this interval + (``100 * (cur-coldest) / (hottest-coldest)``). + +Divider highlight group used: ``background:divider``. + +Highlight groups used: ``weather_conditions`` or ``weather``, ``weather_temp_gradient`` (gradient) or ``weather``. +Also uses ``weather_conditions_{condition}`` for all weather conditions supported by OpenWeatherMap. +''') diff --git a/powerline/segments/i3wm.py b/powerline/segments/i3wm.py new file mode 100644 index 0000000..57ab377 --- /dev/null +++ b/powerline/segments/i3wm.py @@ -0,0 +1,309 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import re + +from powerline.theme import requires_segment_info +from powerline.bindings.wm import get_i3_connection + +WORKSPACE_REGEX = re.compile(r'^[0-9]+: ?') + +def workspace_groups(w): + group = [] + if w.focused: + group.append('workspace:focused') + group.append('w_focused') + if w.urgent: + group.append('workspace:urgent') + group.append('w_urgent') + if w.visible: + group.append('workspace:visible') + group.append('w_visible') + group.append('workspace') + return group + + +def format_name(name, strip=False): + if strip: + return WORKSPACE_REGEX.sub('', name, count=1) + return name + + +def is_empty_workspace(workspace, containers): + if workspace.focused or workspace.visible: + return False + wins = [win for win in containers[workspace.name].leaves()] + return False if len(wins) > 0 else True + +WS_ICONS = {"multiple": "M"} + +def get_icon(workspace, separator, icons, show_multiple_icons, ws_containers): + icons_tmp = WS_ICONS + icons_tmp.update(icons) + icons = icons_tmp + + wins = [win for win in ws_containers[workspace.name].leaves() \ + if win.parent.scratchpad_state == 'none'] + if len(wins) == 0: + return '' + + result = '' + cnt = 0 + for key in icons: + if not icons[key] or len(icons[key]) < 1: + continue + if any(key in win.window_class for win in wins if win.window_class): + result += (separator if cnt > 0 else '') + icons[key] + cnt += 1 + if not show_multiple_icons and cnt > 1: + if 'multiple' in icons: + return icons['multiple'] + else: + return '' + return result + +@requires_segment_info +def workspaces(pl, segment_info, only_show=None, output=None, strip=0, format='{name}', + icons=WS_ICONS, sort_workspaces=False, show_output=False, priority_workspaces=[], + hide_empty_workspaces=False): + '''Return list of used workspaces + + :param list only_show: + Specifies which workspaces to show. Valid entries are ``"visible"``, + ``"urgent"`` and ``"focused"``. If omitted or ``null`` all workspaces + are shown. + :param str output: + May be set to the name of an X output. If specified, only workspaces + on that output are shown. Overrides automatic output detection by + the lemonbar renderer and bindings. + Use "__all__" to show workspaces on all outputs. + :param int strip: + Specifies how many characters from the front of each workspace name + should be stripped (e.g. to remove workspace numbers). Defaults to zero. + :param str format: + Specifies the format used to display workspaces; defaults to ``{name}``. + Valid fields are: ``name`` (workspace name), ``number`` (workspace number + if present), `stipped_name`` (workspace name stripped of leading number), + ``icon`` (if available, icon for application running in the workspace, + uses the ``multiple`` icon instead of multiple different icons), ``multi_icon`` + (similar to ``icon``, but does not use ``multiple``, instead joins all icons + with a single space) + :param dict icons: + A dictionary mapping a substring of window classes to strings to be used as an + icon for that window class. + Further, there is a ``multiple`` icon for workspaces containing more than one + window. + :param bool sort_workspaces: + Sort the workspaces displayed by their name according to the natural ordering. + :param bool show_output: + Shows the name of the output if more than one output is connected. + :param list priority_workspaces: + A list of workspace names to be sorted before any other workspaces in the given + order. + :param bool hide_empty_workspaces: + Hides all workspaces without any open window. + Also hides non-focussed workspaces containing only an open scratchpad. + + + Highlight groups used: ``workspace`` or ``w_visible``, ``workspace:visible``, ``workspace`` or ``w_focused``, ``workspace:focused``, ``workspace`` or ``w_urgent``, ``workspace:urgent``, ``workspace`` or ``output``. + ''' + conn = get_i3_connection() + + if not output == "__all__": + output = output or segment_info.get('output') + else: + output = None + + if output: + output = [output] + else: + output = [o.name for o in conn.get_outputs() if o.active] + + + def sort_ws(ws): + if sort_workspaces: + def natural_key(ws): + str = ws.name + return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', str)] + ws = sorted(ws, key=natural_key) + result = [] + for n in priority_workspaces: + result += [w for w in ws if w.name == n] + return result + [w for w in ws if not w.name in priority_workspaces] + + ws_containers = {w_con.name : w_con for w_con in conn.get_tree().workspaces()} + + if len(output) <= 1: + res = [] + if show_output: + res += [{ + 'contents': output[0], + 'highlight_groups': ['output'] + }] + res += [{ + 'contents': format.format(name = w.name[min(len(w.name), strip):], + stripped_name = format_name(w.name, strip=True), + number = w.num, + icon = get_icon(w, '', icons, False, ws_containers), + multi_icon = get_icon(w, ' ', icons, True, ws_containers)), + 'highlight_groups': workspace_groups(w) + } for w in sort_ws(conn.get_workspaces()) \ + if (not only_show or any(getattr(w, tp) for tp in only_show)) \ + if w.output == output[0] \ + if not (hide_empty_workspaces and is_empty_workspace(w, ws_containers))] + return res + else: + res = [] + for n in output: + if show_output: + res += [{ + 'contents': n, + 'highlight_groups': ['output'] + }] + res += [{ + 'contents': format.format(name = w.name[min(len(w.name), strip):], + stripped_name = format_name(w.name, strip=True), + number = w.num, + icon = get_icon(w, '', icons, False, ws_containers), + multi_icon = get_icon(w, ' ', icons, True, ws_containers)), + 'highlight_groups': workspace_groups(w) + } for w in sort_ws(conn.get_workspaces()) \ + if (not only_show or any(getattr(w, tp) for tp in only_show)) \ + if w.output == n \ + if not (hide_empty_workspaces and is_empty_workspace(w, ws_containers))] + return res + +@requires_segment_info +def workspace(pl, segment_info, workspace=None, strip=False, format=None, icons=WS_ICONS): + '''Return the specified workspace name + + :param str workspace: + Specifies which workspace to show. If unspecified, may be set by the + ``list_workspaces`` lister if used, otherwise falls back to + currently focused workspace. + + :param bool strip: + Specifies whether workspace numbers (in the ``1: name`` format) should + be stripped from workspace names before being displayed. Defaults to false. + Deprecated: Use {name} or {stripped_name} of format instead. + + :param str format: + Specifies the format used to display workspaces; defaults to ``{name}``. + Valid fields are: ``name`` (workspace name), ``number`` (workspace number + if present), `stipped_name`` (workspace name stripped of leading number), + ``icon`` (if available, icon for application running in the workspace, + uses the ``multiple`` icon instead of multiple different icons), ``multi_icon`` + (similar to ``icon``, but does not use ``multiple``, instead joins all icons + with a single space) + + :param dict icons: + A dictionary mapping a substring of window classes to strings to be used as an + icon for that window class. + Further, there is a ``multiple`` icon for workspaces containing more than one + window. + + Highlight groups used: ``workspace`` or ``w_visible``, ``workspace:visible``, ``workspace`` or ``w_focused``, ``workspace:focused``, ``workspace`` or ``w_urgent``, ``workspace:urgent``, ``workspace``. + ''' + if format == None: + format = '{stripped_name}' if strip else '{name}' + + conn = get_i3_connection() + ws_containers = {w_con.name : w_con for w_con in conn.get_tree().workspaces()} + + if workspace: + try: + w = next(( + w for w in conn.get_workspaces() + if w.name == workspace + )) + except StopIteration: + return None + elif segment_info.get('workspace'): + w = segment_info['workspace'] + else: + try: + w = next(( + w for w in conn.get_workspaces() + if w.focused + )) + except StopIteration: + return None + + return [{ + 'contents': format.format(name = w.name, + stripped_name = format_name(w.name, strip=True), + number = w.num, + icon = get_icon(w, '', icons, False, ws_containers), + multi_icon = get_icon(w, ' ', icons, True, ws_containers)), + 'highlight_groups': workspace_groups(w) + }] + + +@requires_segment_info +def mode(pl, segment_info, names={'default': None}): + '''Returns current i3 mode + + :param dict names: + Specifies the string to show for various modes. + Use ``null`` to hide a mode (``default`` is hidden by default). + + Highligh groups used: ``mode`` + ''' + mode = segment_info['mode'] + if mode in names: + return names[mode] + return mode + + +def scratchpad_groups(w): + group = [] + if w.urgent: + group.append('scratchpad:urgent') + if w.nodes[0].focused: + group.append('scratchpad:focused') + if w.workspace().name != '__i3_scratch': + group.append('scratchpad:visible') + group.append('scratchpad') + return group + + +SCRATCHPAD_ICONS = { + 'fresh': 'O', + 'changed': 'X', +} + + +def scratchpad(pl, icons=SCRATCHPAD_ICONS): + '''Returns the windows currently on the scratchpad + + :param dict icons: + Specifies the strings to show for the different scratchpad window states. Must + contain the keys ``fresh`` and ``changed``. + + Highlight groups used: ``scratchpad`` or ``scratchpad:visible``, ``scratchpad`` or ``scratchpad:focused``, ``scratchpad`` or ``scratchpad:urgent``. + ''' + + return [ + { + 'contents': icons.get(w.scratchpad_state, icons['changed']), + 'highlight_groups': scratchpad_groups(w) + } + for w in get_i3_connection().get_tree().descendants() + if w.scratchpad_state != 'none' + ] + +def active_window(pl, cutoff=100): + '''Returns the title of the currently active window. + + :param int cutoff: + Maximum title length. If the title is longer, the window_class is used instead. + + Highlight groups used: ``active_window_title``. + ''' + + focused = get_i3_connection().get_tree().find_focused() + cont = focused.name + if len(cont) > cutoff: + cont = focused.window_class + + return [{'contents': cont, 'highlight_groups': ['active_window_title']}] if focused.name != focused.workspace().name else [] diff --git a/powerline/segments/ipython.py b/powerline/segments/ipython.py new file mode 100644 index 0000000..622e0a5 --- /dev/null +++ b/powerline/segments/ipython.py @@ -0,0 +1,9 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info + + +@requires_segment_info +def prompt_count(pl, segment_info): + return str(segment_info['ipython'].prompt_count) diff --git a/powerline/segments/pdb.py b/powerline/segments/pdb.py new file mode 100644 index 0000000..bd6a38b --- /dev/null +++ b/powerline/segments/pdb.py @@ -0,0 +1,61 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os + +from powerline.theme import requires_segment_info + + +@requires_segment_info +def current_line(pl, segment_info): + '''Displays line number that is next to be run + ''' + return str(segment_info['curframe'].f_lineno) + + +@requires_segment_info +def current_file(pl, segment_info, basename=True): + '''Displays current file name + + :param bool basename: + If true only basename is displayed. + ''' + filename = segment_info['curframe'].f_code.co_filename + if basename: + filename = os.path.basename(filename) + return filename + + +@requires_segment_info +def current_code_name(pl, segment_info): + '''Displays name of the code object of the current frame + ''' + return segment_info['curframe'].f_code.co_name + + +@requires_segment_info +def current_context(pl, segment_info): + '''Displays currently executed context name + + This is similar to :py:func:`current_code_name`, but gives more details. + + Currently it only gives module file name if code_name happens to be + ````. + ''' + name = segment_info['curframe'].f_code.co_name + if name == '': + name = os.path.basename(segment_info['curframe'].f_code.co_filename) + return name + + +@requires_segment_info +def stack_depth(pl, segment_info, full_stack=False): + '''Displays current stack depth + + Result is relative to the stack depth at the time prompt was first run. + + :param bool full_stack: + If true then absolute depth is used. + ''' + return str(len(segment_info['pdb'].stack) - ( + 0 if full_stack else segment_info['initial_stack_length'])) diff --git a/powerline/segments/shell.py b/powerline/segments/shell.py new file mode 100644 index 0000000..66991c7 --- /dev/null +++ b/powerline/segments/shell.py @@ -0,0 +1,196 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.theme import requires_segment_info +from powerline.segments import with_docstring +from powerline.segments.common.env import CwdSegment +from powerline.lib.unicode import out_u + + +@requires_segment_info +def jobnum(pl, segment_info, show_zero=False): + '''Return the number of jobs. + + :param bool show_zero: + If False (default) shows nothing if there are no jobs. Otherwise shows + zero for no jobs. + ''' + jobnum = segment_info['args'].jobnum + if jobnum is None or (not show_zero and jobnum == 0): + return None + else: + return str(jobnum) + +try: + import signal + exit_codes = dict((k, v) for v, k in reversed(sorted(signal.__dict__.items())) \ + if v.startswith('SIG') and not v.startswith('SIG_')) +except ImportError: + exit_codes = dict() + +@requires_segment_info +def last_status(pl, segment_info, signal_names=True): + '''Return last exit code. + + :param bool signal_names: + If True (default), translate signal numbers to human-readable names. + + Highlight groups used: ``exit_fail`` + ''' + if not segment_info['args'].last_exit_code: + return None + + try: + if signal_names and segment_info['args'].last_exit_code - 128 in exit_codes: + return [{'contents': exit_codes[segment_info['args'].last_exit_code - 128], 'highlight_groups': ['exit_fail']}] + except TypeError: + pass + return [{'contents': str(segment_info['args'].last_exit_code), 'highlight_groups': ['exit_fail']}] + + +@requires_segment_info +def last_pipe_status(pl, segment_info, signal_names=True): + '''Return last pipe status. + + :param bool signal_names: + If True (default), translate signal numbers to human-readable names. + + Highlight groups used: ``exit_fail``, ``exit_success`` + ''' + last_pipe_status = ( + segment_info['args'].last_pipe_status + or (segment_info['args'].last_exit_code,) + ) + if any(last_pipe_status): + try: + return [{ + 'contents': exit_codes[status - 128] if signal_names and \ + status - 128 in exit_codes else str(status), + 'highlight_groups': ['exit_fail' if status else 'exit_success'], + 'draw_inner_divider': True + } for status in last_pipe_status] + except TypeError: + return [{ + 'contents': str(status), + 'highlight_groups': ['exit_fail' if status else 'exit_success'], + 'draw_inner_divider': True + } for status in last_pipe_status] + else: + return None + +@requires_segment_info +def mode(pl, segment_info, override={'vicmd': 'COMMND', 'viins': 'INSERT'}, default=None): + '''Return the current mode. + + :param dict override: + dict for overriding mode strings. + :param str default: + If current mode is equal to this string then this segment will not get + displayed. If not specified the value is taken from + ``$POWERLINE_DEFAULT_MODE`` variable. This variable is set by zsh + bindings for any mode that does not start from ``vi``. + ''' + mode = segment_info.get('mode', None) + if not mode: + pl.debug('No mode specified') + return None + default = default or segment_info.get('default_mode', None) + if mode == default: + return None + try: + return override[mode] + except KeyError: + # Note: with zsh line editor you can emulate as much modes as you wish. + # Thus having unknown mode is not an error: maybe just some developer + # added support for his own zle widgets. As there is no built-in mode() + # function like in VimL and mode is likely be defined by our code or by + # somebody knowing what he is doing there is absolutely no need in + # keeping translations dictionary. + return mode.upper() + + +@requires_segment_info +def continuation(pl, segment_info, omit_cmdsubst=True, right_align=False, renames={}): + '''Display parser state. + + :param bool omit_cmdsubst: + Do not display cmdsubst parser state if it is the last one. + :param bool right_align: + Align to the right. + :param dict renames: + Rename states: ``{old_name : new_name}``. If ``new_name`` is ``None`` + then given state is not displayed. + + Highlight groups used: ``continuation``, ``continuation:current``. + ''' + if not segment_info.get('parser_state'): + return [{ + 'contents': '', + 'width': 'auto', + 'highlight_groups': ['continuation:current', 'continuation'], + }] + ret = [] + + for state in segment_info['parser_state'].split(): + state = renames.get(state, state) + if state: + ret.append({ + 'contents': state, + 'highlight_groups': ['continuation'], + 'draw_inner_divider': True, + }) + + if omit_cmdsubst and ret[-1]['contents'] == 'cmdsubst': + ret.pop(-1) + + if not ret: + ret.append({ + 'contents': '' + }) + + if right_align: + ret[0].update(width='auto', align='r') + ret[-1]['highlight_groups'] = ['continuation:current', 'continuation'] + else: + ret[-1].update(width='auto', align='l', highlight_groups=['continuation:current', 'continuation']) + + return ret + + +@requires_segment_info +class ShellCwdSegment(CwdSegment): + def get_shortened_path(self, pl, segment_info, use_shortened_path=True, **kwargs): + if use_shortened_path: + try: + return out_u(segment_info['shortened_path']) + except KeyError: + pass + return super(ShellCwdSegment, self).get_shortened_path(pl, segment_info, **kwargs) + + +cwd = with_docstring(ShellCwdSegment(), +'''Return the current working directory. + +Returns a segment list to create a breadcrumb-like effect. + +:param int dir_shorten_len: + shorten parent directory names to this length (e.g. + :file:`/long/path/to/powerline` → :file:`/l/p/t/powerline`) +:param int dir_limit_depth: + limit directory depth to this number (e.g. + :file:`/long/path/to/powerline` → :file:`⋯/to/powerline`) +:param bool use_path_separator: + Use path separator in place of soft divider. +:param bool use_shortened_path: + Use path from shortened_path ``--renderer-arg`` argument. If this argument + is present ``shorten_home`` argument is ignored. +:param bool shorten_home: + Shorten home directory to ``~``. +:param str ellipsis: + Specifies what to use in place of omitted directories. Use None to not + show this subsegment at all. + +Divider highlight group used: ``cwd:divider``. + +Highlight groups used: ``cwd:current_folder`` or ``cwd``. It is recommended to define all highlight groups. +''') diff --git a/powerline/segments/tmux.py b/powerline/segments/tmux.py new file mode 100644 index 0000000..1f34389 --- /dev/null +++ b/powerline/segments/tmux.py @@ -0,0 +1,22 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline.bindings.tmux import get_tmux_output + + +def attached_clients(pl, minimum=1): + '''Return the number of tmux clients attached to the currently active session + + :param int minimum: + The minimum number of attached clients that must be present for this + segment to be visible. + ''' + session_output = get_tmux_output(pl, 'list-panes', '-F', '#{session_name}') + if not session_output: + return None + session_name = session_output.rstrip().split('\n')[0] + + attached_clients_output = get_tmux_output(pl, 'list-clients', '-t', session_name) + attached_count = len(attached_clients_output.rstrip().split('\n')) + + return None if attached_count < minimum else str(attached_count) diff --git a/powerline/segments/vim/__init__.py b/powerline/segments/vim/__init__.py new file mode 100644 index 0000000..d999d07 --- /dev/null +++ b/powerline/segments/vim/__init__.py @@ -0,0 +1,805 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import re +import csv +import sys + +from collections import defaultdict + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import (vim_get_func, getbufvar, vim_getbufoption, + buffer_name, vim_getwinvar, + register_buffer_cache, current_tabpage, + list_tabpage_buffers_segment_info) +from powerline.theme import requires_segment_info, requires_filesystem_watcher +from powerline.lib import add_divider_highlight_group +from powerline.lib.vcs import guess +from powerline.lib.humanize_bytes import humanize_bytes +from powerline.lib import wraps_saveargs as wraps +from powerline.segments.common.vcs import BranchSegment, StashSegment +from powerline.segments import with_docstring +from powerline.lib.unicode import string, unicode + +try: + from __builtin__ import xrange as range +except ImportError: + pass + + +vim_funcs = { + 'virtcol': vim_get_func('virtcol', rettype='int'), + 'getpos': vim_get_func('getpos'), + 'fnamemodify': vim_get_func('fnamemodify', rettype='bytes'), + 'line2byte': vim_get_func('line2byte', rettype='int'), + 'line': vim_get_func('line', rettype='int'), +} + +vim_modes = { + 'n': 'NORMAL', + 'no': 'N-OPER', + 'v': 'VISUAL', + 'V': 'V-LINE', + '^V': 'V-BLCK', + 's': 'SELECT', + 'S': 'S-LINE', + '^S': 'S-BLCK', + 'i': 'INSERT', + 'ic': 'I-COMP', + 'ix': 'I-C_X ', + 'R': 'RPLACE', + 'Rv': 'V-RPLC', + 'Rc': 'R-COMP', + 'Rx': 'R-C_X ', + 'c': 'COMMND', + 'cv': 'VIM-EX', + 'ce': 'NRM-EX', + 'r': 'PROMPT', + 'rm': '-MORE-', + 'r?': 'CNFIRM', + '!': '!SHELL', + 't': 'TERM ', +} + + +# TODO Remove cache when needed +def window_cached(func): + cache = {} + + @requires_segment_info + @wraps(func) + def ret(segment_info, **kwargs): + window_id = segment_info['window_id'] + if segment_info['mode'] == 'nc': + return cache.get(window_id) + else: + if getattr(func, 'powerline_requires_segment_info', False): + r = func(segment_info=segment_info, **kwargs) + else: + r = func(**kwargs) + cache[window_id] = r + return r + + return ret + + +@requires_segment_info +def mode(pl, segment_info, override=None): + '''Return the current vim mode. + + If mode (returned by ``mode()`` VimL function, see ``:h mode()`` in Vim) + consists of multiple characters and necessary mode is not known to powerline + then it will fall back to mode with last character(s) ignored. + + :param dict override: + dict for overriding default mode strings, e.g. ``{ 'n': 'NORM' }`` + ''' + mode = segment_info['mode'] + if mode == 'nc': + return None + while mode: + try: + if not override: + return vim_modes[mode] + try: + return override[mode] + except KeyError: + return vim_modes[mode] + except KeyError: + mode = mode[:-1] + return 'BUG' + + +@window_cached +@requires_segment_info +def visual_range(pl, segment_info, CTRL_V_text='{rows} x {vcols}', v_text_oneline='C:{vcols}', v_text_multiline='L:{rows}', V_text='L:{rows}'): + '''Return the current visual selection range. + + :param str CTRL_V_text: + Text to display when in block visual or select mode. + :param str v_text_oneline: + Text to display when in charaterwise visual or select mode, assuming + selection occupies only one line. + :param str v_text_multiline: + Text to display when in charaterwise visual or select mode, assuming + selection occupies more then one line. + :param str V_text: + Text to display when in linewise visual or select mode. + + All texts are format strings which are passed the following parameters: + + ========= ============================================================= + Parameter Description + ========= ============================================================= + sline Line number of the first line of the selection + eline Line number of the last line of the selection + scol Column number of the first character of the selection + ecol Column number of the last character of the selection + svcol Virtual column number of the first character of the selection + secol Virtual column number of the last character of the selection + rows Number of lines in the selection + cols Number of columns in the selection + vcols Number of virtual columns in the selection + ========= ============================================================= + ''' + sline, scol, soff = [int(v) for v in vim_funcs['getpos']('v')[1:]] + eline, ecol, eoff = [int(v) for v in vim_funcs['getpos']('.')[1:]] + svcol = vim_funcs['virtcol']([sline, scol, soff]) + evcol = vim_funcs['virtcol']([eline, ecol, eoff]) + rows = abs(eline - sline) + 1 + cols = abs(ecol - scol) + 1 + vcols = abs(evcol - svcol) + 1 + return { + '^': CTRL_V_text, + 's': v_text_oneline if rows == 1 else v_text_multiline, + 'S': V_text, + 'v': v_text_oneline if rows == 1 else v_text_multiline, + 'V': V_text, + }.get(segment_info['mode'][0], '').format( + sline=sline, eline=eline, + scol=scol, ecol=ecol, + svcol=svcol, evcol=evcol, + rows=rows, cols=cols, vcols=vcols, + ) + + +@requires_segment_info +def modified_indicator(pl, segment_info, text='+'): + '''Return a file modified indicator. + + :param string text: + text to display if the current buffer is modified + ''' + return text if int(vim_getbufoption(segment_info, 'modified')) else None + + +@requires_segment_info +def tab_modified_indicator(pl, segment_info, text='+'): + '''Return a file modified indicator for tabpages. + + :param string text: + text to display if any buffer in the current tab is modified + + Highlight groups used: ``tab_modified_indicator`` or ``modified_indicator``. + ''' + for buf_segment_info in list_tabpage_buffers_segment_info(segment_info): + if int(vim_getbufoption(buf_segment_info, 'modified')): + return [{ + 'contents': text, + 'highlight_groups': ['tab_modified_indicator', 'modified_indicator'], + }] + return None + + +@requires_segment_info +def paste_indicator(pl, segment_info, text='PASTE'): + '''Return a paste mode indicator. + + :param string text: + text to display if paste mode is enabled + ''' + return text if int(vim.eval('&paste')) else None + + +@requires_segment_info +def readonly_indicator(pl, segment_info, text='RO'): + '''Return a read-only indicator. + + :param string text: + text to display if the current buffer is read-only + ''' + return text if int(vim_getbufoption(segment_info, 'readonly')) else None + + +SCHEME_RE = re.compile(b'^\\w[\\w\\d+\\-.]*(?=:)') + + +@requires_segment_info +def file_scheme(pl, segment_info): + '''Return the protocol part of the file. + + Protocol is the part of the full filename just before the colon which + starts with a latin letter and contains only latin letters, digits, plus, + period or hyphen (refer to `RFC3986 + `_ for the description of + URI scheme). If there is no such a thing ``None`` is returned, effectively + removing segment. + + .. note:: + Segment will not check whether there is ``//`` just after the + colon or if there is at least one slash after the scheme. Reason: it is + not always present. E.g. when opening file inside a zip archive file + name will look like :file:`zipfile:/path/to/archive.zip::file.txt`. + ``file_scheme`` segment will catch ``zipfile`` part here. + ''' + name = buffer_name(segment_info) + if not name: + return None + match = SCHEME_RE.match(name) + if match: + return match.group(0).decode('ascii') + + +@requires_segment_info +def file_directory(pl, segment_info, remove_scheme=True, shorten_user=True, shorten_cwd=True, shorten_home=False): + '''Return file directory (head component of the file path). + + :param bool remove_scheme: + Remove scheme part from the segment name, if present. See documentation + of file_scheme segment for the description of what scheme is. Also + removes the colon. + + :param bool shorten_user: + Shorten ``$HOME`` directory to :file:`~/`. Does not work for files with + scheme. + + :param bool shorten_cwd: + Shorten current directory to :file:`./`. Does not work for files with + scheme present. + + :param bool shorten_home: + Shorten all directories in :file:`/home/` to :file:`~user/` instead of + :file:`/home/user/`. Does not work for files with scheme present. + ''' + name = buffer_name(segment_info) + if not name: + return None + match = SCHEME_RE.match(name) + if match: + if remove_scheme: + name = name[len(match.group(0)) + 1:] # Remove scheme and colon + file_directory = vim_funcs['fnamemodify'](name, ':h') + else: + file_directory = vim_funcs['fnamemodify']( + name, + (':~' if shorten_user else '') + (':.' if shorten_cwd else '') + ':h' + ) + if not file_directory: + return None + if shorten_home and file_directory.startswith('/home/'): + file_directory = b'~' + file_directory[6:] + file_directory = file_directory.decode(segment_info['encoding'], 'powerline_vim_strtrans_error') + return file_directory + os.sep + + +@requires_segment_info +def file_name(pl, segment_info, display_no_file=False, no_file_text='[No file]'): + '''Return file name (tail component of the file path). + + :param bool display_no_file: + display a string if the buffer is missing a file name + :param str no_file_text: + the string to display if the buffer is missing a file name + + Highlight groups used: ``file_name_no_file`` or ``file_name``, ``file_name``. + ''' + name = buffer_name(segment_info) + if not name: + if display_no_file: + return [{ + 'contents': no_file_text, + 'highlight_groups': ['file_name_no_file', 'file_name'], + }] + else: + return None + return os.path.basename(name).decode(segment_info['encoding'], 'powerline_vim_strtrans_error') + + +@window_cached +def file_size(pl, suffix='B', si_prefix=False): + '''Return file size in &encoding. + + :param str suffix: + string appended to the file size + :param bool si_prefix: + use SI prefix, e.g. MB instead of MiB + :return: file size or None if the file isn’t saved or if the size is too big to fit in a number + ''' + # Note: returns file size in &encoding, not in &fileencoding. But returned + # size is updated immediately; and it is valid for any buffer + file_size = vim_funcs['line2byte'](len(vim.current.buffer) + 1) - 1 + if file_size < 0: + file_size = 0 + return humanize_bytes(file_size, suffix, si_prefix) + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_format(pl, segment_info): + '''Return file format (i.e. line ending type). + + :return: file format or None if unknown or missing file format + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'fileformat') or None + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_encoding(pl, segment_info): + '''Return file encoding/character set. + + :return: file encoding/character set or None if unknown or missing file encoding + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'fileencoding') or None + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_bom(pl, segment_info): + '''Return BOM of the current file + + :return: Byte order mark or None if unknown or missing BOM + + Divider highlight group used: ``background:divider``. + ''' + return 'bom' if vim_getbufoption(segment_info, 'bomb') else None + + +@requires_segment_info +@add_divider_highlight_group('background:divider') +def file_type(pl, segment_info): + '''Return file type. + + :return: file type or None if unknown file type + + Divider highlight group used: ``background:divider``. + ''' + return vim_getbufoption(segment_info, 'filetype') or None + + +@requires_segment_info +def window_title(pl, segment_info): + '''Return the window title. + + This currently looks at the ``quickfix_title`` window variable, + which is used by Syntastic and Vim itself. + + It is used in the quickfix theme.''' + try: + return vim_getwinvar(segment_info, 'quickfix_title') + except KeyError: + return None + + +@requires_segment_info +def line_percent(pl, segment_info, gradient=False): + '''Return the cursor position in the file as a percentage. + + :param bool gradient: + highlight the percentage with a color gradient (by default a green to red gradient) + + Highlight groups used: ``line_percent_gradient`` (gradient), ``line_percent``. + ''' + line_current = segment_info['window'].cursor[0] + line_last = len(segment_info['buffer']) + percentage = line_current * 100.0 / line_last + if not gradient: + return str(int(round(percentage))) + return [{ + 'contents': str(int(round(percentage))), + 'highlight_groups': ['line_percent_gradient', 'line_percent'], + 'gradient_level': percentage, + }] + + +@window_cached +def position(pl, position_strings={'top': 'Top', 'bottom': 'Bot', 'all': 'All'}, gradient=False): + '''Return the position of the current view in the file as a percentage. + + :param dict position_strings: + dict for translation of the position strings, e.g. ``{"top":"Oben", "bottom":"Unten", "all":"Alles"}`` + + :param bool gradient: + highlight the percentage with a color gradient (by default a green to red gradient) + + Highlight groups used: ``position_gradient`` (gradient), ``position``. + ''' + line_last = len(vim.current.buffer) + + winline_first = vim_funcs['line']('w0') + winline_last = vim_funcs['line']('w$') + if winline_first == 1 and winline_last == line_last: + percentage = 0.0 + content = position_strings['all'] + elif winline_first == 1: + percentage = 0.0 + content = position_strings['top'] + elif winline_last == line_last: + percentage = 100.0 + content = position_strings['bottom'] + else: + percentage = winline_first * 100.0 / (line_last - winline_last + winline_first) + content = str(int(round(percentage))) + '%' + + if not gradient: + return content + return [{ + 'contents': content, + 'highlight_groups': ['position_gradient', 'position'], + 'gradient_level': percentage, + }] + + +@requires_segment_info +def line_current(pl, segment_info): + '''Return the current cursor line.''' + return str(segment_info['window'].cursor[0]) + + +@requires_segment_info +def line_count(pl, segment_info): + '''Return the line count of the current buffer.''' + return str(len(segment_info['buffer'])) + + +@requires_segment_info +def col_current(pl, segment_info): + '''Return the current cursor column. + ''' + return str(segment_info['window'].cursor[1] + 1) + + +@window_cached +def virtcol_current(pl, gradient=True): + '''Return current visual column with concealed characters ignored + + :param bool gradient: + Determines whether it should show textwidth-based gradient (gradient level is ``virtcol * 100 / textwidth``). + + Highlight groups used: ``virtcol_current_gradient`` (gradient), ``virtcol_current`` or ``col_current``. + ''' + col = vim_funcs['virtcol']('.') + r = [{'contents': str(col), 'highlight_groups': ['virtcol_current', 'col_current']}] + if gradient: + textwidth = int(getbufvar('%', '&textwidth')) + r[-1]['gradient_level'] = min(col * 100 / textwidth, 100) if textwidth else 0 + r[-1]['highlight_groups'].insert(0, 'virtcol_current_gradient') + return r + + +def modified_buffers(pl, text='+ ', join_str=','): + '''Return a comma-separated list of modified buffers. + + :param str text: + text to display before the modified buffer list + :param str join_str: + string to use for joining the modified buffer list + ''' + buffer_mod_text = join_str.join(( + str(buffer.number) + for buffer in vim.buffers + if int(vim_getbufoption({'buffer': buffer, 'bufnr': buffer.number}, 'modified')) + )) + if buffer_mod_text: + return text + buffer_mod_text + return None + + +@requires_filesystem_watcher +@requires_segment_info +class VimBranchSegment(BranchSegment): + divider_highlight_group = 'branch:divider' + + @staticmethod + def get_directory(segment_info): + if vim_getbufoption(segment_info, 'buftype'): + return None + return buffer_name(segment_info) + + +branch = with_docstring(VimBranchSegment(), +'''Return the current working branch. + +:param bool status_colors: + Determines whether repository status will be used to determine highlighting. + Default: False. +:param bool ignore_statuses: + List of statuses which will not result in repo being marked as dirty. Most + useful is setting this option to ``["U"]``: this will ignore repository + which has just untracked files (i.e. repository with modified, deleted or + removed files will be marked as dirty, while just untracked files will make + segment show clean repository). Only applicable if ``status_colors`` option + is True. + +Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``. + +Divider highlight group used: ``branch:divider``. +''') + + +@requires_filesystem_watcher +@requires_segment_info +class VimStashSegment(StashSegment): + divider_highlight_group = 'stash:divider' + + @staticmethod + def get_directory(segment_info): + if vim_getbufoption(segment_info, 'buftype'): + return None + return buffer_name(segment_info) + + +stash = with_docstring(VimStashSegment(), +'''Return the number of stashes in the current working branch. + +Highlight groups used: ``stash``. +''') + + +@requires_filesystem_watcher +@requires_segment_info +def file_vcs_status(pl, segment_info, create_watcher): + '''Return the VCS status for this buffer. + + Highlight groups used: ``file_vcs_status``. + ''' + name = buffer_name(segment_info) + skip = not (name and (not vim_getbufoption(segment_info, 'buftype'))) + if not skip: + repo = guess(path=name, create_watcher=create_watcher) + if repo is not None: + status = repo.status(os.path.relpath(name, repo.directory)) + if not status: + return None + status = status.strip() + ret = [] + for status in status: + ret.append({ + 'contents': status, + 'highlight_groups': ['file_vcs_status_' + status, 'file_vcs_status'], + }) + return ret + + +trailing_whitespace_cache = None + + +@requires_segment_info +def trailing_whitespace(pl, segment_info): + '''Return the line number for trailing whitespaces + + It is advised not to use this segment in insert mode: in Insert mode it will + iterate over all lines in buffer each time you happen to type a character + which may cause lags. It will also show you whitespace warning each time you + happen to type space. + + Highlight groups used: ``trailing_whitespace`` or ``warning``. + ''' + global trailing_whitespace_cache + if trailing_whitespace_cache is None: + trailing_whitespace_cache = register_buffer_cache(defaultdict(lambda: (0, None))) + bufnr = segment_info['bufnr'] + changedtick = getbufvar(bufnr, 'changedtick') + if trailing_whitespace_cache[bufnr][0] == changedtick: + return trailing_whitespace_cache[bufnr][1] + else: + buf = segment_info['buffer'] + bws = b' \t' + sws = str(' \t') # Ignore unicode_literals and use native str. + for i in range(len(buf)): + try: + line = buf[i] + except UnicodeDecodeError: # May happen in Python 3 + if hasattr(vim, 'bindeval'): + line = vim.bindeval('getbufline({0}, {1})'.format( + bufnr, i + 1)) + has_trailing_ws = (line[-1] in bws) + else: + line = vim.eval('strtrans(getbufline({0}, {1}))'.format( + bufnr, i + 1)) + has_trailing_ws = (line[-1] in bws) + else: + has_trailing_ws = (line and line[-1] in sws) + if has_trailing_ws: + break + if has_trailing_ws: + ret = [{ + 'contents': str(i + 1), + 'highlight_groups': ['trailing_whitespace', 'warning'], + }] + else: + ret = None + trailing_whitespace_cache[bufnr] = (changedtick, ret) + return ret + + +@requires_segment_info +def tabnr(pl, segment_info, show_current=True): + '''Show tabpage number + + :param bool show_current: + If False do not show current tabpage number. This is default because + tabnr is by default only present in tabline. + ''' + try: + tabnr = segment_info['tabnr'] + except KeyError: + return None + if show_current or tabnr != current_tabpage().number: + return str(tabnr) + + +@requires_segment_info +def bufnr(pl, segment_info, show_current=True): + '''Show buffer number + + :param bool show_current: + If False do not show current window number. + ''' + bufnr = segment_info['bufnr'] + if show_current or bufnr != vim.current.buffer.number: + return str(bufnr) + + +@requires_segment_info +def winnr(pl, segment_info, show_current=True): + '''Show window number + + :param bool show_current: + If False do not show current window number. + ''' + winnr = segment_info['winnr'] + if show_current or winnr != vim.current.window.number: + return str(winnr) + + +csv_cache = None +sniffer = csv.Sniffer() + + +def detect_text_csv_dialect(text, display_name, header_text=None): + return ( + sniffer.sniff(string(text)), + sniffer.has_header(string(header_text or text)) if display_name == 'auto' else display_name, + ) + + +CSV_SNIFF_LINES = 100 +CSV_PARSE_LINES = 10 + + +if sys.version_info < (2, 7): + def read_csv(l, dialect, fin=next): + try: + return fin(csv.reader(l, dialect)) + except csv.Error as e: + if str(e) == 'newline inside string' and dialect.quotechar: + # Maybe we are inside an unfinished quoted string. Python-2.6 + # does not handle this fine + return fin(csv.reader(l[:-1] + [l[-1] + dialect.quotechar])) + else: + raise +else: + def read_csv(l, dialect, fin=next): + return fin(csv.reader(l, dialect)) + + +def process_csv_buffer(pl, buffer, line, col, display_name): + global csv_cache + if csv_cache is None: + csv_cache = register_buffer_cache(defaultdict(lambda: (None, None, None))) + try: + cur_first_line = buffer[0] + except UnicodeDecodeError: + cur_first_line = vim.eval('strtrans(getline(1))') + dialect, has_header, first_line = csv_cache[buffer.number] + if dialect is None or (cur_first_line != first_line and display_name == 'auto'): + try: + text = '\n'.join(buffer[:CSV_SNIFF_LINES]) + except UnicodeDecodeError: # May happen in Python 3 + text = vim.eval('join(map(getline(1, {0}), "strtrans(v:val)"), "\\n")'.format(CSV_SNIFF_LINES)) + try: + dialect, has_header = detect_text_csv_dialect(text, display_name) + except csv.Error as e: + pl.warn('Failed to detect csv format: {0}', str(e)) + # Try detecting using three lines only: + if line == 1: + rng = (0, line + 2) + elif line == len(buffer): + rng = (line - 3, line) + else: + rng = (line - 2, line + 1) + try: + dialect, has_header = detect_text_csv_dialect( + '\n'.join(buffer[rng[0]:rng[1]]), + display_name, + header_text='\n'.join(buffer[:4]), + ) + except csv.Error as e: + pl.error('Failed to detect csv format: {0}', str(e)) + return None, None + if len(buffer) > 2: + csv_cache[buffer.number] = dialect, has_header, cur_first_line + column_number = len(read_csv( + buffer[max(0, line - CSV_PARSE_LINES):line - 1] + [buffer[line - 1][:col]], + dialect=dialect, + fin=list, + )[-1]) or 1 + if has_header: + try: + header = read_csv(buffer[0:1], dialect=dialect) + except UnicodeDecodeError: + header = read_csv([vim.eval('strtrans(getline(1))')], dialect=dialect) + column_name = header[column_number - 1] + else: + column_name = None + return unicode(column_number), column_name + + +@requires_segment_info +def csv_col_current(pl, segment_info, display_name='auto', name_format=' ({column_name:.15})'): + '''Display CSV column number and column name + + Requires filetype to be set to ``csv``. + + :param bool or str name: + May be ``True``, ``False`` and ``"auto"``. In the first case value from + the first raw will always be displayed. In the second case it will never + be displayed. In the last case ``csv.Sniffer().has_header()`` will be + used to detect whether current file contains header in the first column. + :param str name_format: + String used to format column name (in case ``display_name`` is set to + ``True`` or ``"auto"``). Accepts ``column_name`` keyword argument. + + Highlight groups used: ``csv:column_number`` or ``csv``, ``csv:column_name`` or ``csv``. + ''' + if vim_getbufoption(segment_info, 'filetype') != 'csv': + return None + line, col = segment_info['window'].cursor + column_number, column_name = process_csv_buffer(pl, segment_info['buffer'], line, col, display_name) + if not column_number: + return None + return [{ + 'contents': column_number, + 'highlight_groups': ['csv:column_number', 'csv'], + }] + ([{ + 'contents': name_format.format(column_name=column_name), + 'highlight_groups': ['csv:column_name', 'csv'], + }] if column_name else []) + + +@requires_segment_info +def tab(pl, segment_info, end=False): + '''Mark start of the clickable region for tabpage + + :param bool end: + In place of starting region for the current tab end it. + + No highlight groups are used (literal segment). + ''' + try: + return [{ + 'contents': None, + 'literal_contents': (0, '%{tabnr}T'.format(tabnr=('' if end else segment_info['tabnr']))), + }] + except KeyError: + return None diff --git a/powerline/segments/vim/plugin/__init__.py b/powerline/segments/vim/plugin/__init__.py new file mode 100644 index 0000000..b2b9f10 --- /dev/null +++ b/powerline/segments/vim/plugin/__init__.py @@ -0,0 +1,6 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) +from pkgutil import extend_path + + +__path__ = extend_path(__path__, __name__) diff --git a/powerline/segments/vim/plugin/ale.py b/powerline/segments/vim/plugin/ale.py new file mode 100644 index 0000000..4f4bdee --- /dev/null +++ b/powerline/segments/vim/plugin/ale.py @@ -0,0 +1,52 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_global_exists +from powerline.theme import requires_segment_info + + +@requires_segment_info +def ale(segment_info, pl, err_format='ERR: ln {first_line} ({num}) ', warn_format='WARN: ln {first_line} ({num}) '): + '''Show whether ALE has found any errors or warnings + + :param str err_format: + Format string for errors. + + :param str warn_format: + Format string for warnings. + + Highlight groups used: ``ale:warning`` or ``warning``, ``ale:error`` or ``error``. + ''' + if not (vim_global_exists('ale_enabled') and int(vim.eval('g:ale_enabled'))): + return None + has_errors = int(vim.eval('ale#statusline#Count(' + str(segment_info['bufnr']) + ').total')) + if not has_errors: + return + error = None + warning = None + errors_count = 0 + warnings_count = 0 + for issue in vim.eval('ale#engine#GetLoclist(' + str(segment_info['bufnr']) + ')'): + if issue['type'] == 'E': + error = error or issue + errors_count += 1 + elif issue['type'] == 'W': + warning = warning or issue + warnings_count += 1 + segments = [] + if error: + segments.append({ + 'contents': err_format.format(first_line=error['lnum'], num=errors_count), + 'highlight_groups': ['ale:error', 'error'], + }) + if warning: + segments.append({ + 'contents': warn_format.format(first_line=warning['lnum'], num=warnings_count), + 'highlight_groups': ['ale:warning', 'warning'], + }) + return segments diff --git a/powerline/segments/vim/plugin/capslock.py b/powerline/segments/vim/plugin/capslock.py new file mode 100644 index 0000000..d2c474d --- /dev/null +++ b/powerline/segments/vim/plugin/capslock.py @@ -0,0 +1,30 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_func_exists +from powerline.theme import requires_segment_info + + +@requires_segment_info +def capslock_indicator(pl, segment_info, text='CAPS'): + '''Shows the indicator if tpope/vim-capslock plugin is enabled + + .. note:: + In the current state plugin automatically disables itself when leaving + insert mode. So trying to use this segment not in insert or replace + modes is useless. + + :param str text: + String to show when software capslock presented by this plugin is + active. + ''' + if not vim_func_exists('CapsLockStatusline'): + return None + # CapsLockStatusline() function returns an empty string when plugin is + # disabled. If it is not then string is non-empty. + return text if vim.eval('CapsLockStatusline()') else None diff --git a/powerline/segments/vim/plugin/coc.py b/powerline/segments/vim/plugin/coc.py new file mode 100644 index 0000000..290faec --- /dev/null +++ b/powerline/segments/vim/plugin/coc.py @@ -0,0 +1,51 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_command_exists +from powerline.theme import requires_segment_info + +# coc_status's format: E1 W2 +def parse_coc_status(coc_status): + # type(coc_status) is tuple + errors_count = 0 + warnings_count = 0 + if len(coc_status) <= 0: + return errors_count, warnings_count + status_str = coc_status[0] + if len(status_str) <= 0: + return errors_count, warnings_count + status_list = status_str.split(' ') + for item in status_list: + if len(item) > 0 and item[0] == 'E': + errors_count = int(item[1:]) + if len(item) > 0 and item[0] == 'W': + warnings_count = int(item[1:]) + return errors_count, warnings_count + +@requires_segment_info +def coc(segment_info, pl): + '''Show whether coc.nvim has found any errors or warnings + + Highlight groups used: ``coc:warning`` or ``warning``, ``coc:error`` or ``error``. + ''' + segments = [] + if not vim_command_exists('CocCommand'): + return segments + coc_status = vim.eval('coc#status()'), + errors_count, warnings_count = parse_coc_status(coc_status) + if errors_count > 0: + segments.append({ + 'contents': 'E:' + str(errors_count), + 'highlight_groups': ['coc:error', 'error'], + }) + if warnings_count > 0: + segments.append({ + 'contents': 'W:' + str(warnings_count), + 'highlight_groups': ['coc:warning', 'warning'], + }) + return segments diff --git a/powerline/segments/vim/plugin/commandt.py b/powerline/segments/vim/plugin/commandt.py new file mode 100644 index 0000000..7e5262e --- /dev/null +++ b/powerline/segments/vim/plugin/commandt.py @@ -0,0 +1,97 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import create_ruby_dpowerline + + +def initialize(): + global initialized + if initialized: + return + initialized = True + create_ruby_dpowerline() + vim.command(( + # When using :execute (vim.command uses the same code) one should not + # use << EOF. + ''' + ruby + if (not ($command_t.respond_to? 'active_finder')) + def $command_t.active_finder + @active_finder and @active_finder.class.name or '' + end + end + if (not ($command_t.respond_to? 'path')) + def $command_t.path + @path or '' + end + end + def $powerline.commandt_set_active_finder + ::VIM::command "let g:powerline_commandt_reply = '#{$command_t.active_finder}'" + end + def $powerline.commandt_set_path + ::VIM::command "let g:powerline_commandt_reply = '#{($command_t.path or '').gsub(/'/, "''")}'" + end + ''' + )) + + +initialized = False + + +def finder(pl): + '''Display Command-T finder name + + Requires $command_t.active_finder and methods (code above may monkey-patch + $command_t to add them). All Command-T finders have ``CommandT::`` module + prefix, but it is stripped out (actually, any ``CommandT::`` substring will + be stripped out). + + Highlight groups used: ``commandt:finder``. + ''' + initialize() + vim.command('ruby $powerline.commandt_set_active_finder') + return [{ + 'highlight_groups': ['commandt:finder'], + 'contents': vim.eval('g:powerline_commandt_reply').replace('CommandT::', '').replace('Finder::', '') + }] + + +FINDERS_WITHOUT_PATH = set(( + 'CommandT::MRUBufferFinder', + 'CommandT::BufferFinder', + 'CommandT::TagFinder', + 'CommandT::JumpFinder', + 'CommandT::Finder::MRUBufferFinder', + 'CommandT::Finder::BufferFinder', + 'CommandT::Finder::TagFinder', + 'CommandT::Finder::JumpFinder', +)) + + +def path(pl): + '''Display path used by Command-T + + Requires $command_t.active_finder and .path methods (code above may + monkey-patch $command_t to add them). + + $command_t.active_finder is required in order to omit displaying path for + finders ``MRUBufferFinder``, ``BufferFinder``, ``TagFinder`` and + ``JumpFinder`` (pretty much any finder, except ``FileFinder``). + + Highlight groups used: ``commandt:path``. + ''' + initialize() + vim.command('ruby $powerline.commandt_set_active_finder') + finder = vim.eval('g:powerline_commandt_reply') + if finder in FINDERS_WITHOUT_PATH: + return None + vim.command('ruby $powerline.commandt_set_path') + return [{ + 'highlight_groups': ['commandt:path'], + 'contents': vim.eval('g:powerline_commandt_reply') + }] diff --git a/powerline/segments/vim/plugin/nerdtree.py b/powerline/segments/vim/plugin/nerdtree.py new file mode 100644 index 0000000..f11be14 --- /dev/null +++ b/powerline/segments/vim/plugin/nerdtree.py @@ -0,0 +1,25 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import bufvar_exists +from powerline.segments.vim import window_cached + + +@window_cached +def nerdtree(pl): + '''Return directory that is shown by the current buffer. + + Highlight groups used: ``nerdtree:path`` or ``file_name``. + ''' + if not bufvar_exists(None, 'NERDTreeRoot'): + return None + path_str = vim.eval('getbufvar("%", "NERDTreeRoot").path.str()') + return [{ + 'contents': path_str, + 'highlight_groups': ['nerdtree:path', 'file_name'], + }] diff --git a/powerline/segments/vim/plugin/syntastic.py b/powerline/segments/vim/plugin/syntastic.py new file mode 100644 index 0000000..5bef3c7 --- /dev/null +++ b/powerline/segments/vim/plugin/syntastic.py @@ -0,0 +1,43 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.segments.vim import window_cached +from powerline.bindings.vim import vim_global_exists + + +@window_cached +def syntastic(pl, err_format='ERR:  {first_line} ({num}) ', warn_format='WARN:  {first_line} ({num}) '): + '''Show whether syntastic has found any errors or warnings + + :param str err_format: + Format string for errors. + + :param str warn_format: + Format string for warnings. + + Highlight groups used: ``syntastic:warning`` or ``warning``, ``syntastic:error`` or ``error``. + ''' + if not vim_global_exists('SyntasticLoclist'): + return None + has_errors = int(vim.eval('g:SyntasticLoclist.current().hasErrorsOrWarningsToDisplay()')) + if not has_errors: + return + errors = vim.eval('g:SyntasticLoclist.current().errors()') + warnings = vim.eval('g:SyntasticLoclist.current().warnings()') + segments = [] + if errors: + segments.append({ + 'contents': err_format.format(first_line=errors[0]['lnum'], num=len(errors)), + 'highlight_groups': ['syntastic:error', 'error'], + }) + if warnings: + segments.append({ + 'contents': warn_format.format(first_line=warnings[0]['lnum'], num=len(warnings)), + 'highlight_groups': ['syntastic:warning', 'warning'], + }) + return segments diff --git a/powerline/segments/vim/plugin/tagbar.py b/powerline/segments/vim/plugin/tagbar.py new file mode 100644 index 0000000..e683758 --- /dev/null +++ b/powerline/segments/vim/plugin/tagbar.py @@ -0,0 +1,51 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_command_exists, vim_get_autoload_func +from powerline.theme import requires_segment_info + + +currenttag = None +tag_cache = {} + + +@requires_segment_info +def current_tag(segment_info, pl, flags='s'): + '''Return tag that is near the cursor. + + :param str flags: + Specifies additional properties of the displayed tag. Supported values: + + * s - display complete signature + * f - display the full hierarchy of the tag + * p - display the raw prototype + + More info in the `official documentation`_ (search for + “tagbar#currenttag”). + + .. _`official documentation`: https://github.com/majutsushi/tagbar/blob/master/doc/tagbar.txt + ''' + global currenttag + global tag_cache + window_id = segment_info['window_id'] + if segment_info['mode'] == 'nc': + return tag_cache.get(window_id, (None,))[-1] + if not currenttag: + if vim_command_exists('Tagbar'): + currenttag = vim_get_autoload_func('tagbar#currenttag') + if not currenttag: + return None + else: + return None + prev_key, r = tag_cache.get(window_id, (None, None)) + key = (int(vim.eval('b:changedtick')), segment_info['window'].cursor[0]) + if prev_key and key == prev_key: + return r + r = currenttag('%s', '', flags) + tag_cache[window_id] = (key, r) + return r diff --git a/powerline/selectors/__init__.py b/powerline/selectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/powerline/selectors/vim.py b/powerline/selectors/vim.py new file mode 100644 index 0000000..d111de9 --- /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 0000000..e10692c --- /dev/null +++ b/powerline/shell.py @@ -0,0 +1,38 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +from powerline import Powerline +from powerline.lib.dict import mergedicts + + +class ShellPowerline(Powerline): + def init(self, args, **kwargs): + self.args = args + 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_override: + mergedicts(r, self.args.config_override) + return r + + def load_theme_config(self, name): + r = super(ShellPowerline, self).load_theme_config(name) + if self.args.theme_override and name in self.args.theme_override: + mergedicts(r, self.args.theme_override[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 diff --git a/powerline/theme.py b/powerline/theme.py new file mode 100644 index 0000000..b3a23a1 --- /dev/null +++ b/powerline/theme.py @@ -0,0 +1,182 @@ +# 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, get_fallback_segment +from powerline.lib.unicode import u, safe_unicode + + +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.outer_padding = int(theme_config.get('outer_padding', 1)) + self.segments = [] + self.EMPTY_SEGMENT = { + 'contents': None, + 'highlight': {'fg': False, 'bg': False, 'attrs': 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: + self.pl.prefix = segment['name'] + try: + 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) + + try: + segment['contents'] = segment['before'] + u( + segment['contents'] if segment['contents'] is not None else '' + ) + segment['after'] + except Exception as e: + self.pl.exception('Failed to compute segment contents: {0}', str(e)) + segment['contents'] = safe_unicode(segment.get('contents')) + # 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() + except Exception as e: + self.pl.exception('Failed to compute segment: {0}', str(e)) + fallback = get_fallback_segment() + fallback.update(side=side) + yield fallback diff --git a/powerline/version.py b/powerline/version.py new file mode 100644 index 0000000..f380c80 --- /dev/null +++ b/powerline/version.py @@ -0,0 +1,7 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__version__ = "2.8.3" + +def get_version(): + return __version__ diff --git a/powerline/vim.py b/powerline/vim.py new file mode 100644 index 0000000..2638c91 --- /dev/null +++ b/powerline/vim.py @@ -0,0 +1,359 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import json +import logging + +from itertools import count + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_get_func, vim_getvar, get_vim_encoding, python_to_vim +from powerline import Powerline, FailedUnicode, finish_common_config +from powerline.lib.dict import mergedicts +from powerline.lib.unicode import u + + +def _override_from(config, override_varname, key=None): + try: + overrides = vim_getvar(override_varname) + except KeyError: + return config + if key is not None: + try: + overrides = overrides[key] + except KeyError: + return config + mergedicts(config, overrides) + return config + + +class VimVarHandler(logging.Handler, object): + '''Vim-specific handler which emits messages to Vim global variables + + :param str varname: + Variable where + ''' + def __init__(self, varname): + super(VimVarHandler, self).__init__() + utf_varname = u(varname) + self.vim_varname = utf_varname.encode('ascii') + vim.command('unlet! g:' + utf_varname) + vim.command('let g:' + utf_varname + ' = []') + + def emit(self, record): + message = u(record.message) + if record.exc_text: + message += '\n' + u(record.exc_text) + vim.eval(b'add(g:' + self.vim_varname + b', ' + python_to_vim(message) + b')') + + +class VimPowerline(Powerline): + def init(self, pyeval='PowerlinePyeval', **kwargs): + super(VimPowerline, self).init('vim', **kwargs) + self.last_window_id = 1 + self.pyeval = pyeval + self.construct_window_statusline = self.create_window_statusline_constructor() + if all((hasattr(vim.current.window, attr) for attr in ('options', 'vars', 'number'))): + self.win_idx = self.new_win_idx + else: + self.win_idx = self.old_win_idx + self._vim_getwinvar = vim_get_func('getwinvar', 'bytes') + self._vim_setwinvar = vim_get_func('setwinvar') + + if sys.version_info < (3,): + def create_window_statusline_constructor(self): + window_statusline = b'%!' + str(self.pyeval) + b'(\'powerline.statusline({0})\')' + return window_statusline.format + else: + def create_window_statusline_constructor(self): + startstr = b'%!' + self.pyeval.encode('ascii') + b'(\'powerline.statusline(' + endstr = b')\')' + return lambda idx: ( + startstr + str(idx).encode('ascii') + endstr + ) + + create_window_statusline_constructor.__doc__ = ( + '''Create function which returns &l:stl value being given window index + + Created function must return :py:class:`bytes` instance because this is + what ``window.options['statusline']`` returns (``window`` is + :py:class:`vim.Window` instance). + + :return: + Function with type ``int → bytes``. + ''' + ) + + 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 + + get_encoding = staticmethod(get_vim_encoding) + + def load_main_config(self): + main_config = _override_from(super(VimPowerline, self).load_main_config(), 'powerline_config_overrides') + try: + use_var_handler = bool(int(vim_getvar('powerline_use_var_handler'))) + except KeyError: + use_var_handler = False + if use_var_handler: + main_config.setdefault('common', {}) + main_config['common'] = finish_common_config(self.get_encoding(), main_config['common']) + main_config['common']['log_file'].append(['powerline.vim.VimVarHandler', [['powerline_log_messages']]]) + return main_config + + def load_theme_config(self, name): + return _override_from( + 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) or pyeval == 'PowerlinePyeval': + vim.command((''' + function! PowerlinePyeval(e) + {pycmd} powerline.do_pyeval() + endfunction + ''').format(pycmd=pycmd)) + pyeval = 'PowerlinePyeval' + + self.pyeval = pyeval + self.construct_window_statusline = self.create_window_statusline_constructor() + + self.update_renderer() + __main__.powerline = self + + try: + 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() + except UnicodeDecodeError: + # vim.current.buffer.name may raise UnicodeDecodeError when using + # Python-3*. Fortunately, this means that current buffer is not + # empty buffer, so the above condition should be False. + pass + + # 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) + + 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 + + def new_win_idx(self, window_id): + r = None + + for window in vim.windows: + try: + curwindow_id = window.vars['powerline_window_id'] + if r is not None and curwindow_id == window_id: + raise KeyError + except KeyError: + curwindow_id = self.last_window_id + self.last_window_id += 1 + window.vars['powerline_window_id'] = curwindow_id + statusline = self.construct_window_statusline(curwindow_id) + if window.options['statusline'] != statusline: + window.options['statusline'] = statusline + if curwindow_id == window_id if window_id else window is vim.current.window: + r = (window, curwindow_id, window.number) + return r + + def old_win_idx(self, window_id): + r = None + for winnr, window in zip(count(1), vim.windows): + curwindow_id = self._vim_getwinvar(winnr, 'powerline_window_id') + if curwindow_id and not (r is not None and curwindow_id == window_id): + curwindow_id = int(curwindow_id) + else: + curwindow_id = self.last_window_id + self.last_window_id += 1 + self._vim_setwinvar(winnr, 'powerline_window_id', curwindow_id) + statusline = self.construct_window_statusline(curwindow_id) + if self._vim_getwinvar(winnr, '&statusline') != statusline: + self._vim_setwinvar(winnr, '&statusline', statusline) + if curwindow_id == window_id if window_id else window is vim.current.window: + r = (window, curwindow_id, winnr) + return r + + def statusline(self, window_id): + window, window_id, winnr = self.win_idx(window_id) or (None, None, None) + if not window: + return FailedUnicode('No window {0}'.format(window_id)) + return self.render(window, window_id, winnr) + + def tabline(self): + r = self.win_idx(None) + + if r: + return self.render(*r, is_tabline=True) + else: + win = vim.current.window + r = ( + win, + win.vars.get('powerline_window_id', self.last_window_id), + win.number, + ) + return self.render(*r, is_tabline=True) + + def new_window(self): + return self.render(*self.win_idx(None)) + + @staticmethod + def do_pyeval(): + '''Evaluate python string passed to PowerlinePyeval + + Is here to reduce the number of requirements to __main__ globals to just + one powerline object (previously it required as well vim and json). + ''' + 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 0000000..f2ffc12 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +powerline diff --git a/scripts/powerline-config b/scripts/powerline-config new file mode 100755 index 0000000..1fb2029 --- /dev/null +++ b/scripts/powerline-config @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +try: + from powerline.commands.config import get_argparser +except ImportError: + import sys + import os + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__))))) + from powerline.commands.config import get_argparser + +import powerline.bindings.config as config + + +if __name__ == '__main__': + parser = get_argparser() + 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 0000000..f15ac69 --- /dev/null +++ b/scripts/powerline-daemon @@ -0,0 +1,495 @@ +#!/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 +import fcntl +import atexit +import stat + +from argparse import ArgumentParser +from select import select +from signal import signal, SIGTERM +from time import sleep +from functools import partial +from io import BytesIO +from threading import Event +from itertools import chain +from logging import StreamHandler + +from powerline.shell import ShellPowerline +from powerline.commands.main import finish_args, write_output +from powerline.lib.monotonic import monotonic +from powerline.lib.encoding import get_preferred_output_encoding, get_preferred_arguments_encoding, get_unicode_writer +from powerline.bindings.wm import wm_threads + +from powerline.commands.main import get_argparser as get_main_argparser +from powerline.commands.daemon import get_argparser as get_daemon_argparser + + +USE_FILESYSTEM = not sys.platform.lower().startswith('linux') + + +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()) + + +EOF = b'EOF\0\0' + + +class State(object): + __slots__ = ('powerlines', 'logger', 'config_loader', 'started_wm_threads', + 'ts_shutdown_event') + + def __init__(self, **kwargs): + self.logger = None + self.config_loader = None + self.started_wm_threads = {} + self.powerlines = {} + self.ts_shutdown_event = Event() + + +HOME = os.path.expanduser('~') + + +class NonDaemonShellPowerline(ShellPowerline): + def get_log_handler(self): + return StreamHandler() + + +def start_wm(args, environ, cwd, is_daemon, state): + wm_name = args.ext[0][3:] + if wm_name in state.started_wm_threads: + return b'' + thread_shutdown_event = Event() + thread = wm_threads[wm_name]( + thread_shutdown_event=thread_shutdown_event, + pl_shutdown_event=state.ts_shutdown_event, + pl_config_loader=state.config_loader, + ) + thread.start() + state.started_wm_threads[wm_name] = (thread, thread_shutdown_event) + return b'' + + +def render(args, environ, cwd, is_daemon, state): + segment_info = { + 'getcwd': lambda: cwd, + 'home': environ.get('HOME', HOME), + 'environ': environ, + 'args': args, + } + key = ( + args.ext[0], + args.renderer_module, + tuple(args.config_override) if args.config_override else None, + tuple(args.theme_override) if args.theme_override else None, + tuple(args.config_path) if args.config_path else None, + environ.get('POWERLINE_THEME_OVERRIDES', ''), + environ.get('POWERLINE_CONFIG_OVERRIDES', ''), + environ.get('POWERLINE_CONFIG_PATHS', ''), + ) + + PowerlineClass = ShellPowerline if is_daemon else NonDaemonShellPowerline + powerline = None + try: + powerline = state.powerlines[key] + except KeyError: + try: + powerline = state.powerlines[key] = PowerlineClass( + args, + logger=state.logger, + config_loader=state.config_loader, + run_once=False, + shutdown_event=state.ts_shutdown_event, + ) + if state.logger is None: + state.logger = powerline.logger + if state.config_loader is None: + state.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, get_unicode_writer(stream=s)) + 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 + + +def safe_bytes(o, encoding=get_preferred_output_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, parser, encoding=get_preferred_arguments_encoding()): + 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:]))) + cwd = cwd or environ.get('PWD', '/') + return shell_args, environ, cwd + + +def get_answer(req, is_daemon, argparser, state): + try: + args, environ, cwd = parse_args(req, argparser) + finish_args(argparser, environ, args, is_daemon=True) + if args.ext[0].startswith('wm.'): + return safe_bytes(start_wm(args, environ, cwd, is_daemon, state)) + else: + return safe_bytes(render(args, environ, cwd, is_daemon, state)) + except Exception as e: + return safe_bytes(str(e)) + + +def do_one(sock, read_sockets, write_sockets, result_map, is_daemon, argparser, + state): + 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 = get_answer(req, is_daemon, argparser, state) + 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 shutdown(sock, read_sockets, write_sockets, state): + '''Perform operations necessary for nicely shutting down daemon + + Specifically it + + #. Closes all sockets. + #. Notifies segments based on + :py:class:`powerline.lib.threaded.ThreadedSegment` and WM-specific + threads that daemon is shutting down. + #. Waits for threads to finish, but no more then 2 seconds total. + #. Waits so that total execution time of this function is 2 seconds in order + to allow ThreadedSegments to finish. + ''' + total_wait_time = 2 + shutdown_start_time = monotonic() + + for s in chain((sock,), read_sockets, write_sockets): + s.close() + + # Notify ThreadedSegments + state.ts_shutdown_event.set() + for thread, shutdown_event in state.started_wm_threads.values(): + shutdown_event.set() + + for thread, shutdown_event in state.started_wm_threads.values(): + wait_time = total_wait_time - (monotonic() - shutdown_start_time) + if wait_time > 0: + thread.join(wait_time) + + wait_time = total_wait_time - (monotonic() - shutdown_start_time) + sleep(wait_time) + + +def main_loop(sock, is_daemon): + sock.listen(128) + sock.setblocking(0) + + read_sockets, write_sockets = set(), set() + result_map = {} + parser = get_main_argparser(NonInteractiveArgParser) + state = State() + try: + try: + while True: + do_one( + sock, read_sockets, write_sockets, result_map, + is_daemon=is_daemon, + argparser=parser, + state=state, + ) + except KeyboardInterrupt: + raise SystemExit(0) + except SystemExit as e: + shutdown(sock, read_sockets, write_sockets, state) + raise e + return 0 + + +def daemonize(stdin=os.devnull, stdout=os.devnull, stderr=os.devnull): + try: + pid = os.fork() + if pid > 0: + # exit first parent + raise SystemExit(0) + except OSError as e: + sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + raise SystemExit(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 + raise SystemExit(0) + except OSError as e: + sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) + raise SystemExit(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()) + return True + + +def check_existing(address): + 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(address): + 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(pidfile, 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(pidfile): + 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, pidfile, fd) + signal(SIGTERM, cleanup) + atexit.register(cleanup) + return fd + + +def main(): + parser = get_daemon_argparser() + args = parser.parse_args() + is_daemon = False + address = None + pidfile = None + + 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 args.foreground or args.replace: + parser.error('--kill and --foreground/--replace cannot be used together') + if kill_daemon(address): + 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(address): + 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 + is_daemon = daemonize() + + if USE_FILESYSTEM: + # Create a locked pid file containing the daemon’s PID + if lockpidfile(pidfile) 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(address) + 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 not USE_FILESYSTEM and not args.foreground: + # We daemonize on linux + is_daemon = daemonize() + + return main_loop(sock, is_daemon) + + +if __name__ == '__main__': + main() diff --git a/scripts/powerline-lint b/scripts/powerline-lint new file mode 100755 index 0000000..f665ba1 --- /dev/null +++ b/scripts/powerline-lint @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys + +from powerline.lint import check +from powerline.commands.lint import get_argparser + + +if __name__ == '__main__': + args = get_argparser().parse_args() + sys.exit(check(args.config_path, args.debug)) diff --git a/scripts/powerline-release.py b/scripts/powerline-release.py new file mode 100755 index 0000000..42381bd --- /dev/null +++ b/scripts/powerline-release.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import argparse +import codecs +import os +import re + +from subprocess import check_output, check_call, CalledProcessError +from getpass import getpass + +from github import Github + + +OVERLAY_NAME = 'raiagent' +OVERLAY = 'leycec/' + OVERLAY_NAME +OVERLAY_BRANCH_FORMAT = 'powerline-release-{0}' + + +def parse_version(s): + if s == ('+' * len(s)): + try: + last_version = next(iter(sorted([ + tuple(map(int, tag.split('.'))) + for tag in check_output(['git', 'tag', '-l', '[0-9]*.*']).split('\n')[:-1] + ], reverse=True))) + except StopIteration: + raise ValueError('No existing versions found') + + version = [] + for i in range(len(s) - 1): + try: + version.append(last_version[i]) + except IndexError: + version.append(0) + + try: + version.append(last_version[len(s) - 1] + 1) + except IndexError: + version.append(1) + + if len(version) == 1: + version.append(0) + + return tuple(version) + else: + return tuple(map(int, s.split('.'))) + + +def setup_py_filter(filter_func): + with codecs.open('.setup.py.new', 'w', encoding='utf-8') as NS: + with codecs.open('setup.py', 'r', encoding='utf-8') as OS: + for line in OS: + line = filter_func(line) + NS.write(line) + + os.unlink('setup.py') + os.rename('.setup.py.new', 'setup.py') + + +def setup_py_develop_filter(line, version_string): + if line.startswith('\tbase_version = '): + line = '\tbase_version = \'' + version_string + '\'\n' + return line + + +def setup_py_master_filter(line, version_string): + if line.startswith('\tversion='): + line = '\tversion=\'' + version_string + '\',\n' + elif 'Development Status' in line: + line = '\t\t\'Development Status :: 5 - Production/Stable\',\n' + return line + + +def merge(version_string, rev, **kwargs): + check_call(['git', 'checkout', rev]) + + temp_branch_name = 'release-' + version_string + check_call(['git', 'checkout', '-b', temp_branch_name]) + setup_py_filter(lambda line: setup_py_develop_filter(line, version_string)) + check_call(['git', 'add', 'setup.py']) + check_call(['git', 'commit', '-m', 'Update base version']) + check_call(['git', 'checkout', rev]) + check_call(['git', 'merge', '--no-ff', + '--strategy', 'recursive', + '--strategy-option', 'theirs', + '--commit', + '-m', 'Merge branch \'{0}\' into {1}'.format(temp_branch_name, rev), + temp_branch_name]) + check_call(['git', 'branch', '-d', temp_branch_name]) + + rev = check_output(['git', 'rev-parse', 'HEAD']).strip() + + check_call(['git', 'checkout', 'master']) + try: + check_call(['git', 'merge', '--no-ff', '--no-commit', '--log', rev]) + except CalledProcessError: + check_call(['git', 'mergetool', '--tool', 'vimdiff2']) + + setup_py_filter(lambda line: setup_py_master_filter(line, version_string)) + check_call(['git', 'add', 'setup.py']) + + check_call(['git', 'commit']) + check_call(['git', 'tag', '-m', 'Release ' + version_string, '-a', version_string]) + + +def push(version_string, rev, **kwargs): + check_call(['git', 'push', 'upstream', 'master']) + check_call(['git', 'push', 'upstream', version_string]) + check_call(['git', 'push', 'upstream', rev]) + + +def upload(**args): + check_call(['python', 'setup.py', 'sdist', 'upload']) + check_call(['python', 'setup.py', 'upload_docs']) + + +gh = None + + +def get_gh(user, password): + global gh + if not gh: + gh = Github(user, password) + return gh + + +def create_ebuilds(version_string, overlay, user, **kwargs): + overlay_url = 'git://github.com/' + OVERLAY + if not os.path.isdir(overlay): + check_call(['git', 'clone', overlay_url, overlay]) + check_call(['git', 'checkout', 'master'], cwd=overlay) + check_call(['git', 'pull', '--ff-only', overlay_url, 'master'], cwd=overlay) + branch = OVERLAY_BRANCH_FORMAT.format(version_string) + check_call(['git', 'branch', branch], cwd=overlay) + check_call(['git', 'checkout', branch], cwd=overlay) + os.environ['DISTDIR'] = '/tmp/powerline-distfiles' + if not os.path.isdir(os.environ['DISTDIR']): + os.mkdir(os.environ['DISTDIR']) + new_files = [] + for category, pn in ( + ('app-misc', 'powerline'), + ('app-vim', 'powerline-vim'), + ): + pdir = os.path.join(overlay, category, pn) + live_ebuild = None + for ebuild in os.listdir(pdir): + if ebuild.endswith('.ebuild') and '9999' in ebuild: + live_ebuild_base = ebuild + live_ebuild = os.path.join(pdir, ebuild) + break + assert(live_ebuild) + vcur = os.path.join(pdir, '{0}-{1}.ebuild'.format(pn, version_string)) + if pn == 'powerline-vim': + with open(live_ebuild) as LEF: + with open(vcur, 'w') as F: + dropnext = False + for line in LEF: + if line.startswith('EGIT'): + # Drop all EGIT_… and the next empty line + dropnext = True + next_re = re.compile('^$') + continue + if dropnext: + assert(next_re.match(line)) + dropnext = False + continue + if line.startswith('# Note the lack of an assignment to ${S}'): + next_re = re.compile('^#') + dropnext = True + line = 'S="${WORKDIR}/${MY_P}"\n' + if line.startswith('inherit'): + line = line.replace(' git-r3', '') + line += '\n' + line += 'MY_PN="powerline-status"\n' + line += 'MY_P="${MY_PN}-${PV}"' + line += '\n' + elif line.startswith('HOMEPAGE'): + line += 'SRC_URI="mirror://pypi/p/${MY_PN}/${MY_P}.tar.gz"\n' + elif line.startswith('KEYWORDS'): + line = 'KEYWORDS="~amd64 ~ppc ~x86 ~x86-fbsd"\n' + F.write(line) + else: + os.symlink(live_ebuild_base, vcur) + new_files.append(vcur) + check_call(['ebuild', vcur, 'manifest']) + new_files.append(os.path.join(pdir, 'Manifest')) + check_call(['git', 'add', '--'] + new_files, cwd=overlay) + check_call(['git', 'commit'] + new_files + ['-m', 'powerline*: Release {0}'.format(version_string)], + cwd=overlay) + check_call(['git', 'push', '-f', 'git@github.com:{0}/{1}'.format(user, OVERLAY_NAME), branch], cwd=overlay) + + +def update_overlay(version_string, user, password, **kwargs): + gh = get_gh(user, password) + overlay = gh.get_repo(OVERLAY) + overlay.create_pull( + title='New powerline version: ' + version_string, + body='Add ebuilds for new powerline version\n\n---\n\nCreated automatically by release script', + base='master', + head=user + ':' + OVERLAY_BRANCH_FORMAT.format(version_string), + ) + + +stages = ( + ('merge', merge), + ('push', push), + ('upload', upload), + ('create_ebuilds', create_ebuilds), + ('update_overlay', update_overlay), +) + + +def create_release(version, user, password=None, run_stages=None, **kwargs): + version_string = '.'.join((str(v) for v in version)) + if not password: + password = getpass('Password for {0}: '.format(user)) + for stname, stfunc in stages: + if run_stages is None or stname in run_stages: + stfunc(version_string=version_string, user=user, password=password, **kwargs) + + +p = argparse.ArgumentParser(description='Powerline release script') +p.add_argument('-u', '--user', type=str, metavar='USER', help='Github username.', required=True) +p.add_argument('-v', '--version', type=parse_version, metavar='VERSION', help='Use given version for the release. If version contains only `+\' signs then it will increase latest version number: one `+\' increases major version number (e.g. 1.2.3 -> 2.0), `++\' increases minor version number (e.g. 1.2.3 -> 1.3), `+++\' increases patch level (e.g. 1.2.3 -> 1.2.4). Defaults to `+++\'.', default='+++') +p.add_argument('-r', '--rev', metavar='COMMIT', help='Use given revision for the release. Defaults to `develop\'.', default='develop') +p.add_argument('-s', '--stages', action='append', metavar='STAGE', help='Only run one of the given stages (default to all).', choices=tuple((stname for stname, stfunc in stages))) +p.add_argument('-p', '--password', type=str, metavar='PASS', help='Github password. You will be prompted if it is not supplied.') +p.add_argument('-o', '--overlay', type=str, metavar='PATH', help='Location of the local clone of the {0} overlay. If provided directory does not exist it will be created by “git clone”. Defaults to /tmp/powerline-{0}.'.format(OVERLAY_NAME), default='/tmp/powerline-' + OVERLAY_NAME) + + +if __name__ == '__main__': + args = p.parse_args() + create_release( + version=args.version, + rev=args.rev, + user=args.user, + password=args.password, + overlay=args.overlay, + run_stages=args.stages, + ) diff --git a/scripts/powerline-render b/scripts/powerline-render new file mode 100755 index 0000000..8b71b00 --- /dev/null +++ b/scripts/powerline-render @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os + +try: + from powerline.shell import ShellPowerline +except ImportError: + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__))))) + from powerline.shell import ShellPowerline + +from powerline.commands.main import get_argparser, finish_args, write_output +from powerline.lib.encoding import get_unicode_writer + + +if sys.version_info < (3,): + write = sys.stdout.write +else: + write = sys.stdout.buffer.write + + +if __name__ == '__main__': + parser = get_argparser() + args = parser.parse_args() + finish_args(parser, os.environ, args) + powerline = ShellPowerline(args, run_once=True) + segment_info = {'args': args, 'environ': os.environ} + write_output(args, powerline, segment_info, get_unicode_writer()) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0a105dc --- /dev/null +++ b/setup.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import logging +import shlex +import subprocess + +from setuptools import setup, find_packages + +from powerline.version import get_version + +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 + cflags = os.environ.get('CFLAGS', str('-O3')) + # A normal split would do a split on each space which might be incorrect. The + # shlex will not split if a space occurs in an arguments value. + subprocess.check_call(compiler + shlex.split(cflags) + ['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.shell import which + can_use_scripts = True + 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') + else: + print('Using powerline.py script instead of C version') + shutil.copyfile('client/powerline.py', 'scripts/powerline') +else: + can_use_scripts = False + +setup( + name='powerline-status', + version=get_version(), + 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 :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + download_url='https://github.com/powerline/powerline/archive/develop.zip', + author='Kim Silkebaekken', + author_email='kim.silkebaekken+vim@gmail.com', + url='https://github.com/powerline/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=['argparse'] if OLD_PYTHON else [], + 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 0000000..e69de29 diff --git a/tests/install.sh b/tests/install.sh new file mode 100755 index 0000000..6925a34 --- /dev/null +++ b/tests/install.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +set -e +set -x + +remote_master_hex() { + local url="$1" + git ls-remote "$url" refs/heads/master | cut -f1 +} + +checkout_cached_dir() { + local url="$1" + local target="$2" + if ! test -e "$target/.version" || \ + test "$(cat "$target/.version")" != "$(remote_master_hex "$url")" ; then + rm -rf "$target" + fi + if ! test -d "$target" ; then + git clone --depth=1 "$url" "$target" + git rev-parse HEAD > "$target/.version" + rm -rf "$target"/.git + fi +} + +checkout_cached_dir git://github.com/powerline/bot-ci tests/bot-ci +checkout_cached_dir git://github.com/powerline/deps tests/bot-ci/deps + +. tests/bot-ci/scripts/common/main.sh + +mkdir -p "$HOME/opt" + +if test -n "$USE_UCS2_PYTHON" ; then + if test "$UCS2_PYTHON_VARIANT" = "2.6" ; then + pip install 'virtualenvwrapper==4.6.0' + else + pip install virtualenvwrapper + fi + set +e + . virtualenvwrapper.sh + set -e + archive="${PWD:-$(pwd)}/tests/bot-ci/deps/cpython-ucs2/cpython-ucs2-${UCS2_PYTHON_VARIANT}.tar.gz" + sh -c "cd $HOME/opt && tar xzf $archive" + PYTHON="$HOME/opt/cpython-ucs2-$UCS2_PYTHON_VARIANT/bin/python$UCS2_PYTHON_VARIANT" + export LD_LIBRARY_PATH="$HOME/opt/cpython-ucs2-$UCS2_PYTHON_VARIANT/lib${LD_LIBRARY_PATH:+:}${LD_LIBRARY_PATH}" + set +e + mkvirtualenv -p "$PYTHON" cpython-ucs2-$UCS2_PYTHON_VARIANT + set -e + . tests/bot-ci/scripts/common/main.sh + pip install --verbose --verbose --verbose . + if test "$UCS2_PYTHON_VARIANT" = "2.6" ; then + rm tests/bot-ci/deps/wheels/ucs2-CPython-${UCS2_PYTHON_VARIANT}*/pyuv*.whl + fi + pip install --no-deps tests/bot-ci/deps/wheels/ucs2-CPython-${UCS2_PYTHON_VARIANT}*/*.whl +else + pip install --verbose --verbose --verbose . + # FIXME Uv watcher sometimes misses events and INotify is not available in + # Python-2.6, thus pyuv should be removed in order for VCS tests to + # pass. + if test "$PYTHON_VERSION_MAJOR" -eq 2 && test "$PYTHON_VERSION_MINOR" -lt 7 ; then + rm tests/bot-ci/deps/wheels/$PYTHON_SUFFIX/pyuv*.whl + fi + pip install --no-deps tests/bot-ci/deps/wheels/$PYTHON_SUFFIX/*.whl +fi +if test "$PYTHON_IMPLEMENTATION" = "CPython" ; then + archive="${PWD:-$(pwd)}/tests/bot-ci/deps/zpython/zsh-${PYTHON_MM}${USE_UCS2_PYTHON:+-ucs2}.tar.gz" + sh -c "cd $HOME/opt && tar xzf $archive" +fi + +archive="${PWD:-$(pwd)}/tests/bot-ci/deps/fish/fish.tar.gz" +sh -c "cd $HOME/opt && tar xzf $archive" + +mkdir tests/vim-plugins + +for archive in "$ROOT"/tests/bot-ci/deps/vim-plugins/*.tar.gz ; do + ( + cd tests/vim-plugins + tar -xzvf "$archive" + ) +done + +true diff --git a/tests/modules/__init__.py b/tests/modules/__init__.py new file mode 100644 index 0000000..5d671b6 --- /dev/null +++ b/tests/modules/__init__.py @@ -0,0 +1,95 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os + +if sys.version_info < (2, 7): + from unittest2 import TestCase as _TestCase # NOQA + from unittest2 import main as _main # NOQA + from unittest2.case import SkipTest # NOQA +else: + from unittest import TestCase as _TestCase # NOQA + from unittest import main as _main # NOQA + from unittest.case import SkipTest # NOQA + +from tests.modules.lib import PowerlineSingleTest + + +class PowerlineDummyTest(object): + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def fail(self, *args, **kwargs): + pass + + def exception(self, *args, **kwargs): + pass + + +class PowerlineTestSuite(object): + def __init__(self, name): + self.name = name + self.suite = '' + + def __enter__(self): + self.saved_current_suite = os.environ['POWERLINE_CURRENT_SUITE'] + os.environ['POWERLINE_CURRENT_SUITE'] = ( + self.saved_current_suite + '/' + self.name) + self.suite = self.saved_current_suite + '/' + self.name + return self + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is not None: + self.exception( + 'suite_noexcept', + 'Exception while running test suite: {0!r}'.format(exc_value), + ) + os.environ['POWERLINE_CURRENT_SUITE'] = self.saved_current_suite + + def record_test_failure(self, fail_char, test_name, message, allow_failure=False): + if allow_failure: + fail_char = 'A' + fail_char + full_msg = '{fail_char} {suite}|{test_name} :: {message}'.format( + fail_char=fail_char, + suite=self.suite, + test_name=test_name, + message=message, + ) + with open(os.environ['FAILURES_FILE'], 'a') as ffd: + ffd.write(full_msg + '\n') + return False + + def exception(self, test_name, message, allow_failure=False): + return self.record_test_failure('E', test_name, message, allow_failure) + + def fail(self, test_name, message, allow_failure=False): + return self.record_test_failure('F', test_name, message, allow_failure) + + def test(self, name, attempts_left=0): + if not attempts_left: + return PowerlineSingleTest(self, name) + else: + return PowerlineDummyTest() + + def subsuite(self, name): + return PowerlineTestSuite(name) + + +suite = None + + +def main(*args, **kwargs): + global suite + suite = PowerlineTestSuite(sys.argv[0]) + _main(*args, **kwargs) + + +class TestCase(_TestCase): + def fail(self, msg=None): + suite.fail(self.__class__.__name__, + msg or 'Test failed without message') + super(TestCase, self).fail(*args, **kwargs) diff --git a/tests/modules/lib/__init__.py b/tests/modules/lib/__init__.py new file mode 100644 index 0000000..f6df920 --- /dev/null +++ b/tests/modules/lib/__init__.py @@ -0,0 +1,191 @@ +# 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)) + + def __nonzero__(self): + return bool(self.exceptions or self.errors or self.warns) + + __bool__ = __nonzero__ + + +class Args(object): + theme_override = {} + config_override = {} + 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://ipv6.icanhazip.com'): + return '2001:4801:7818:6:abc5:ba2c:ff10:275f' + + elif query_url.startswith('https://freegeoip.app/json/'): + return '{"ip":"82.145.55.16","country_code":"DE","country_name":"Germany","region_code":"NI","region_name":"Lower Saxony","city":"Meppen","zip_code":"49716","time_zone":"Europe/Berlin","latitude":52.6833,"longitude":7.3167,"metro_code":0}' + elif query_url.startswith('http://geoip.nekudo.com/api/'): + return '{"city":"Meppen","country":{"name":"Germany", "code":"DE"},"location":{"accuracy_radius":100,"latitude":52.6833,"longitude":7.3167,"time_zone":"Europe\/Berlin"},"ip":"82.145.55.16"}' + elif query_url.startswith('http://query.yahooapis.com/v1/public/'): + if 'Meppen' in query_url or '52.6833' in query_url: + return r'{"query":{"count":1,"created":"2016-05-13T19:43:18Z","lang":"en-US","results":{"channel":{"units":{"distance":"mi","pressure":"in","speed":"mph","temperature":"C"},"title":"Yahoo! Weather - Meppen, NI, DE","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-674836/","description":"Yahoo! Weather for Meppen, NI, DE","language":"en-us","lastBuildDate":"Fri, 13 May 2016 09:43 PM CEST","ttl":"60","location":{"city":"Meppen","country":"Germany","region":" NI"},"wind":{"chill":"55","direction":"350","speed":"25"},"atmosphere":{"humidity":"57","pressure":"1004.0","rising":"0","visibility":"16.1"},"astronomy":{"sunrise":"5:35 am","sunset":"9:21 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 Meppen, NI, DE at 08:00 PM CEST","lat":"52.68993","long":"7.29115","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-674836/","pubDate":"Fri, 13 May 2016 08:00 PM CEST","condition":{"code":"23","date":"Fri, 13 May 2016 08:00 PM CEST","temp":"14","text":"Breezy"},"forecast":[{"code":"30","date":"13 May 2016","day":"Fri","high":"71","low":"48","text":"Partly Cloudy"},{"code":"28","date":"14 May 2016","day":"Sat","high":"54","low":"44","text":"Mostly Cloudy"},{"code":"11","date":"15 May 2016","day":"Sun","high":"55","low":"43","text":"Showers"},{"code":"28","date":"16 May 2016","day":"Mon","high":"54","low":"42","text":"Mostly Cloudy"},{"code":"28","date":"17 May 2016","day":"Tue","high":"57","low":"43","text":"Mostly Cloudy"},{"code":"12","date":"18 May 2016","day":"Wed","high":"62","low":"45","text":"Rain"},{"code":"28","date":"19 May 2016","day":"Thu","high":"63","low":"48","text":"Mostly Cloudy"},{"code":"28","date":"20 May 2016","day":"Fri","high":"67","low":"50","text":"Mostly Cloudy"},{"code":"30","date":"21 May 2016","day":"Sat","high":"71","low":"50","text":"Partly Cloudy"},{"code":"30","date":"22 May 2016","day":"Sun","high":"74","low":"54","text":"Partly Cloudy"}],"description":"\n
\nCurrent Conditions:\n
Breezy\n
\n
\nForecast:\n
Fri - Partly Cloudy. High: 71Low: 48\n
Sat - Mostly Cloudy. High: 54Low: 44\n
Sun - Showers. High: 55Low: 43\n
Mon - Mostly Cloudy. High: 54Low: 42\n
Tue - Mostly Cloudy. High: 57Low: 43\n
\n
\nFull Forecast at Yahoo! Weather\n
\n
\n(provided by The Weather Channel)\n
\n]]>","guid":{"isPermaLink":"false"}}}}}}' + elif 'Moscow' in query_url: + return r'{"query":{"count":1,"created":"2016-05-13T19:47:01Z","lang":"en-US","results":{"channel":{"units":{"distance":"mi","pressure":"in","speed":"mph","temperature":"C"},"title":"Yahoo! Weather - Moscow, Moscow Federal City, RU","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-2122265/","description":"Yahoo! Weather for Moscow, Moscow Federal City, RU","language":"en-us","lastBuildDate":"Fri, 13 May 2016 10:47 PM MSK","ttl":"60","location":{"city":"Moscow","country":"Russia","region":" Moscow Federal City"},"wind":{"chill":"45","direction":"80","speed":"11"},"atmosphere":{"humidity":"52","pressure":"993.0","rising":"0","visibility":"16.1"},"astronomy":{"sunrise":"4:19 am","sunset":"8:34 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 Moscow, Moscow Federal City, RU at 09:00 PM MSK","lat":"55.741638","long":"37.605061","link":"http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-2122265/","pubDate":"Fri, 13 May 2016 09:00 PM MSK","condition":{"code":"33","date":"Fri, 13 May 2016 09:00 PM MSK","temp":"9","text":"Mostly Clear"},"forecast":[{"code":"30","date":"13 May 2016","day":"Fri","high":"62","low":"41","text":"Partly Cloudy"},{"code":"30","date":"14 May 2016","day":"Sat","high":"64","low":"43","text":"Partly Cloudy"},{"code":"30","date":"15 May 2016","day":"Sun","high":"63","low":"44","text":"Partly Cloudy"},{"code":"12","date":"16 May 2016","day":"Mon","high":"60","low":"47","text":"Rain"},{"code":"12","date":"17 May 2016","day":"Tue","high":"64","low":"48","text":"Rain"},{"code":"28","date":"18 May 2016","day":"Wed","high":"67","low":"48","text":"Mostly Cloudy"},{"code":"12","date":"19 May 2016","day":"Thu","high":"68","low":"49","text":"Rain"},{"code":"39","date":"20 May 2016","day":"Fri","high":"66","low":"50","text":"Scattered Showers"},{"code":"39","date":"21 May 2016","day":"Sat","high":"69","low":"49","text":"Scattered Showers"},{"code":"30","date":"22 May 2016","day":"Sun","high":"73","low":"50","text":"Partly Cloudy"}],"description":"\n
\nCurrent Conditions:\n
Mostly Clear\n
\n
\nForecast:\n
Fri - Partly Cloudy. High: 62Low: 41\n
Sat - Partly Cloudy. High: 64Low: 43\n
Sun - Partly Cloudy. High: 63Low: 44\n
Mon - Rain. High: 60Low: 47\n
Tue - Rain. High: 64Low: 48\n
\n
\nFull Forecast at Yahoo! Weather\n
\n
\n(provided by The Weather Channel)\n
\n]]>","guid":{"isPermaLink":"false"}}}}}}' + elif query_url.startswith('https://api.openweathermap.org/data/2.5/'): + if 'Meppen' in query_url or '52.6833' in query_url: + return r'{"coord":{"lon":7.29,"lat":52.69},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"base":"stations","main":{"temp":293.15,"feels_like":295.16,"temp_min":293.15,"temp_max":295.37,"pressure":1018,"humidity":77},"visibility":10000,"wind":{"speed":1.12,"deg":126},"clouds":{"all":0},"dt":1600196220,"sys":{"type":1,"id":1871,"country":"DE","sunrise":1600146332,"sunset":1600191996},"timezone":7200,"id":2871845,"name":"Meppen","cod":200}' + elif 'Moscow' in query_url: + return r'{"coord":{"lon":37.62,"lat":55.75},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"base":"stations","main":{"temp":283.15,"feels_like":280.78,"temp_min":283.15,"temp_max":284.26,"pressure":1019,"humidity":71},"visibility":10000,"wind":{"speed":3,"deg":330},"clouds":{"all":0},"dt":1600196224,"sys":{"type":1,"id":9029,"country":"RU","sunrise":1600138909,"sunset":1600184863},"timezone":10800,"id":524901,"name":"Moscow","cod":200}' + + 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) + + +class PowerlineSingleTest(object): + def __init__(self, suite, name): + self.suite = suite + self.name = name + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if exc_type is not None: + self.exception('Exception while running test: {0!r}'.format( + exc_value)) + + def fail(self, message, allow_failure=False): + return self.suite.fail(self.name, message, allow_failure) + + def exception(self, message, allow_failure=False): + return self.suite.exception(self.name, message, allow_failure) diff --git a/tests/modules/lib/config_mock.py b/tests/modules/lib/config_mock.py new file mode 100644 index 0000000..900b60f --- /dev/null +++ b/tests/modules/lib/config_mock.py @@ -0,0 +1,230 @@ +# 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, get_default_theme + +from tests.modules.lib import Args, replace_attr + + +UT = get_default_theme(is_unicode=True) +AT = get_default_theme(is_unicode=False) + + +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, attrs=None): + return '<{fg} {bg} {attrs}>'.format(fg=fg and fg[0], bg=bg and bg[0], attrs=attrs) + + +class EvenSimplerRenderer(Renderer): + def hlstyle(self, fg=None, bg=None, attrs=None): + return '{{{fg}{bg}{attrs}}}'.format( + fg=fg and fg[0] or '-', + bg=bg and bg[0] or '-', + attrs=attrs if attrs 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.modules.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, replace_gcp=False, **kwargs): + if not isinstance(helpers, TestHelpers): + helpers = TestHelpers(helpers) + select_renderer(kwargs.pop('simpler_renderer', False)) + + if replace_gcp: + class PowerlineClass(PowerlineClass): + @staticmethod + def get_config_paths(): + return ['/'] + + 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/modules/lib/fsconfig.py b/tests/modules/lib/fsconfig.py new file mode 100644 index 0000000..757e874 --- /dev/null +++ b/tests/modules/lib/fsconfig.py @@ -0,0 +1,83 @@ +# 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 shutil import rmtree +from itertools import chain + +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.modules.lib.config_mock', + **self.p_kwargs + ) + if os.environ.get('POWERLINE_RUN_LINT_DURING_TESTS'): + try: + check_call(chain(['scripts/powerline-lint'], *[ + ('-p', d) for d in ( + self.p.get_config_paths() if self.p + else self.get_config_paths(self.root) + ) + ])) + 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/modules/lib/terminal.py b/tests/modules/lib/terminal.py new file mode 100644 index 0000000..540135d --- /dev/null +++ b/tests/modules/lib/terminal.py @@ -0,0 +1,307 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import threading +import os + +from time import sleep +from itertools import groupby +from signal import SIGKILL +from difflib import ndiff + +import pexpect + +from powerline.lib.unicode import u + +from tests.modules.lib.vterm import VTerm, Dimensions + + +class MutableDimensions(object): + def __init__(self, rows, cols): + super(MutableDimensions, self).__init__() + self._list = [rows, cols] + + def __getitem__(self, idx): + return self._list[idx] + + def __setitem__(self, idx, val): + self._list[idx] = val + + def __iter__(self): + return iter(self._list) + + def __len__(self): + return 2 + + def __nonzero__(self): + return True + + __bool__ = __nonzero__ + + rows = property( + fget = lambda self: self._list[0], + fset = lambda self, val: self._list.__setitem__(0, val), + ) + cols = property( + fget = lambda self: self._list[1], + fset = lambda self, val: self._list.__setitem__(1, val), + ) + + +class ExpectProcess(threading.Thread): + def __init__(self, lib, dim, cmd, args, cwd=None, env=None): + super(ExpectProcess, self).__init__() + self.vterm = VTerm(lib, dim) + self.lock = threading.Lock() + self.dim = Dimensions(*dim) + self.cmd = cmd + self.args = args + self.cwd = cwd + self.env = env + self.buffer = [] + self.child_lock = threading.Lock() + self.shutdown_event = threading.Event() + self.started_event = threading.Event() + + def run(self): + with self.child_lock: + child = pexpect.spawn(self.cmd, self.args, cwd=self.cwd, + env=self.env) + sleep(0.5) + child.setwinsize(self.dim.rows, self.dim.cols) + sleep(0.5) + self.child = child + self.started_event.set() + status = None + while status is None and not self.shutdown_event.is_set(): + try: + with self.child_lock: + s = child.read_nonblocking(size=1024, timeout=0) + status = child.status + except pexpect.TIMEOUT: + pass + except pexpect.EOF: + break + else: + with self.lock: + self.vterm.push(s) + self.buffer.append(s) + + if status is None: + child.kill(SIGKILL) + + def kill(self): + self.shutdown_event.set() + + def resize(self, dim): + with self.child_lock: + self.dim = Dimensions(*dim) + self.child.setwinsize(self.dim.rows, self.dim.cols) + self.vterm.resize(self.dim) + + def __getitem__(self, position): + with self.lock: + return self.vterm.vtscreen[position] + + def read(self): + with self.lock: + ret = b''.join(self.buffer) + del self.buffer[:] + return ret + + def send(self, data): + with self.child_lock: + self.child.send(data) + + def get_highlighted_text(self, text, attrs, default_props=(), + use_escapes=False): + ret = [] + new_attrs = attrs.copy() + for cell_properties, segment_text in text: + if use_escapes: + escapes = ('\033[38;2;{0};{1};{2};48;2;{3};{4};{5}'.format( + *(cell_properties[0] + cell_properties[1]))) + ( + ';1' if cell_properties[2] else '' + ) + ( + ';3' if cell_properties[3] else '' + ) + ( + ';4' if cell_properties[4] else '' + ) + 'm' + ret.append(escapes + segment_text + '\033[0m') + else: + segment_text = segment_text.translate({'{': '{{', '}': '}}'}) + if cell_properties not in new_attrs: + new_attrs[cell_properties] = len(new_attrs) + 1 + props_name = new_attrs[cell_properties] + if props_name in default_props: + ret.append(segment_text) + else: + ret.append('{' + str(props_name) + ':' + segment_text + '}') + return ''.join(ret), new_attrs + + def get_row(self, row, attrs, default_props=(), use_escapes=False): + with self.lock: + return self.get_highlighted_text(( + (key, ''.join((cell.text for cell in subline))) + for key, subline in groupby(( + self.vterm.vtscreen[row, col] + for col in range(self.dim.cols) + ), lambda cell: cell.cell_properties_key) + ), attrs, default_props, use_escapes) + + def get_screen(self, attrs, default_props=(), use_escapes=False): + lines = [] + for row in range(self.dim.rows): + line, attrs = self.get_row(row, attrs, default_props, use_escapes) + lines.append(line) + return '\n'.join(lines), attrs + + +def test_expected_result(p, test, last_attempt, last_attempt_cb, attempts): + debugging_tests = not not os.environ.get('_POWERLINE_DEBUGGING_TESTS') + expected_text, attrs = test['expected_result'] + result = None + while attempts: + if 'row' in test: + row = test['row'] + else: + row = p.dim.rows - 1 + while row >= 0 and not p[row, 0].text: + row -= 1 + if row < 0: + row = 0 + actual_text, all_attrs = p.get_row(row, attrs) + if actual_text == expected_text: + return True + attempts -= 1 + print('Actual result does not match expected for row {0}. Attempts left: {1}.'.format( + row, attempts)) + sleep(2) + print('Result (row {0}):'.format(row)) + print(actual_text) + print('Expected:') + print(expected_text) + print('Attributes:') + for v, k in sorted( + ((v, k) for k, v in all_attrs.items()), + key=(lambda t: '%02u'.format(t[0]) if isinstance(t[0], int) else t[0]), + ): + print('{k!r}: {v!r},'.format(v=v, k=k)) + print('Screen:') + screen, screen_attrs = p.get_screen(attrs, use_escapes=debugging_tests) + print(screen) + print(screen_attrs) + print('_' * 80) + print('Diff:') + print('=' * 80) + print(''.join(( + u(line) for line in ndiff([actual_text + '\n'], [expected_text + '\n'])) + )) + if last_attempt and last_attempt_cb: + last_attempt_cb() + return False + + +ENV_BASE = { + # Reasoning: + # 1. vt* TERMs (used to be vt100 here) make tmux-1.9 use different and + # identical colors for inactive windows. This is not like tmux-1.6: + # foreground color is different from separator color and equal to (0, + # 102, 153) for some reason (separator has correct color). tmux-1.8 is + # fine, so are older versions (though tmux-1.6 and tmux-1.7 do not have + # highlighting for previously active window) and my system tmux-1.9a. + # 2. screen, xterm and some other non-256color terminals both have the same + # issue and make libvterm emit complains like `Unhandled CSI SGR 3231`. + # 3. screen-256color, xterm-256color and other -256color terminals make + # libvterm emit complains about unhandled escapes to stderr. + # 4. `st-256color` does not have any of the above problems, but it may be + # not present on the target system because it is installed with + # x11-terms/st and not with sys-libs/ncurses. + # + # For the given reasons decision was made: to fix tmux-1.9 tests and not + # make libvterm emit any data to stderr st-256color $TERM should be used, up + # until libvterm has its own terminfo database entry (if it ever will). To + # make sure that relevant terminfo entry is present on the target system it + # should be distributed with powerline test package. To make distribution + # not require modifying anything outside of powerline test directory + # TERMINFO variable is set. + # + # This fix propagates to non-tmux vterm tests just in case. + 'TERM': 'st-256color', + # Also $TERMINFO definition in get_env + + 'POWERLINE_CONFIG_PATHS': os.path.abspath('powerline/config_files'), + 'POWERLINE_COMMAND': 'powerline-render', + 'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH', ''), + 'PYTHONPATH': os.environ.get('PYTHONPATH', ''), +} + + +def get_env(vterm_path, test_dir, *args, **kwargs): + env = ENV_BASE.copy() + env.update({ + 'TERMINFO': os.path.join(test_dir, 'terminfo'), + 'PATH': vterm_path, + 'SHELL': os.path.join(vterm_path, 'bash'), + }) + env.update(*args, **kwargs) + return env + + +def do_terminal_tests(tests, cmd, dim, args, env, suite, cwd=None, fin_cb=None, + last_attempt_cb=None, attempts=None): + debugging_tests = not not os.environ.get('_POWERLINE_DEBUGGING_TESTS') + default_attempts = 2 if debugging_tests else 3 + if attempts is None: + attempts = default_attempts + lib = os.environ.get('POWERLINE_LIBVTERM') + if not lib: + if os.path.exists('tests/bot-ci/deps/libvterm/libvterm.so'): + lib = 'tests/bot-ci/deps/libvterm/libvterm.so' + else: + lib = 'libvterm.so' + + while attempts: + try: + p = ExpectProcess( + lib=lib, + dim=dim, + cmd=cmd, + args=args, + cwd=cwd, + env=env, + ) + p.start() + p.started_event.wait() + + ret = True + + for i, test in enumerate(tests): + with suite.test(test.get('name', 'test_{0}'.format(i)), + attempts - 1) as ptest: + try: + test_prep = test['prep_cb'] + except KeyError: + pass + else: + test_prep(p) + test_result = test_expected_result( + p, test, attempts == 0, last_attempt_cb, + test.get('attempts', default_attempts) + ) + if not test_result: + ptest.fail('Result does not match expected') + ret = ret and test_result + + if ret: + return ret + finally: + if fin_cb: + fin_cb(p=p, cmd=cmd, env=env) + p.kill() + p.join(10) + assert(not p.isAlive()) + + attempts -= 1 + + return False diff --git a/tests/modules/lib/vterm.py b/tests/modules/lib/vterm.py new file mode 100644 index 0000000..1984e1b --- /dev/null +++ b/tests/modules/lib/vterm.py @@ -0,0 +1,193 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import ctypes + +from collections import namedtuple + +from powerline.lib.unicode import unicode, unichr, tointiter + + +Dimensions = namedtuple('Dimensions', ('rows', 'cols')) + + +class CTypesFunction(object): + def __init__(self, library, name, rettype, args): + self.name = name + self.prototype = ctypes.CFUNCTYPE(rettype, *[ + arg[1] for arg in args + ]) + self.args = args + self.func = self.prototype((name, library), tuple(( + (1, arg[0]) for arg in args + ))) + + def __call__(self, *args, **kwargs): + return self.func(*args, **kwargs) + + def __repr__(self): + return '{cls}(, {name!r}, {rettype!r}, {args!r})'.format( + cls=self.__class__.__name__, + **self.__dict__ + ) + + +class CTypesLibraryFuncsCollection(object): + def __init__(self, lib, **kwargs): + self.lib = lib + library_loader = ctypes.LibraryLoader(ctypes.CDLL) + library = library_loader.LoadLibrary(lib) + self.library = library + for name, args in kwargs.items(): + self.__dict__[name] = CTypesFunction(library, name, *args) + + +class VTermPos_s(ctypes.Structure): + _fields_ = ( + ('row', ctypes.c_int), + ('col', ctypes.c_int), + ) + + +class VTermColor_s(ctypes.Structure): + _fields_ = ( + ('red', ctypes.c_uint8), + ('green', ctypes.c_uint8), + ('blue', ctypes.c_uint8), + ) + + +class VTermScreenCellAttrs_s(ctypes.Structure): + _fields_ = ( + ('bold', ctypes.c_uint, 1), + ('underline', ctypes.c_uint, 2), + ('italic', ctypes.c_uint, 1), + ('blink', ctypes.c_uint, 1), + ('reverse', ctypes.c_uint, 1), + ('strike', ctypes.c_uint, 1), + ('font', ctypes.c_uint, 4), + ('dwl', ctypes.c_uint, 1), + ('dhl', ctypes.c_uint, 2), + ) + + +VTERM_MAX_CHARS_PER_CELL = 6 + + +class VTermScreenCell_s(ctypes.Structure): + _fields_ = ( + ('chars', ctypes.ARRAY(ctypes.c_uint32, VTERM_MAX_CHARS_PER_CELL)), + ('width', ctypes.c_char), + ('attrs', VTermScreenCellAttrs_s), + ('fg', VTermColor_s), + ('bg', VTermColor_s), + ) + + +VTerm_p = ctypes.c_void_p +VTermScreen_p = ctypes.c_void_p + + +def get_functions(lib): + return CTypesLibraryFuncsCollection( + lib, + vterm_new=(VTerm_p, ( + ('rows', ctypes.c_int), + ('cols', ctypes.c_int) + )), + vterm_obtain_screen=(VTermScreen_p, (('vt', VTerm_p),)), + vterm_set_size=(None, ( + ('vt', VTerm_p), + ('rows', ctypes.c_int), + ('cols', ctypes.c_int) + )), + vterm_screen_reset=(None, ( + ('screen', VTermScreen_p), + ('hard', ctypes.c_int) + )), + vterm_input_write=(ctypes.c_size_t, ( + ('vt', VTerm_p), + ('bytes', ctypes.POINTER(ctypes.c_char)), + ('size', ctypes.c_size_t), + )), + vterm_screen_get_cell=(ctypes.c_int, ( + ('screen', VTermScreen_p), + ('pos', VTermPos_s), + ('cell', ctypes.POINTER(VTermScreenCell_s)) + )), + vterm_free=(None, (('vt', VTerm_p),)), + vterm_set_utf8=(None, (('vt', VTerm_p), ('is_utf8', ctypes.c_int))), + ) + + +class VTermColor(object): + __slots__ = ('red', 'green', 'blue') + + def __init__(self, color): + self.red = color.red + self.green = color.green + self.blue = color.blue + + @property + def color_key(self): + return (self.red, self.green, self.blue) + + +class VTermScreenCell(object): + def __init__(self, vtsc): + for field in VTermScreenCellAttrs_s._fields_: + field_name = field[0] + setattr(self, field_name, getattr(vtsc.attrs, field_name)) + self.text = ''.join(( + unichr(vtsc.chars[i]) for i in range(VTERM_MAX_CHARS_PER_CELL) + )).rstrip('\x00') + self.width = next(tointiter(vtsc.width)) + self.fg = VTermColor(vtsc.fg) + self.bg = VTermColor(vtsc.bg) + self.cell_properties_key = ( + self.fg.color_key, + self.bg.color_key, + self.bold, + self.underline, + self.italic, + ) + + +class VTermScreen(object): + def __init__(self, functions, screen): + self.functions = functions + self.screen = screen + + def __getitem__(self, position): + pos = VTermPos_s(*position) + cell = VTermScreenCell_s() + ret = self.functions.vterm_screen_get_cell(self.screen, pos, cell) + if ret != 1: + raise ValueError('vterm_screen_get_cell returned {0}'.format(ret)) + return VTermScreenCell(cell) + + def reset(self, hard): + self.functions.vterm_screen_reset(self.screen, int(bool(hard))) + + +class VTerm(object): + def __init__(self, lib, dim): + self.functions = get_functions(lib) + self.vt = self.functions.vterm_new(dim.rows, dim.cols) + self.functions.vterm_set_utf8(self.vt, 1) + self.vtscreen = VTermScreen(self.functions, self.functions.vterm_obtain_screen(self.vt)) + self.vtscreen.reset(True) + + def push(self, data): + if isinstance(data, unicode): + data = data.encode('utf-8') + return self.functions.vterm_input_write(self.vt, data, len(data)) + + def resize(self, dim): + self.functions.vterm_set_size(self.vt, dim.rows, dim.cols) + + def __del__(self): + try: + self.functions.vterm_free(self.vt) + except AttributeError: + pass diff --git a/tests/modules/matchers.py b/tests/modules/matchers.py new file mode 100644 index 0000000..e905de3 --- /dev/null +++ b/tests/modules/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/modules/vim.py b/tests/modules/vim.py new file mode 100644 index 0000000..3f6882c --- /dev/null +++ b/tests/modules/vim.py @@ -0,0 +1,927 @@ +# 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('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.startswith('getbufvar('): + import re + match = re.match(r'^getbufvar\((\d+), ["\'](.+)["\']\)$', expr) + if not match: + raise NotImplementedError(expr) + bufnr = int(match.group(1)) + varname = match.group(2) + return _emul_getbufvar(bufnr, varname) + elif expr == 'tabpagenr()': + return current.tabpage.number + elif expr == 'tabpagenr("$")': + 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] + elif expr.startswith('type(function('): + import re + match = re.match(r'^type\(function\("([^"]+)"\)\) == 2$', expr) + if not match: + raise NotImplementedError(expr) + return 0 + 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(ident): + if ident.startswith('g:'): + return ident[2:] in vars + elif ident.startswith(':'): + return 0 + 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, + 'encoding': options['encoding'], + } + + +@_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 + + 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/tests/shlib/common.sh b/tests/shlib/common.sh new file mode 100644 index 0000000..7794192 --- /dev/null +++ b/tests/shlib/common.sh @@ -0,0 +1,150 @@ +: ${USER:=`id -un`} +: ${HOME:=`getent passwd $USER | cut -d: -f6`} + +if test -z "${PYTHON}" ; then + if test -n "$USE_UCS2_PYTHON" ; then + LD_LIBRARY_PATH="$HOME/opt/cpython-ucs2-$UCS2_PYTHON_VARIANT/lib${LD_LIBRARY_PATH:+:}${LD_LIBRARY_PATH}" + fi +fi + +export LD_LIBRARY_PATH +export USER +export HOME + +if test -n "$USE_UCS2_PYTHON" ; then + POWERLINE_VIRTUALENV="cpython-ucs2-$UCS2_PYTHON_VARIANT" + PYTHON="$HOME/.virtualenvs/$POWERLINE_VIRTUALENV/bin/python" + if test -n "$BASH_VERSION" ; then + set +e + . virtualenvwrapper.sh + workon "$POWERLINE_VIRTUALENV" + set -e + fi +fi + +. tests/bot-ci/scripts/common/main.sh silent + +export USER HOME + +if test -z "$FAILED" ; then + FAILED=0 + + FAIL_SUMMARY="" + + TMP_ROOT="$ROOT/tests/tmp" + export FAILURES_FILE="$ROOT/tests/status" +fi + +ANSI_CLEAR="\033[0K" + +travis_fold() { + local action="$1" + local name="$2" + name="$(echo -n "$name" | tr '\n\0' '--' | sed -r 's/[^A-Za-z0-9]+/-/g')" + name="$(echo -n "$name" | sed -r 's/-$//')" + echo -en "travis_fold:${action}:${name}\r${ANSI_CLEAR}" +} + +print_environ() { + echo "Using $PYTHON_IMPLEMENTATION version $PYTHON_VERSION." + echo "Path to Python executable: $PYTHON." + echo "Root: $ROOT." + echo "Branch: $BRANCH_NAME." + echo "sys.path:" + "$PYTHON" -c "for path in __import__('sys').path: print(' %r' % path)" +} + +enter_suite() { + set +x + local suite_name="$1" ; shift + local final="$1" + export POWERLINE_CURRENT_SUITE="${POWERLINE_CURRENT_SUITE}/$suite_name" + travis_fold start "$POWERLINE_CURRENT_SUITE" + print_environ + if test "$final" = final ; then + if test -n "$POWERLINE_SUITE_FINAL" ; then + fail __suite__/enter/final E "Final suites do not allow nesting" + fi + export POWERLINE_SUITE_FINAL=1 + # set -x + fi +} + +exit_suite() { + if test "$POWERLINE_CURRENT_SUITE" = "$POWERLINE_TMP_DIR_SUITE" ; then + rm_test_root + fi + if test $FAILED -ne 0 ; then + echo "Suite ${POWERLINE_CURRENT_SUITE} failed, summary:" + echo "${FAIL_SUMMARY}" + fi + set +x + travis_fold end "$POWERLINE_CURRENT_SUITE" + export POWERLINE_CURRENT_SUITE="${POWERLINE_CURRENT_SUITE%/*}" + if test "$1" != "--continue" ; then + exit $FAILED + else + unset POWERLINE_SUITE_FINAL + fi +} + +_fail() { + local allow_failure= + if test "$1" = "--allow-failure" ; then + shift + allow_failure=A + fi + local test_name="$1" ; shift + local fail_char="$allow_failure$1" ; shift + local message="$1" ; shift + local verb="$1" ; shift + local full_msg="$fail_char $POWERLINE_CURRENT_SUITE|$test_name :: $message" + FAIL_SUMMARY="${FAIL_SUMMARY}${NL}${full_msg}" + echo "$verb: $full_msg" + echo "$full_msg" >> "$FAILURES_FILE" + if test -z "$allow_failure" ; then + FAILED=1 + fi +} + +fail() { + _fail "$@" "Failed" +} + +skip() { + local test_name="$1" ; shift + local message="$1" ; shift + _fail --allow-failure "$test_name" S "$message" "Skipped" +} + +make_test_root() { + local suffix="${POWERLINE_CURRENT_SUITE##*/}" + + local tmpdir="$TMP_ROOT/$suffix/" + export POWERLINE_TMP_DIR_SUITE="$POWERLINE_CURRENT_SUITE" + + if test -d "$tmpdir" ; then + rm -r "$tmpdir" + fi + + mkdir -p "$tmpdir" + + export TEST_ROOT="$tmpdir" +} + +rm_test_root() { + if test -e "$FAILURES_FILE" ; then + return 0 + fi + local suffix="${POWERLINE_CURRENT_SUITE##*/}" + if test -d "$TMP_ROOT/$suffix" ; then + rm -r "$TMP_ROOT/$suffix" + rmdir "$TMP_ROOT" &>/dev/null || true + fi +} + +if ! command -v realpath ; then + realpath() { + $PYTHON -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$1" + } +fi diff --git a/tests/shlib/vim.sh b/tests/shlib/vim.sh new file mode 100644 index 0000000..49346da --- /dev/null +++ b/tests/shlib/vim.sh @@ -0,0 +1,33 @@ +. tests/bot-ci/scripts/common/main.sh + +if test -z "$POWERLINE_VIM_EXE" ; then + if test -n "$USE_UCS2_PYTHON" ; then + NEW_VIM="$ROOT/tests/bot-ci/deps/vim/master-$UCS2_PYTHON_VARIANT-ucs2-double/vim" + OLD_VIM="$ROOT/tests/bot-ci/deps/vim/v7.0.112-$UCS2_PYTHON_VARIANT-ucs2/vim" + opt_dir="$HOME/opt/cpython-ucs2-$UCS2_PYTHON_VARIANT" + main_path="$opt_dir/lib/python$UCS2_PYTHON_VARIANT" + site_path="$main_path/site-packages" + venv_main_path="$VIRTUAL_ENV/lib/python$UCS2_PYTHON_VARIANT" + venv_site_path="$venv_main_path/site-packages" + new_paths="${main_path}:${site_path}:${venv_main_path}:${venv_site_path}" + export PYTHONPATH="$new_paths${PYTHONPATH:+:}$PYTHONPATH" + else + if test "$PYTHON_IMPLEMENTATION" != "CPython" ; then + exit 0 + fi + if test -d "$ROOT/tests/bot-ci/deps" ; then + NEW_VIM="$ROOT/tests/bot-ci/deps/vim/master-$PYTHON_MM/vim" + OLD_VIM="$ROOT/tests/bot-ci/deps/vim/v7.0.112-$PYTHON_MM/vim" + else + NEW_VIM="vim" + fi + if test -e "$OLD_VIM" ; then + VIMS="NEW_VIM OLD_VIM" + else + VIMS="NEW_VIM" + fi + fi +else + NEW_VIM="$POWERLINE_VIM_EXE" + OLD_VIM="$POWERLINE_VIM_EXE" +fi diff --git a/tests/shlib/vterm.sh b/tests/shlib/vterm.sh new file mode 100644 index 0000000..bf95bb1 --- /dev/null +++ b/tests/shlib/vterm.sh @@ -0,0 +1,17 @@ +. tests/shlib/common.sh +set +x + +vterm_setup() { + make_test_root + + mkdir "$TEST_ROOT/path" + + ln -s "$(command -v "${PYTHON}")" "$TEST_ROOT/path/python" + ln -s "$(command -v bash)" "$TEST_ROOT/path" + + cp -r "$ROOT/tests/terminfo" "$TEST_ROOT" +} + +vterm_shutdown() { + rm_test_root +} diff --git a/tests/terminfo/s/screen b/tests/terminfo/s/screen new file mode 100644 index 0000000..d380072 Binary files /dev/null and b/tests/terminfo/s/screen differ diff --git a/tests/terminfo/s/st-256color b/tests/terminfo/s/st-256color new file mode 100644 index 0000000..9c2db04 Binary files /dev/null and b/tests/terminfo/s/st-256color differ diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..a08806f --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,42 @@ +#!/bin/bash +. tests/shlib/common.sh + +enter_suite root + +if test "$TRAVIS" = true ; then + export PATH="$HOME/opt/fish/bin:${PATH}" + export PATH="$PWD/tests/bot-ci/deps/rc:$PATH" + + if test "$PYTHON_IMPLEMENTATION" = "CPython" ; then + export PATH="$HOME/opt/zsh-${PYTHON_MM}${USE_UCS2_PYTHON:+-ucs2}/bin:${PATH}" + fi + + if test -n "$USE_UCS2_PYTHON" ; then + export LD_LIBRARY_PATH="$HOME/opt/cpython-ucs2-$UCS2_PYTHON_VARIANT/lib${LD_LIBRARY_PATH:+:}$LD_LIBRARY_PATH" + set +e + . virtualenvwrapper.sh + workon cpython-ucs2-$UCS2_PYTHON_VARIANT + set -e + else + LIBRARY_PATH="$(ldd "$(command -v python)" | grep libpython | sed 's/^.* => //;s/ .*$//')" + LIBRARY_DIR="$(dirname "${LIBRARY_PATH}")" + export LD_LIBRARY_PATH="$LIBRARY_DIR${LD_LIBRARY_PATH:+:}$LD_LIBRARY_PATH" + fi +fi + +export PYTHON="${PYTHON:=python}" +export PYTHONPATH="${PYTHONPATH}${PYTHONPATH:+:}`realpath .`" +for script in "$ROOT"/tests/test_*/test.sh ; do + test_name="${script##*/run_}" + if ! sh $script ; then + fail "${test_name%_tests.sh}" F "Failed $script" + fi +done + +if test -e "$FAILURES_FILE" ; then + echo "Fails and skips summary:" + cat "$FAILURES_FILE" + rm "$FAILURES_FILE" +fi + +exit_suite diff --git a/tests/test_awesome/path/awesome-client b/tests/test_awesome/path/awesome-client new file mode 100755 index 0000000..2088ff0 --- /dev/null +++ b/tests/test_awesome/path/awesome-client @@ -0,0 +1,3 @@ +#!/bin/sh +echo "$@" >> "$TEST_ROOT/results/args" +cat >> "$TEST_ROOT/results/requests" diff --git a/tests/test_awesome/powerline/config.json b/tests/test_awesome/powerline/config.json new file mode 100644 index 0000000..b8dfd01 --- /dev/null +++ b/tests/test_awesome/powerline/config.json @@ -0,0 +1,7 @@ +{ + "ext": { + "wm": { + "update_interval": 0.5 + } + } +} diff --git a/tests/test_awesome/powerline/themes/wm/default.json b/tests/test_awesome/powerline/themes/wm/default.json new file mode 100644 index 0000000..d9eaf5f --- /dev/null +++ b/tests/test_awesome/powerline/themes/wm/default.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "default-left" + } + ], + "right": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "default-right" + } + ] + } +} diff --git a/tests/test_awesome/powerline/themes/wm/dvi.json b/tests/test_awesome/powerline/themes/wm/dvi.json new file mode 100644 index 0000000..4cf3731 --- /dev/null +++ b/tests/test_awesome/powerline/themes/wm/dvi.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "dvi-left" + } + ], + "right": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "dvi-right" + } + ] + } +} diff --git a/tests/test_awesome/test.sh b/tests/test_awesome/test.sh new file mode 100755 index 0000000..7a5614e --- /dev/null +++ b/tests/test_awesome/test.sh @@ -0,0 +1,188 @@ +#!/bin/sh +. tests/shlib/common.sh + +enter_suite awesome + +make_test_root + +TEST_PATH="$TEST_ROOT/path" +TEST_STATIC_ROOT="$ROOT/tests/test_awesome" + +cp -r "$TEST_STATIC_ROOT/path" "$TEST_ROOT" +cp -r "$TEST_STATIC_ROOT/powerline" "$TEST_ROOT" + +export PYTHONPATH="$ROOT${PYTHONPATH:+:}$PYTHONPATH" + +ln -s "$(command -v "${PYTHON}")" "$TEST_PATH"/python +ln -s "$(command -v cat)" "$TEST_PATH" +ln -s "$(command -v sh)" "$TEST_PATH" +ln -s "$(command -v env)" "$TEST_PATH" +if command -v socat ; then + ln -s "$(command -v socat)" "$TEST_PATH" +fi +for pexe in powerline powerline.sh powerline.py ; do + if test -e scripts/$pexe ; then + ln -s "$PWD/scripts/$pexe" $TEST_ROOT/path + elif test -e client/$pexe ; then + ln -s "$PWD/client/$pexe" $TEST_ROOT/path + elif command -v $pexe ; then + ln -s "$(command -v $pexe)" $TEST_ROOT/path + else + continue + fi + if test "$pexe" != 'powerline.sh' || test -e "$TEST_PATH/socat" ; then + POWERLINE_COMMAND="$pexe" + break + fi +done + +DEPRECATED_SCRIPT="$ROOT/powerline/bindings/awesome/powerline-awesome.py" +POWERLINE_DAEMON="scripts/powerline-daemon" + +run() { + env -i \ + LANG=C \ + PATH="$TEST_PATH" \ + XDG_CONFIG_HOME="$TEST_ROOT" \ + XDG_CONFIG_DIRS="$TEST_ROOT/dummy" \ + PYTHONPATH="$PYTHONPATH" \ + TEST_ROOT="$TEST_ROOT" \ + LD_LIBRARY_PATH="$LD_LIBRARY_PATH" \ + "$@" || true +} + +display_log() { + local log_file="$1" + echo "$log_file:" + echo '============================================================' + cat -v "$log_file" + echo + echo '____________________________________________________________' +} + +check_log() { + local args_file="$TEST_ROOT/results/args" + local log_file="$TEST_ROOT/results/requests" + local line="$(head -n1 "$log_file")" + local linenum="$(cat "$log_file" | wc -l)" + echo "Number of runs: $linenum (expected approx 5 / 0.5 = 10 runs)" + if test $linenum -lt 5 ; then + fail "log:lt" F "Script was run not enough times: $linenum < 5" + return 1 + elif test $linenum -gt 15 ; then + fail "log:gt" E "Script was run too many times: $linenum > 15" + return 1 + fi + local expline="powerline_widget:set_markup(' default-right ')" + if test "$expline" != "$line" ; then + echo "Line: '$line'" + echo "Expected: '$expline'" + fail "log:line" F "Unexpected line" + return 1 + fi + local ret=0 + while test $linenum -gt 0 ; do + echo "$line" >> "$TEST_ROOT/ok" + linenum=$(( linenum - 1 )) + done + if ! diff "$TEST_ROOT/ok" "$log_file" ; then + fail "log:diff" F "Unexpected output" + ret=1 + fi + rm "$TEST_ROOT/ok" + return $ret +} + +killscript() { + kill -KILL $1 || true +} + +if ! test -e "$DEPRECATED_SCRIPT" ; then + # TODO: uncomment when skip is available + # skip "deprecated" "Missing deprecated bar bindings script" + : +else + enter_suite "deprecated" final + for args in "" "0.5"; do + rm -rf "$TEST_ROOT/results" + mkdir "$TEST_ROOT/results" + DEPRECATED_LOG="$TEST_ROOT/deprecated.log" + run env \ + DEPRECATED_SCRIPT="$DEPRECATED_SCRIPT" \ + args="$args" \ + DEPRECATED_LOG="$DEPRECATED_LOG" \ + TEST_ROOT="$TEST_ROOT" \ + sh -c ' + echo $$ > "$TEST_ROOT/$args-pid" + exec "$DEPRECATED_SCRIPT" $args > "$DEPRECATED_LOG" 2>&1 + ' & + sleep 5 + killscript "$(cat "$TEST_ROOT/$args-pid")" + rm "$TEST_ROOT/$args-pid" + if test -n "$(cat "$DEPRECATED_LOG")" ; then + display_log "$DEPRECATED_LOG" + fail "output" E "Nonempty $DEPRECATED_SCRIPT output" + fi + rm "$DEPRECATED_LOG" + if ! check_log ; then + display_log "$TEST_ROOT/results/args" + fail "log" F "Checking log failed" + fi + done + exit_suite --continue +fi + +enter_suite "awesome" final +ADDRESS="powerline-ipc-test-$$" +echo "Powerline address: $ADDRESS" +rm -rf "$TEST_ROOT/results" +mkdir "$TEST_ROOT/results" +run env \ + POWERLINE_DAEMON="$POWERLINE_DAEMON" \ + TEST_ROOT="$TEST_ROOT" \ + ADDRESS="$ADDRESS" \ + sh -c ' + echo $$ > "$TEST_ROOT/dpid" + exec python "$POWERLINE_DAEMON" --socket $ADDRESS --foreground > "$TEST_ROOT/daemon.log" 2>&1 + ' & +DPID=$! +sleep 2 +run "$POWERLINE_COMMAND" --socket $ADDRESS wm.awesome > "$TEST_ROOT/output.log.1" 2>&1 +run "$POWERLINE_COMMAND" --socket $ADDRESS wm.awesome > "$TEST_ROOT/output.log.2" 2>&1 +run "$POWERLINE_COMMAND" --socket $ADDRESS wm.awesome > "$TEST_ROOT/output.log.3" 2>&1 +run "$POWERLINE_COMMAND" --socket $ADDRESS wm.awesome > "$TEST_ROOT/output.log.4" 2>&1 +run "$POWERLINE_COMMAND" --socket $ADDRESS wm.awesome > "$TEST_ROOT/output.log.5" 2>&1 +for log_file in "$TEST_ROOT"/output.log.* ; do + if test -n "$(cat "$log_file")" ; then + display_log "$log_file" + fail "output" E "Nonempty $POWERLINE_COMMAND output at run ${log_file#*.}" + fi + rm "$log_file" +done +sleep 5 +run python "$POWERLINE_DAEMON" --socket $ADDRESS --quiet --kill > "$TEST_ROOT/kill.log" 2>&1 +if test -n "$(cat "$TEST_ROOT/kill.log")" ; then + display_log "$TEST_ROOT/kill.log" + fail "daemonlog" E "Nonempty kill log" +fi +rm "$TEST_ROOT/kill.log" +wait $DPID +if test -n "$(cat "$TEST_ROOT/daemon.log")" ; then + display_log "$TEST_ROOT/daemon.log" + fail "daemonlog" E "Nonempty daemon log" +fi +rm "$TEST_ROOT/daemon.log" +if ! check_log ; then + display_log "$TEST_ROOT/results/args" + fail "log" F "Checking log failed" +fi +exit_suite --continue + +if ! powerline-lint \ + -p "$ROOT/powerline/config_files" \ + -p "$TEST_STATIC_ROOT/powerline" +then + fail "lint" F "Checking test config failed" +fi + +exit_suite diff --git a/tests/test_bar/path/lemonbar b/tests/test_bar/path/lemonbar new file mode 100755 index 0000000..13c6030 --- /dev/null +++ b/tests/test_bar/path/lemonbar @@ -0,0 +1,11 @@ +#!/bin/sh + +RES_DIR="$TEST_ROOT/results" +mkdir -p "$RES_DIR" +RES_FILE="$RES_DIR/$$" +while test -e "$RES_FILE.log" ; do + RES_FILE="$RES_FILE.${RANDOM:-`date +%N | sed s/^0*//`}" +done + +echo $(basename $0) "$@" > "$RES_FILE.args" +cat > "$RES_FILE.log" diff --git a/tests/test_bar/path/xrandr b/tests/test_bar/path/xrandr new file mode 100755 index 0000000..d02e300 --- /dev/null +++ b/tests/test_bar/path/xrandr @@ -0,0 +1,31 @@ +#!/bin/sh + +cat << EOF +Screen 0: minimum 8 x 8, current 1920 x 1200, maximum 16384 x 16384 +DVI-I-0 disconnected (normal left inverted right x axis y axis) +VGA-0 connected 1920x1200+1+0 (normal left inverted right x axis y axis) 520mm x 330mm + 1920x1200 59.95*+ + 1920x1080 60.00 + 1680x1050 59.95 + 1600x1200 60.00 + 1440x900 59.89 + 1280x1024 75.02 60.02 + 1280x800 59.81 + 1152x864 75.00 + 1024x768 75.03 60.00 + 800x600 75.00 60.32 + 640x480 75.00 59.94 +DVI-I-1 connected 1920x1200+0+0 (normal left inverted right x axis y axis) 520mm x 330mm + 1920x1200 59.95*+ + 1920x1080 60.00 + 1680x1050 59.95 + 1600x1200 60.00 + 1440x900 59.89 + 1280x1024 75.02 60.02 + 1280x800 59.81 + 1152x864 75.00 + 1024x768 75.03 60.00 + 800x600 75.00 60.32 + 640x480 75.00 59.94 +HDMI-0 disconnected (normal left inverted right x axis y axis) +EOF diff --git a/tests/test_bar/powerline/config.json b/tests/test_bar/powerline/config.json new file mode 100644 index 0000000..0b35d56 --- /dev/null +++ b/tests/test_bar/powerline/config.json @@ -0,0 +1,9 @@ +{ + "ext": { + "wm": { + "local_themes": { + "DVI-I-1": "dvi" + } + } + } +} diff --git a/tests/test_bar/powerline/themes/wm/default.json b/tests/test_bar/powerline/themes/wm/default.json new file mode 100644 index 0000000..d9eaf5f --- /dev/null +++ b/tests/test_bar/powerline/themes/wm/default.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "default-left" + } + ], + "right": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "default-right" + } + ] + } +} diff --git a/tests/test_bar/powerline/themes/wm/dvi.json b/tests/test_bar/powerline/themes/wm/dvi.json new file mode 100644 index 0000000..4cf3731 --- /dev/null +++ b/tests/test_bar/powerline/themes/wm/dvi.json @@ -0,0 +1,18 @@ +{ + "segments": { + "left": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "dvi-left" + } + ], + "right": [ + { + "type": "string", + "highlight_groups": ["time"], + "contents": "dvi-right" + } + ] + } +} diff --git a/tests/test_bar/test.sh b/tests/test_bar/test.sh new file mode 100755 index 0000000..fa36799 --- /dev/null +++ b/tests/test_bar/test.sh @@ -0,0 +1,195 @@ +#!/bin/sh +. tests/shlib/common.sh + +enter_suite bar + +make_test_root +TEST_PATH="$TEST_ROOT/path" +TEST_STATIC_ROOT="$ROOT/tests/test_bar" + +cp -r "$TEST_STATIC_ROOT/path" "$TEST_ROOT" +cp -r "$TEST_STATIC_ROOT/powerline" "$TEST_ROOT" + +export PYTHONPATH="$ROOT${PYTHONPATH:+:}$PYTHONPATH" + +ln -s "$(command -v "${PYTHON}")" "$TEST_PATH"/python +ln -s "$(command -v sed)" "$TEST_PATH" +ln -s "$(command -v cat)" "$TEST_PATH" +ln -s "$(command -v mkdir)" "$TEST_PATH" +ln -s "$(command -v basename)" "$TEST_PATH" +ln -s "$TEST_PATH/lemonbar" "$TEST_PATH/bar-aint-recursive" + +DEPRECATED_SCRIPT="$ROOT/powerline/bindings/bar/powerline-bar.py" + +run() { + env -i \ + LANG=C \ + PATH="$TEST_PATH" \ + XDG_CONFIG_HOME="$TEST_ROOT" \ + XDG_CONFIG_DIRS="$TEST_ROOT/dummy" \ + PYTHONPATH="$PYTHONPATH" \ + TEST_ROOT="$TEST_ROOT" \ + LD_LIBRARY_PATH="$LD_LIBRARY_PATH" \ + "$@" || true +} + +display_log() { + local log_file="$1" + echo "$log_file:" + echo '============================================================' + cat -v "$log_file" + echo + echo '____________________________________________________________' +} + +check_log() { + local log_file="$1" + local text="$2" + local warns="$3" + if test "$warns" = "warns" ; then + local warning="$(head -n1 "$log_file" | sed 's/.*://')" + local expwarning="The 'bar' bindings are deprecated, please switch to 'lemonbar'" + if test "$warning" != "$expwarning" ; then + echo "Got: $warning" + echo "Exp: $expwarning" + fail "warn" F "Expected warning" + fi + sed -r -i -e '1d' "$log_file" + fi + local line="$(head -n1 "$log_file")" + local linenum="$(cat "$log_file" | wc -l)" + if test $linenum -lt 5 ; then + fail "log:lt" F "Script was run not enough times" + return 1 + elif test $linenum -gt 15 ; then + fail "log:gt" E "Script was run too many times" + return 1 + fi + local expline="%{l}%{F#ffd0d0d0}%{B#ff303030} $text-left %{F-B--u}%{F#ff303030} %{F-B--u}%{r}%{F#ff303030} %{F-B--u}%{F#ffd0d0d0}%{B#ff303030} $text-right %{F-B--u}" + if test "$expline" != "$line" ; then + echo "Line: '$line'" + echo "Expected: '$expline'" + fail "log:line" F "Unexpected line" + return 1 + fi + local ret=0 + while test $linenum -gt 0 ; do + echo "$line" >> "$TEST_ROOT/ok" + linenum=$(( linenum - 1 )) + done + if ! diff "$TEST_ROOT/ok" "$log_file" ; then + fail "log:diff" F "Unexpected output" + ret=1 + fi + rm "$TEST_ROOT/ok" + return $ret +} + +killscript() { + kill -KILL $1 || true +} + +if ! test -e "$DEPRECATED_SCRIPT" ; then + # TODO: uncomment when skip is available + # skip "deprecated" "Missing deprecated bar bindings script" + : +else + enter_suite "deprecated" final + run python "$DEPRECATED_SCRIPT" $args > "$TEST_ROOT/deprecated.log" 2>&1 & + SPID=$! + sleep 5 + killscript $SPID + if ! check_log "$TEST_ROOT/deprecated.log" "default" warns ; then + display_log "$TEST_ROOT/deprecated.log" + fail "log" F "Checking log failed" + fi + rm "$TEST_ROOT/deprecated.log" + exit_suite --continue +fi + +LEMONBAR_SCRIPT="$ROOT/powerline/bindings/lemonbar/powerline-lemonbar.py" + +if ! test -e "$LEMONBAR_SCRIPT" ; then + # TODO: uncomment when skip is available + # skip "lemonbar" "Missing lemonbar bindings script" + : +else + enter_suite "lemonbar" + for args in "" "-i0.5" "--interval=0.5" "-- test args" "--bar-command bar-aint-recursive" "--height=10"; do + rm -rf "$TEST_ROOT/results" + run python "$LEMONBAR_SCRIPT" $args > "$TEST_ROOT/lemonbar.log" 2>&1 & + SPID=$! + sleep 5 + killscript $SPID + sleep 0.5 + enter_suite "args($args)" final + fnum=0 + for file in "$TEST_ROOT/results"/*.log ; do + if ! test -e "$file" ; then + fail "log" E "Log file is missing" + break + fi + fnum=$(( fnum + 1 )) + args_file="${file%.log}.args" + if ! test -e "$args_file" ; then + fail "args" E "$args_file is missing" + else + cat "$args_file" >> "$TEST_ROOT/args.log" + fi + text="dvi" + if cat "$args_file" | grep -q +1 ; then + text="default" + fi + if ! check_log "$file" "$text" ; then + display_log "$file" + fail "log" F "Checking log failed" + fi + rm "$file" + done + if test "$fnum" -ne 2 ; then + fail "fnum" F "Expected two output files" + fi + if test "${args#--height}" != "$args" ; then + height="${args#--height}" + height="${height# }" + height="${height#=}" + height="${height%% *}" + fi + command="lemonbar" + if test "${args#--bar-command}" != "$args" ; then + command="${args#--bar-command}" + command="${command# }" + command="${command#=}" + command="${command%% *}" + fi + received_args="$(cat "$TEST_ROOT/args.log" | sort)" + rm "$TEST_ROOT/args.log" + script_args="${args#*-- }" + script_args="${script_args# }" + if test "${script_args}" = "$args" ; then + script_args= + fi + expected_args="$command -g 1920x$height+0+0${script_args:+ }$script_args${NL}$command -g 1920x$height+1+0${script_args:+ }$script_args" + if test "$expected_args" != "$received_args" ; then + echo "args:${NL}<$received_args>" + echo "expected:${NL}<$expected_args>" + fail "args" F "Expected different args" + fi + if ! test -z "$(cat "$TEST_ROOT/lemonbar.log")" ; then + display_log "$TEST_ROOT/lemonbar.log" + fail "stderr" E "Unexpected script output" + fi + rm "$TEST_ROOT/lemonbar.log" + exit_suite --continue + done + exit_suite --continue +fi + +if ! powerline-lint \ + -p "$ROOT/powerline/config_files" \ + -p "$TEST_STATIC_ROOT/powerline" +then + fail "lint" F "Checking test config failed" +fi + +exit_suite diff --git a/tests/test_daemon/test.sh b/tests/test_daemon/test.sh new file mode 100755 index 0000000..a5c12c1 --- /dev/null +++ b/tests/test_daemon/test.sh @@ -0,0 +1,41 @@ +#!/bin/sh +. tests/shlib/common.sh + +enter_suite daemon final + +export ADDRESS="powerline-ipc-test-$$" +echo "Powerline address: $ADDRESS" +if "$PYTHON" "$ROOT/scripts/powerline-daemon" -s"$ADDRESS" ; then + sleep 1 + if ! ( \ + "$PYTHON" "$ROOT/client/powerline.py" \ + --socket "$ADDRESS" -p/dev/null shell left \ + | grep "file not found" + ) ; then + fail "devnull" F "-p/dev/null argument ignored or not treated properly" + fi + if ( \ + "$PYTHON" "$ROOT/client/powerline.py" --socket "$ADDRESS" \ + -p"$ROOT/powerline/config_files" shell left \ + | grep "file not found" + ) ; then + fail "nodevnull" F "-p/dev/null argument remembered while it should not" + fi + if ! ( \ + cd "$ROOT/tests/test_daemon" \ + && "$PYTHON" "$ROOT/client/powerline.py" --socket "$ADDRESS" \ + -p"$ROOT/powerline/config_files" shell left \ + | grep "test_daemon" + ) ; then + fail "segment" F "Output lacks string “tests”" + fi +else + fail "exitcode" E "Daemon exited with status $?" +fi +if "$PYTHON" "$ROOT/scripts/powerline-daemon" -s"$ADDRESS" -k ; then + : +else + fail "-k" F "powerline-daemon -k failed with exit code $?" +fi + +exit_suite diff --git a/tests/test_in_vterm/shell/inits/dash b/tests/test_in_vterm/shell/inits/dash new file mode 100644 index 0000000..7b146ff --- /dev/null +++ b/tests/test_in_vterm/shell/inits/dash @@ -0,0 +1,16 @@ +# vim: ft=sh + +set_theme_option() { + export POWERLINE_THEME_OVERRIDES="${POWERLINE_THEME_OVERRIDES};$1=$2" +} +set_theme() { + export POWERLINE_CONFIG_OVERRIDES="ext.shell.theme=$1" +} +set_virtual_env() { + export VIRTUAL_ENV="$HOME/.virtenvs/$1" +} +set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false +set_theme default_leftonly +. "$ROOT/powerline/bindings/shell/powerline.sh" +export VIRTUAL_ENV= +cd "$TEST_ROOT/3rd" diff --git a/tests/test_in_vterm/test.sh b/tests/test_in_vterm/test.sh new file mode 100755 index 0000000..ad8d72a --- /dev/null +++ b/tests/test_in_vterm/test.sh @@ -0,0 +1,14 @@ +#!/bin/sh +. tests/shlib/common.sh + +enter_suite vterm + +# (Disabled) +#for t in "$ROOT"/tests/test_in_vterm/test_*.sh ; do +# test_name="${t##*/test_}" +# if ! "$t" ; then +# fail "${test_name%.sh}" F "Failed running $t" +# fi +#done + +exit_suite diff --git a/tests/test_in_vterm/test_shells.py b/tests/test_in_vterm/test_shells.py new file mode 100755 index 0000000..faf7976 --- /dev/null +++ b/tests/test_in_vterm/test_shells.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys + +from time import sleep +from subprocess import check_call +from glob import glob1 +from traceback import print_exc + +from argparse import ArgumentParser + +from powerline.lib.dict import updated + +from tests.modules.lib.terminal import (ExpectProcess, MutableDimensions, + do_terminal_tests, get_env) +from tests.modules import PowerlineTestSuite + + +TEST_ROOT = os.path.abspath(os.environ['TEST_ROOT']) + + +def get_parser(): + parser = ArgumentParser() + parser.add_argument('--type', action='store') + parser.add_argument('--client', action='store') + parser.add_argument('--binding', action='store') + parser.add_argument('args', action='append') + return parser + + +BINDING_OPTIONS = { + 'dash': { + 'cmd': 'dash', + 'args': ['-i'], + 'init': [ + '. "$ROOT/tests/test_in_vterm/shell/inits/dash"', + ], + }, +} + + +def main(argv): + script_args = get_parser().parse_args(argv) + + vterm_path = os.path.join(TEST_ROOT, 'path') + + env = get_env(vterm_path, TEST_ROOT) + env['ROOT'] = os.path.abspath('.') + env['TEST_ROOT'] = TEST_ROOT + env['TEST_TYPE'] = script_args.type + env['TEST_CLIENT'] = script_args.client + env['LANG'] = 'en_US.UTF_8' + env['_POWERLINE_RUNNING_SHELL_TESTS'] = ( + 'ee5bcdc6-b749-11e7-9456-50465d597777') + + dim = MutableDimensions(rows=50, cols=200) + + binding_opts = BINDING_OPTIONS[script_args.binding] + + cmd = os.path.join(vterm_path, binding_opts['cmd']) + args = binding_opts['args'] + + def gen_init(binding): + def init(p): + for line in binding_opts['init']: + p.send(line + '\n') + sleep(1) + + return init + + def gen_feed(line): + def feed(p): + p.send(line + '\n') + sleep(0.1) + + return feed + + base_attrs = { + ((255, 204,0), (204, 51, 0), 0, 0, 0): 'H', + ((204, 51, 0), (0, 102, 153), 0, 0, 0): 'sHU', + ((255, 255, 255), (0, 102, 153), 1, 0, 0): 'U', + ((0, 102, 153), (44, 44, 44), 0, 0, 0): 'sUB', + ((199, 199, 199), (44, 44, 44), 0, 0, 0): 'B', + ((44, 44, 44), (88, 88, 88), 0, 0, 0): 'sBD', + ((199, 199, 199), (88, 88, 88), 0, 0, 0): 'D', + ((144, 144, 144), (88, 88, 88), 0, 0, 0): 'sD', + ((221, 221, 221), (88, 88, 88), 1, 0, 0): 'C', + ((88, 88, 88), (0, 0, 0), 0, 0, 0): 'sDN', + ((240, 240, 240), (0, 0, 0), 0, 0, 0): 'N', + ((0, 102, 153), (51, 153, 204), 0, 0, 0): 'sUE', + ((255, 255, 255), (51, 153, 204), 0, 0, 0): 'E', + ((51, 153, 204), (44, 44, 44), 0, 0, 0): 'sEB', + } + + tests = ( + { + 'expected_result': ( + '{H:  hostname }{sHU: }' + '{U:user }{sUB: }' + '{B: BRANCH }{sBD: }' + '{D:… }{sD: }{D:tmp }{sD: }{D:vshells }{sD: }{C:3rd }{sDN: }' + '{N:}', + base_attrs, + ), + 'prep_cb': gen_init(script_args.binding), + }, + { + 'expected_result': ( + '{H:  hostname }{sHU: }' + '{U:user }{sUB: }' + '{B: BRANCH }{sBD: }' + '{D:… }{sD: }{D:vshells }{sD: }{D:3rd }{sD: }{C:.git }{sDN: }' + '{N:}', + base_attrs + ), + 'prep_cb': gen_feed('cd .git'), + }, + { + 'expected_result': ( + '{H:  hostname }{sHU: }' + '{U:user }{sUB: }' + '{B: BRANCH }{sBD: }' + '{D:… }{sD: }{D:tmp }{sD: }{D:vshells }{sD: }{C:3rd }{sDN: }' + '{N:}', + base_attrs, + ), + 'prep_cb': gen_feed('cd ..'), + }, + { + 'expected_result': ( + '{H:  hostname }{sHU: }' + '{U:user }{sUE: }' + '{E:(e) some-venv }{sEB: }' + '{B: BRANCH }{sBD: }' + '{D:… }{sD: }{D:tmp }{sD: }{D:vshells }{sD: }{C:3rd }{sDN: }' + '{N:}', + base_attrs, + ), + 'prep_cb': gen_feed('set_virtual_env some-venv'), + }, + ) + + with PowerlineTestSuite('shell') as suite: + return do_terminal_tests( + tests=tests, + cmd=cmd, + dim=dim, + args=args, + env=env, + cwd=TEST_ROOT, + suite=suite, + ) + + +if __name__ == '__main__': + if main(sys.argv[1:]): + raise SystemExit(0) + else: + raise SystemExit(1) diff --git a/tests/test_in_vterm/test_shells.sh b/tests/test_in_vterm/test_shells.sh new file mode 100755 index 0000000..f27442a --- /dev/null +++ b/tests/test_in_vterm/test_shells.sh @@ -0,0 +1,119 @@ +#!/bin/bash +. tests/shlib/common.sh +. tests/shlib/vterm.sh + +enter_suite vshells + +vterm_setup + +HAS_SOCAT= +HAS_C_CLIENT= + +git init "$TEST_ROOT/3rd" +git --git-dir="$TEST_ROOT/3rd/.git" checkout -b BRANCH +export DIR1="" +export DIR2="" +mkdir "$TEST_ROOT/3rd/$DIR1" +mkdir "$TEST_ROOT/3rd/$DIR2" +mkdir "$TEST_ROOT"/3rd/'\[\]' +mkdir "$TEST_ROOT"/3rd/'%%' +mkdir "$TEST_ROOT"/3rd/'#[bold]' +mkdir "$TEST_ROOT"/3rd/'(echo)' +mkdir "$TEST_ROOT"/3rd/'$(echo)' +mkdir "$TEST_ROOT"/3rd/'`echo`' +mkdir "$TEST_ROOT"/3rd/'«Unicode!»' +mkdir "$TEST_ROOT/fish_home" +mkdir "$TEST_ROOT/fish_home/fish" +mkdir "$TEST_ROOT/fish_home/fish/generated_completions" +cp -r "$ROOT/tests/test_shells/ipython_home" "$TEST_ROOT" + +ln -s "$(command -v env)" "$TEST_ROOT/path" +ln -s "$(command -v git)" "$TEST_ROOT/path" +ln -s "$(command -v sleep)" "$TEST_ROOT/path" +ln -s "$(command -v cat)" "$TEST_ROOT/path" +ln -s "$(command -v false)" "$TEST_ROOT/path" +ln -s "$(command -v true)" "$TEST_ROOT/path" +ln -s "$(command -v kill)" "$TEST_ROOT/path" +ln -s "$(command -v echo)" "$TEST_ROOT/path" +ln -s "$(command -v which)" "$TEST_ROOT/path" +ln -s "$(command -v dirname)" "$TEST_ROOT/path" +ln -s "$(command -v wc)" "$TEST_ROOT/path" +ln -s "$(command -v stty)" "$TEST_ROOT/path" +ln -s "$(command -v cut)" "$TEST_ROOT/path" +ln -s "$(command -v bc)" "$TEST_ROOT/path" +ln -s "$(command -v expr)" "$TEST_ROOT/path" +ln -s "$(command -v mktemp)" "$TEST_ROOT/path" +ln -s "$(command -v grep)" "$TEST_ROOT/path" +ln -s "$(command -v sed)" "$TEST_ROOT/path" +ln -s "$(command -v rm)" "$TEST_ROOT/path" +ln -s "$(command -v tr)" "$TEST_ROOT/path" +ln -s "$(command -v uname)" "$TEST_ROOT/path" +ln -s "$(command -v test)" "$TEST_ROOT/path" +ln -s "$(command -v pwd)" "$TEST_ROOT/path" +ln -s "$(command -v hostname)" "$TEST_ROOT/path" +ln -s "$ROOT/tests/test_shells/bgscript.sh" "$TEST_ROOT/path" +ln -s "$ROOT/tests/test_shells/waitpid.sh" "$TEST_ROOT/path" + +ln -s "$ROOT/scripts/powerline-config" "$TEST_ROOT/path" +ln -s "$ROOT/scripts/powerline-render" "$TEST_ROOT/path" +ln -s "$ROOT/client/powerline.py" "$TEST_ROOT/path" + +if test -e "$ROOT/scripts/powerline" ; then + ln -s "$ROOT/scripts/powerline" "$TEST_ROOT/path" +elif test -e client/powerline ; then + ln -s "$ROOT/client/powerline" "$TEST_ROOT/path" +elif command -v powerline ; then + ln -s "$(command -v powerline)" "$TEST_ROOT/path" +else + echo "Executable powerline was not found" + exit 1 +fi + +if test "$( + file --mime-type --brief --dereference "$TEST_ROOT/path/powerline" \ + | cut -d/ -f1)" = "application" ; then + HAS_C_CLIENT=1 +fi + +if command -v socat ; then + HAS_SOCAT=1 + ln -s "$(command -v socat)" "$TEST_ROOT/path" + ln -s "$ROOT/client/powerline.sh" "$TEST_ROOT/path" +fi + +# Test type: daemon, renderer, … +# Test client: python, shell, c, none +# Test binding: *sh, ipython, pdb, … +test_shell() { + local test_type="$1" ; shift + local test_client="$1" ; shift + local test_binding="$1" ; shift + + if test "$test_client" = shell && test -z "$HAS_SOCAT" ; then + echo "Skipping test, socat not available" + return + fi + if test "$test_client" = c && test -z "$HAS_C_CLIENT" ; then + echo "Skipping test, C client not available" + return + fi + if command -v "$test_binding" ; then + ln -s "$(command -v "$test_binding")" "$TEST_ROOT/path" + fi + + if ! "${PYTHON}" "$ROOT/tests/test_in_vterm/test_shells.py" \ + --type=$test_type \ + --client=$test_client \ + --binding=$test_binding \ + -- "$@" + then + local test_name="$test_type-$test_client-$test_binding" + fail "$test_name" F "Failed vterm shell test" + fi +} + +test_shell renderer python dash -i || true + +vterm_shutdown + +exit_suite diff --git a/tests/test_in_vterm/test_tmux.py b/tests/test_in_vterm/test_tmux.py new file mode 100755 index 0000000..c1e126b --- /dev/null +++ b/tests/test_in_vterm/test_tmux.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys +import json + +from time import sleep +from subprocess import check_call +from glob import glob1 +from traceback import print_exc + +from powerline.lib.dict import updated +from powerline.bindings.tmux import get_tmux_version +from powerline import get_fallback_logger + +from tests.modules.lib.terminal import (ExpectProcess, MutableDimensions, + do_terminal_tests, get_env) +from tests.modules import PowerlineTestSuite + + +TEST_ROOT = os.path.abspath(os.environ['TEST_ROOT']) + + +def tmux_logs_iter(test_dir): + for tail in glob1(test_dir, '*.log'): + yield os.path.join(test_dir, tail) + + +def print_tmux_logs(): + for f in tmux_logs_iter(TEST_ROOT): + print('_' * 80) + print(os.path.basename(f) + ':') + print('=' * 80) + with open(f, 'r') as fp: + for line in fp: + sys.stdout.write(line) + os.unlink(f) + + +def get_expected_result(tmux_version, + expected_result_old, + expected_result_1_7=None, + expected_result_1_8=None, + expected_result_2_0=None): + if tmux_version >= (2, 0) and expected_result_2_0: + return expected_result_2_0 + elif tmux_version >= (1, 8) and expected_result_1_8: + return expected_result_1_8 + elif tmux_version >= (1, 7) and expected_result_1_7: + return expected_result_1_7 + else: + return expected_result_old + + +def tmux_fin_cb(p, cmd, env): + try: + check_call([ + cmd, '-S', env['POWERLINE_TMUX_SOCKET_PATH'], 'kill-server' + ], env=env, cwd=TEST_ROOT) + except Exception: + print_exc() + for f in tmux_logs_iter(TEST_ROOT): + os.unlink(f) + + +def main(attempts=3): + vterm_path = os.path.join(TEST_ROOT, 'path') + + tmux_exe = os.path.join(vterm_path, 'tmux') + + socket_path = os.path.abspath('tmux-socket-{0}'.format(attempts)) + if os.path.exists(socket_path): + os.unlink(socket_path) + + env = get_env(vterm_path, TEST_ROOT, { + 'POWERLINE_THEME_OVERRIDES': ';'.join(( + key + '=' + json.dumps(val) + for key, val in ( + ('default.segments.right', [{ + 'type': 'string', + 'name': 's1', + 'highlight_groups': ['cwd'], + 'priority':50, + }]), + ('default.segments.left', [{ + 'type': 'string', + 'name': 's2', + 'highlight_groups': ['background'], + 'priority':20, + }]), + ('default.segment_data.s1.contents', 'S1 string here'), + ('default.segment_data.s2.contents', 'S2 string here'), + ) + )), + 'POWERLINE_TMUX_SOCKET_PATH': socket_path, + }) + + conf_path = os.path.abspath('powerline/bindings/tmux/powerline.conf') + conf_line = 'source "' + ( + conf_path.replace('\\', '\\\\').replace('"', '\\"')) + '"\n' + conf_file = os.path.realpath(os.path.join(TEST_ROOT, 'tmux.conf')) + with open(conf_file, 'w') as cf_fd: + cf_fd.write(conf_line) + + tmux_version = get_tmux_version(get_fallback_logger()) + + dim = MutableDimensions(rows=50, cols=200) + + def prepare_test_1(p): + sleep(5) + + def prepare_test_2(p): + dim.cols = 40 + p.resize(dim) + sleep(5) + + base_attrs = { + ((0, 0, 0), (243, 243, 243), 1, 0, 0): 'lead', + ((243, 243, 243), (11, 11, 11), 0, 0, 0): 'leadsep', + ((255, 255, 255), (11, 11, 11), 0, 0, 0): 'bg', + ((199, 199, 199), (88, 88, 88), 0, 0, 0): 'cwd', + ((88, 88, 88), (11, 11, 11), 0, 0, 0): 'cwdhsep', + ((0, 0, 0), (0, 224, 0), 0, 0, 0): 'defstl', + } + tests = ( + { + 'expected_result': get_expected_result( + tmux_version, + expected_result_old=( + '{lead: 0 }{leadsep: }{bg: S2 string here }' + '{4: 0 }{cwdhsep:| }{6:bash }' + '{bg: }{4: 1- }{cwdhsep:| }{6:bash }' + '{bg: }{7: }{8:2* | }{9:bash }{10: }' + '{bg:' + (' ' * 124) + '}' + '{cwdhsep: }{cwd: S1 string here }', updated(base_attrs, { + ((133, 133, 133), (11, 11, 11), 0, 0, 0): 4, + ((188, 188, 188), (11, 11, 11), 0, 0, 0): 6, + ((11, 11, 11), (0, 102, 153), 0, 0, 0): 7, + ((102, 204, 255), (0, 102, 153), 0, 0, 0): 8, + ((255, 255, 255), (0, 102, 153), 1, 0, 0): 9, + ((0, 102, 153), (11, 11, 11), 0, 0, 0): 10, + })), + expected_result_1_8=( + '{lead: 0 }{leadsep: }{bg: S2 string here }' + '{4: 0 }{cwdhsep:| }{6:bash }' + '{bg: }{4: 1- }{cwdhsep:| }{7:bash }' + '{bg: }{8: }{9:2* | }{10:bash }{7: }' + '{bg:' + (' ' * 124) + '}' + '{cwdhsep: }{cwd: S1 string here }', updated(base_attrs, { + ((133, 133, 133), (11, 11, 11), 0, 0, 0): 4, + ((188, 188, 188), (11, 11, 11), 0, 0, 0): 6, + ((0, 102, 153), (11, 11, 11), 0, 0, 0): 7, + ((11, 11, 11), (0, 102, 153), 0, 0, 0): 8, + ((102, 204, 255), (0, 102, 153), 0, 0, 0): 9, + ((255, 255, 255), (0, 102, 153), 1, 0, 0): 10, + })), + expected_result_2_0=( + '{lead: 0 }{leadsep: }{bg: S2 string here }' + '{4: 0 }{cwdhsep:| }{6:bash }' + '{bg: }{4: 1- }{cwdhsep:| }{7:bash }' + '{bg: }{8: }{9:2* | }{10:bash }{7: }' + '{bg:' + (' ' * 125) + '}' + '{cwdhsep: }{cwd: S1 string here }', updated(base_attrs, { + ((133, 133, 133), (11, 11, 11), 0, 0, 0): 4, + ((188, 188, 188), (11, 11, 11), 0, 0, 0): 6, + ((0, 102, 153), (11, 11, 11), 0, 0, 0): 7, + ((11, 11, 11), (0, 102, 153), 0, 0, 0): 8, + ((102, 204, 255), (0, 102, 153), 0, 0, 0): 9, + ((255, 255, 255), (0, 102, 153), 1, 0, 0): 10, + })), + ), + 'prep_cb': prepare_test_1, + 'row': dim.rows - 1, + }, { + 'expected_result': get_expected_result( + tmux_version, + expected_result_old=('{bg:' + (' ' * 40) + '}', base_attrs), + expected_result_1_7=( + '{lead: 0 }' + '{leadsep: }{bg: <}{4:h }{bg: }{5: }' + '{6:2* | }{7:bash }{8: }{bg: }{cwdhsep: }' + '{cwd: S1 string here }', updated(base_attrs, { + ((188, 188, 188), (11, 11, 11), 0, 0, 0): 4, + ((11, 11, 11), (0, 102, 153), 0, 0, 0): 5, + ((102, 204, 255), (0, 102, 153), 0, 0, 0): 6, + ((255, 255, 255), (0, 102, 153), 1, 0, 0): 7, + ((0, 102, 153), (11, 11, 11), 0, 0, 0): 8, + })), + expected_result_1_8=( + '{lead: 0 }' + '{leadsep: }{bg: <}{4:h }{bg: }{5: }' + '{6:2* | }{7:bash }{4: }{bg: }{cwdhsep: }' + '{cwd: S1 string here }', updated(base_attrs, { + ((0, 102, 153), (11, 11, 11), 0, 0, 0): 4, + ((11, 11, 11), (0, 102, 153), 0, 0, 0): 5, + ((102, 204, 255), (0, 102, 153), 0, 0, 0): 6, + ((255, 255, 255), (0, 102, 153), 1, 0, 0): 7, + })), + expected_result_2_0=( + '{lead: 0 }' + '{leadsep: }{bg:<}{4:ash }{bg: }{5: }' + '{6:2* | }{7:bash }{4: }{cwdhsep: }' + '{cwd: S1 string here }', updated(base_attrs, { + ((0, 102, 153), (11, 11, 11), 0, 0, 0): 4, + ((11, 11, 11), (0, 102, 153), 0, 0, 0): 5, + ((102, 204, 255), (0, 102, 153), 0, 0, 0): 6, + ((255, 255, 255), (0, 102, 153), 1, 0, 0): 7, + })), + ), + 'prep_cb': prepare_test_2, + 'row': dim.rows - 1, + } + ) + + args = [ + # Specify full path to tmux socket (testing tmux instance must not + # interfere with user one) + '-S', socket_path, + # Force 256-color mode + '-2', + # Request verbose logging just in case + '-v', + # Specify configuration file + '-f', conf_file, + # Run bash three times + 'new-session', 'bash --norc --noprofile -i', ';', + 'new-window', 'bash --norc --noprofile -i', ';', + 'new-window', 'bash --norc --noprofile -i', ';', + ] + + with PowerlineTestSuite('tmux') as suite: + return do_terminal_tests( + tests=tests, + cmd=tmux_exe, + dim=dim, + args=args, + env=env, + cwd=TEST_ROOT, + fin_cb=tmux_fin_cb, + last_attempt_cb=print_tmux_logs, + suite=suite, + ) + + +if __name__ == '__main__': + if main(): + raise SystemExit(0) + else: + raise SystemExit(1) diff --git a/tests/test_in_vterm/test_tmux.sh b/tests/test_in_vterm/test_tmux.sh new file mode 100755 index 0000000..ab4e007 --- /dev/null +++ b/tests/test_in_vterm/test_tmux.sh @@ -0,0 +1,44 @@ +#!/bin/bash +. tests/shlib/common.sh +. tests/shlib/vterm.sh + +enter_suite tmux final + +vterm_setup + +ln -s "$(command -v env)" "$TEST_ROOT/path" +ln -s "$(command -v cut)" "$TEST_ROOT/path" +ln -s "$ROOT/scripts/powerline-render" "$TEST_ROOT/path" +ln -s "$ROOT/scripts/powerline-config" "$TEST_ROOT/path" + +test_tmux() { + if test "$PYTHON_IMPLEMENTATION" = PyPy; then + # FIXME PyPy3 segfaults for some reason, PyPy does it as well, but + # occasionally. + return 0 + fi + if ! command -v "${POWERLINE_TMUX_EXE}" ; then + return 0 + fi + ln -sf "$(command -v "${POWERLINE_TMUX_EXE}")" "$TEST_ROOT/path/tmux" + f="$ROOT/tests/test_in_vterm/test_tmux.py" + if ! "${PYTHON}" "$f" ; then + local test_name="$("$POWERLINE_TMUX_EXE" -V 2>&1 | cut -d' ' -f2)" + fail "$test_name" F "Failed vterm test $f" + fi +} + +if test -z "$POWERLINE_TMUX_EXE" && test -d "$ROOT/tests/bot-ci/deps/tmux" +then + for tmux in "$ROOT"/tests/bot-ci/deps/tmux/tmux-*/tmux ; do + export POWERLINE_TMUX_EXE="$tmux" + test_tmux || true + done +else + export POWERLINE_TMUX_EXE="${POWERLINE_TMUX_EXE:-tmux}" + test_tmux || true +fi + +vterm_shutdown + +exit_suite diff --git a/tests/test_in_vterm/test_vim.py b/tests/test_in_vterm/test_vim.py new file mode 100755 index 0000000..0fbc319 --- /dev/null +++ b/tests/test_in_vterm/test_vim.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import os +import sys + +from time import sleep +from subprocess import check_call +from glob import glob1 +from traceback import print_exc + +from powerline.lib.dict import updated + +from tests.modules.lib.terminal import (ExpectProcess, MutableDimensions, + do_terminal_tests, get_env) +from tests.modules import PowerlineTestSuite + + +TEST_ROOT = os.path.abspath(os.environ['TEST_ROOT']) + + +def main(attempts=3): + vterm_path = os.path.join(TEST_ROOT, 'path') + + vim_exe = os.path.join(vterm_path, 'vim') + + env = get_env(vterm_path, TEST_ROOT) + env['ROOT'] = os.path.abspath('.') + + dim = MutableDimensions(rows=50, cols=200) + + vimrc = os.path.join(TEST_ROOT, 'init.vim') + vimrc_contents = ''' + set laststatus=2 + set runtimepath=$ROOT/powerline/bindings/vim + ''' + with open(vimrc, 'w') as vd: + vd.write(vimrc_contents) + + base_attrs = { + (( 64, 64, 255), (0, 0, 0), 0, 0, 0): 'NT', # NonText + ((240, 240, 240), (0, 0, 0), 0, 0, 0): 'N', # Normal + } + + args = [ + '-u', vimrc, + '-i', 'NONE', + ] + + def feed(p): + p.send(':echo strtrans(eval(&statusline[2:]))\n') + + tests = ( + ) + + with PowerlineTestSuite('vim') as suite: + return do_terminal_tests( + tests=tests, + cmd=vim_exe, + dim=dim, + args=args, + env=env, + cwd=TEST_ROOT, + suite=suite, + ) + + +if __name__ == '__main__': + if main(): + raise SystemExit(0) + else: + raise SystemExit(1) diff --git a/tests/test_in_vterm/test_vim.sh b/tests/test_in_vterm/test_vim.sh new file mode 100755 index 0000000..2811e45 --- /dev/null +++ b/tests/test_in_vterm/test_vim.sh @@ -0,0 +1,39 @@ +#!/bin/sh +. tests/shlib/common.sh +. tests/shlib/vterm.sh +. tests/shlib/vim.sh + +enter_suite vvim final + +vterm_setup + +test_vim() { + if test "$PYTHON_IMPLEMENTATION" != CPython ; then + # Can only link with cpython + return 0 + fi + if ! command -v "$POWERLINE_VIM_EXE" ; then + return 0 + fi + ln -sf "$(command -v "${POWERLINE_VIM_EXE}")" "$TEST_ROOT/path/vim" + f="$ROOT/tests/test_in_vterm/test_vim.py" + if ! "${PYTHON}" "$f" ; then + local test_name="$(LANG=C "$POWERLINE_VIM_EXE" --cmd 'echo version' --cmd qa 2>&1 | tail -n2)" + fail "$test_name" F "Failed vterm test $f" + fi +} + +if test -z "$POWERLINE_VIM_EXE" && test -d "$ROOT/tests/bot-ci/deps/vim" +then + for vim in "$OLD_VIM" "$NEW_VIM" ; do + export POWERLINE_VIM_EXE="$vim" + test_vim || true + done +else + export POWERLINE_VIM_EXE="${POWERLINE_VIM_EXE:-vim}" + test_vim || true +fi + +vterm_shutdown + +exit_suite diff --git a/tests/test_lint/test.sh b/tests/test_lint/test.sh new file mode 100755 index 0000000..03c2f8a --- /dev/null +++ b/tests/test_lint/test.sh @@ -0,0 +1,10 @@ +#!/bin/sh +. tests/shlib/common.sh + +enter_suite lint final + +if ! "$PYTHON" "$ROOT/scripts/powerline-lint" -p "$ROOT/powerline/config_files" ; then + fail "test" F "Running powerline-lint failed" +fi + +exit_suite diff --git a/tests/test_python/empty b/tests/test_python/empty new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_python/test.sh b/tests/test_python/test.sh new file mode 100755 index 0000000..f042237 --- /dev/null +++ b/tests/test_python/test.sh @@ -0,0 +1,13 @@ +#!/bin/sh +. tests/shlib/common.sh + +enter_suite python final + +for file in "$ROOT"/tests/test_python/test_*.py ; do + test_name="${file##*/test_}" + if ! "$PYTHON" "$file" --verbose --catch ; then + fail "${test_name%.py}" F "Failed test(s) from $file" + fi +done + +exit_suite diff --git a/tests/test_python/test_cmdline.py b/tests/test_python/test_cmdline.py new file mode 100644 index 0000000..470a7b4 --- /dev/null +++ b/tests/test_python/test_cmdline.py @@ -0,0 +1,149 @@ +# 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.commands.main import get_argparser, finish_args + +from tests.modules import TestCase +from tests.modules.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-override'], 'expected one argument'), + (['shell', '--theme-override'], '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_or_sig 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', 'left'], {'ext': ['shell'], 'side': 'left'}), + (['shell', 'left', '-r', '.zsh'], {'ext': ['shell'], 'renderer_module': '.zsh', 'side': 'left'}), + ([ + '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_override': {'common': {'term_truecolor': True, 'spaces': 4}}, + 'theme_override': { + 'default': { + 'segment_data': { + 'hostname': { + 'before': 'H:' + } + } + } + }, + 'config_path': ['.', '..'], + 'renderer_arg': {'smth': {'abc': 'def'}}, + }), + (['shell', 'left', '-R', 'arg=true'], { + 'ext': ['shell'], + 'side': 'left', + 'renderer_arg': {'arg': True}, + }), + (['shell', 'left', '-R', 'arg=true', '-R', 'arg='], { + 'ext': ['shell'], + 'side': 'left', + 'renderer_arg': {}, + }), + (['shell', 'left', '-R', 'arg='], {'ext': ['shell'], 'renderer_arg': {}, 'side': 'left'}), + (['shell', 'left', '-t', 'default.segment_info={"hostname": {}}'], { + 'ext': ['shell'], + 'side': 'left', + 'theme_override': { + 'default': { + 'segment_info': { + 'hostname': {} + } + } + }, + }), + (['shell', 'left', '-c', 'common={ }'], { + 'ext': ['shell'], + 'side': 'left', + 'config_override': {'common': {}}, + }), + (['shell', 'left', '--last-pipe-status='], { + 'ext': ['shell'], + 'side': 'left', + 'last_pipe_status': [], + }), + ]: + args = parser.parse_args(argv) + finish_args(parser, {}, 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.modules import main + main() diff --git a/tests/test_python/test_config_merging.py b/tests/test_python/test_config_merging.py new file mode 100644 index 0000000..3f4fa2a --- /dev/null +++ b/tests/test_python/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.dict import mergedicts_copy as mdc +from powerline import Powerline + +from tests.modules import TestCase +from tests.modules.lib.config_mock import select_renderer, UT + + +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', 'attrs': []}, + } +} + + +theme_config = lambda: { + 'segment_data': { + 's': { + 'before': 'b', + }, + }, + 'segments': { + 'left': [ + { + 'type': 'string', + 'name': 's', + 'contents': 't', + 'highlight_groups': ['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/' + UT: 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.modules.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/' + UT: { + 'spaces': 1, + }, + '3/themes/' + UT: { + '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', 'attrs': []}, + } + }, + })) 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.modules import main + main() diff --git a/tests/test_python/test_config_reload.py b/tests/test_python/test_config_reload.py new file mode 100644 index 0000000..a418d49 --- /dev/null +++ b/tests/test_python/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.modules import TestCase +from tests.modules.lib.config_mock import get_powerline, add_watcher_events, UT + + +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', 'attrs': ['bold']}, + 'str2': {'fg': 'col3', 'bg': 'col4', 'attrs': ['underline']}, + }, + }, + 'colorschemes/test/2': { + 'groups': { + 'str1': {'fg': 'col2', 'bg': 'col3', 'attrs': ['bold']}, + 'str2': {'fg': 'col1', 'bg': 'col4', 'attrs': ['underline']}, + }, + }, + 'themes/test/default': { + 'segments': { + "left": [ + { + "type": "string", + "contents": "s", + "highlight_groups": ["str1"], + }, + { + "type": "string", + "contents": "g", + "highlight_groups": ["str2"], + }, + ], + "right": [ + ], + }, + }, + 'themes/' + UT: { + '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_groups": ["str1"], + }, + { + "type": "string", + "contents": "b", + "highlight_groups": ["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/' + UT, '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/' + UT, '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/' + UT, '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', 'attrs': ['bold']}, + 'str2': {'fg': 'col2', 'bg': 'col4', 'attrs': ['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/' + UT, '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/' + UT, '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/' + UT, '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/' + UT, '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/' + UT, 'check:themes/test/__main__') + + config['themes/' + UT]['dividers']['left']['hard'] = '|>' + add_watcher_events(p, 'themes/' + UT) + 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/' + UT, '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/' + UT, '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/' + UT, '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/' + UT, '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.modules import main + main() diff --git a/tests/test_python/test_configuration.py b/tests/test_python/test_configuration.py new file mode 100644 index 0000000..aa9e844 --- /dev/null +++ b/tests/test_python/test_configuration.py @@ -0,0 +1,877 @@ +# 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.modules.vim as vim_module + +from tests.modules import TestCase +from tests.modules.lib.config_mock import (get_powerline, get_powerline_raw, + swap_attributes, UT) +from tests.modules.lib import Args, replace_item + + +def highlighted_string(s, group, **kwargs): + ret = { + 'type': 'string', + 'contents': s, + 'highlight_groups': [group], + } + ret.update(kwargs) + return ret + + +config = { + 'config': { + 'common': { + 'interval': 0, + 'watcher': 'test', + }, + 'ext': { + 'test': { + 'theme': 'default', + 'colorscheme': 'default', + }, + 'vim': { + 'theme': 'default', + 'colorscheme': 'default', + }, + 'shell': { + 'theme': 'default', + 'colorscheme': 'default', + }, + 'wm': { + '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', 'attrs': []}, + } + }, + 'colorschemes/default': { + 'groups': { + 'g1': {'fg': 'col5', 'bg': 'col6', 'attrs': []}, + 'g2': {'fg': 'col7', 'bg': 'col8', 'attrs': []}, + 'g3': {'fg': 'col9', 'bg': 'col10', 'attrs': []}, + } + }, + 'colorschemes/test/default': { + 'groups': { + 'str1': {'fg': 'col1', 'bg': 'col2', 'attrs': ['bold']}, + 'str2': {'fg': 'col3', 'bg': 'col4', 'attrs': ['underline']}, + 'd1': 'g2', + 'd2': 'm2', + 'd3': 'm3', + }, + }, + 'colorschemes/vim/default': { + 'groups': { + 'environment': {'fg': 'col3', 'bg': 'col4', 'attrs': ['underline']}, + }, + }, + 'colorschemes/wm/default': { + 'groups': { + 'hl1': {'fg': 'col1', 'bg': 'col2', 'attrs': ['underline']}, + 'hl2': {'fg': 'col2', 'bg': 'col1', 'attrs': []}, + 'hl3': {'fg': 'col3', 'bg': 'col1', 'attrs': ['underline']}, + 'hl4': {'fg': 'col2', 'bg': 'col4', 'attrs': []}, + }, + }, + '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/' + UT: { + 'dividers': { + 'left': { + 'hard': '>>', + 'soft': '>', + }, + 'right': { + 'hard': '<<', + 'soft': '<', + }, + }, + 'spaces': 0, + }, + 'themes/test/__main__': { + 'dividers': { + 'right': { + 'soft': '|', + }, + }, + }, + 'themes/vim/default': { + 'segments': { + 'left': [ + { + 'function': 'powerline.segments.common.env.environment', + 'args': { + 'variable': 'TEST', + }, + }, + ], + }, + }, + 'themes/shell/default': { + 'default_module': 'powerline.segments.common', + 'segments': { + 'left': [ + highlighted_string('s', 'g1', width='auto'), + ], + }, + }, + 'themes/wm/default': { + 'default_module': 'powerline.segments.common', + 'segments': { + 'left': [ + highlighted_string('A', 'hl1'), + highlighted_string('B', 'hl2'), + ], + 'right': [ + highlighted_string('C', 'hl3'), + highlighted_string('D', 'hl4'), + ], + }, + }, +} + + +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/' + UT]['dividers'] + config['themes/test/__main__']['spaces'] = 1 + del config['themes/' + UT] + 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/' + UT]['dividers'] + config['themes/test/default']['spaces'] = 1 + del config['themes/test/__main__'] + del config['themes/' + UT] + 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/' + UT] + self.assertRenderEqual(p, 'themes/test/default') + self.assertEqual(p.logger._pop_msgs(), [ + 'exception:test:powerline:Failed to load theme: themes/' + UT, + '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/' + UT] + 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/' + UT, + '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 TestOuterPadding(TestRender): + @add_args + def test_outer_padding_left(self, p, config): + config['themes/' + UT]['outer_padding'] = 5 + self.assertRenderEqual(p, '{121} s{24}>>{344}g{4-}>>{--}', side='left') + + @add_args + def test_outer_padding_right(self, p, config): + config['themes/' + UT]['outer_padding'] = 5 + self.assertRenderEqual(p, '{4-}<<{344}f {--}', side='right') + + @add_args + def test_outer_padding_ten(self, p, config): + config['themes/' + UT]['outer_padding'] = 10 + self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}|{344} f {--}', width=30) + + @add_args + def test_outer_padding_zero(self, p, config): + config['themes/' + UT]['outer_padding'] = 0 + self.assertRenderEqual(p, '{121}s {24}>>{344}g{34}>{34}|{344} f{--}', width=30) + + +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 = { + UT: { + '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/' + UT]['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 TestShellEscapes(TestCase): + @with_new_config + def test_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1b[0;38;5;5;48;5;6m\xa0s\x1b[0;38;5;6;49;22m>>\x1b[0m') + + @with_new_config + def test_tmux_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['additional_escapes'] = 'tmux' + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1bPtmux;\x1b\x1b[0;38;5;5;48;5;6m\x1b\\\xa0s\x1bPtmux;\x1b\x1b[0;38;5;6;49;22m\x1b\\>>\x1bPtmux;\x1b\x1b[0m\x1b\\') + + @with_new_config + def test_screen_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['additional_escapes'] = 'screen' + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1bP\x1b\x1b[0;38;5;5;48;5;6m\x1b\\\xa0s\x1bP\x1b\x1b[0;38;5;6;49;22m\x1b\\>>\x1bP\x1b\x1b[0m\x1b\\') + + @with_new_config + def test_fbterm_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['term_escape_style'] = 'fbterm' + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1b[0m\x1b[1;5}\x1b[2;6}\xa0s\x1b[0m\x1b[1;6}\x1b[49m\x1b[22m>>\x1b[0m') + + @with_new_config + def test_fbterm_tmux_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['term_escape_style'] = 'fbterm' + config['config']['common']['additional_escapes'] = 'tmux' + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1bPtmux;\x1b\x1b[0m\x1b\x1b[1;5}\x1b\x1b[2;6}\x1b\\\xa0s\x1bPtmux;\x1b\x1b[0m\x1b\x1b[1;6}\x1b\x1b[49m\x1b\x1b[22m\x1b\\>>\x1bPtmux;\x1b\x1b[0m\x1b\\') + + @with_new_config + def test_fbterm_screen_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['term_escape_style'] = 'fbterm' + config['config']['common']['additional_escapes'] = 'screen' + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1bP\x1b\x1b[0m\x1b\x1b[1;5}\x1b\x1b[2;6}\x1b\\\xa0s\x1bP\x1b\x1b[0m\x1b\x1b[1;6}\x1b\x1b[49m\x1b\x1b[22m\x1b\\>>\x1bP\x1b\x1b[0m\x1b\\') + + @with_new_config + def test_term_truecolor_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['term_truecolor'] = True + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1b[0;38;2;192;0;192;48;2;0;128;128m\xa0s\x1b[0;38;2;0;128;128;49;22m>>\x1b[0m') + + @with_new_config + def test_term_truecolor_fbterm_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['term_escape_style'] = 'fbterm' + config['config']['common']['term_truecolor'] = True + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1b[0m\x1b[1;5}\x1b[2;6}\xa0s\x1b[0m\x1b[1;6}\x1b[49m\x1b[22m>>\x1b[0m') + + @with_new_config + def test_term_truecolor_tmux_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['term_truecolor'] = True + config['config']['common']['additional_escapes'] = 'tmux' + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1bPtmux;\x1b\x1b[0;38;2;192;0;192;48;2;0;128;128m\x1b\\\xa0s\x1bPtmux;\x1b\x1b[0;38;2;0;128;128;49;22m\x1b\\>>\x1bPtmux;\x1b\x1b[0m\x1b\\') + + @with_new_config + def test_term_truecolor_screen_escapes(self, config): + from powerline.shell import ShellPowerline + import powerline as powerline_module + config['config']['common']['term_truecolor'] = True + config['config']['common']['additional_escapes'] = 'screen' + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, ShellPowerline, args=Args(config_path=[''])) as powerline: + self.assertEqual(powerline.render(segment_info={}, side='left'), '\x1bP\x1b\x1b[0;38;2;192;0;192;48;2;0;128;128m\x1b\\\xa0s\x1bP\x1b\x1b[0;38;2;0;128;128;49;22m\x1b\\>>\x1bP\x1b\x1b[0m\x1b\\') + + +class TestVim(TestCase): + def test_environ_update(self): + # Regression test: test that segment obtains environment from vim, not + # from os.environ. + with vim_module._with('globals', powerline_config_paths=['/']): + from powerline.vim import VimPowerline + import powerline as powerline_module + 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), b'%#Pl_3_8404992_4_192_underline#\xc2\xa0abc%#Pl_4_192_NONE_None_NONE#>>') + vim_module._environ['TEST'] = 'def' + self.assertEqual(powerline.render(window, window_id, winnr), b'%#Pl_3_8404992_4_192_underline#\xc2\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, replace_gcp=True) as powerline: + powerline.add_local_theme('tests.modules.matchers.always_true', { + 'segment_data': { + 'foo': { + 'contents': '“bar”' + } + }, + 'segments': { + 'left': [ + { + 'type': 'string', + 'name': 'foo', + 'highlight_groups': ['g1'] + } + ] + } + }) + window = vim_module.current.window + window_id = 1 + winnr = window.number + self.assertEqual(powerline.render(window, window_id, winnr), b'%#Pl_5_12583104_6_32896_NONE#\xc2\xa0\xe2\x80\x9cbar\xe2\x80\x9d%#Pl_6_32896_NONE_None_NONE#>>') + + @classmethod + def setUpClass(cls): + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'vim_sys_path'))) + + @classmethod + def tearDownClass(cls): + sys.path.pop(0) + + +class TestLemonbar(TestRender): + def test_lemonbar(self): + import powerline as powerline_module + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, powerline_module.Powerline, replace_gcp=True, ext='wm', renderer_module='lemonbar') as powerline: + self.assertRenderEqual( + powerline, + '%{l}%{F#ffc00000}%{B#ff008000}%{+u} A%{F-B--u}%{F#ff008000}%{B#ffc00000}>>%{F-B--u}%{F#ff008000}%{B#ffc00000}B%{F-B--u}%{F#ffc00000}>>%{F-B--u}%{r}%{F#ffc00000}<<%{F-B--u}%{F#ff804000}%{B#ffc00000}%{+u}C%{F-B--u}%{F#ff0000c0}%{B#ffc00000}<<%{F-B--u}%{F#ff008000}%{B#ff0000c0}D %{F-B--u}' + ) + + @with_new_config + def test_lemonbar_escape(self, config): + import powerline as powerline_module + config['themes/wm/default']['segments']['left'] = ( + highlighted_string('%{asd}', 'hl1'), + highlighted_string('10% %', 'hl2'), + ) + with swap_attributes(config, powerline_module): + with get_powerline_raw(config, powerline_module.Powerline, replace_gcp=True, ext='wm', renderer_module='lemonbar') as powerline: + self.assertRenderEqual( + powerline, + '%{l}%{F#ffc00000}%{B#ff008000}%{+u} %%{}{asd}%{F-B--u}%{F#ff008000}%{B#ffc00000}>>%{F-B--u}%{F#ff008000}%{B#ffc00000}10%%{} %%{}%{F-B--u}%{F#ffc00000}>>%{F-B--u}%{r}%{F#ffc00000}<<%{F-B--u}%{F#ff804000}%{B#ffc00000}%{+u}C%{F-B--u}%{F#ff0000c0}%{B#ffc00000}<<%{F-B--u}%{F#ff008000}%{B#ff0000c0}D %{F-B--u}' + ) + + +if __name__ == '__main__': + from tests.modules import main + main() diff --git a/tests/test_python/test_lib.py b/tests/test_python/test_lib.py new file mode 100644 index 0000000..4c85436 --- /dev/null +++ b/tests/test_python/test_lib.py @@ -0,0 +1,733 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import threading +import os +import sys +import re +import shutil +import unicodedata + +from time import sleep +from subprocess import call, PIPE + +from powerline.lib import add_divider_highlight_group +from powerline.lib.dict import mergedicts, 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 powerline.lib.shell import run_cmd + +import powerline.lib.unicode as plu + +from tests.modules.lib import Pl, replace_attr +from tests.modules import TestCase, SkipTest + + +try: + __import__('bzrlib') +except ImportError: + use_bzr = False +else: + use_bzr = True + +try: + __import__('hglib') +except ImportError: + use_mercurial = False +else: + use_mercurial = True + + +GIT_REPO = 'git_repo' +HG_REPO = 'hg_repo' +BZR_REPO = 'bzr_repo' + + +def thread_number(): + return len(threading.enumerate()) + + +class TestShell(TestCase): + def test_run_cmd(self): + pl = Pl() + self.assertEqual(run_cmd(pl, ['xxx_nonexistent_command_xxx']), None) + self.assertEqual(len(pl.exceptions), 1) + pl = Pl() + self.assertEqual(run_cmd(pl, ['echo', ' test ']), 'test') + self.assertFalse(pl) + self.assertEqual(run_cmd(pl, ['echo', ' test '], strip=True), 'test') + self.assertFalse(pl) + self.assertEqual(run_cmd(pl, ['echo', ' test '], strip=False), ' test \n') + self.assertFalse(pl) + self.assertEqual(run_cmd(pl, ['cat'], stdin='test'), 'test') + self.assertFalse(pl) + self.assertEqual(run_cmd(pl, ['sh', '-c', 'cat >&2'], stdin='test'), '') + self.assertFalse(pl) + + +class TestThreaded(TestCase): + def test_threaded_segment(self): + log = [] + 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') + + +width_data = { + 'N': 1, # Neutral + 'Na': 1, # Narrow + 'A': 1, # Ambiguous + 'H': 1, # Half-width + 'W': 2, # Wide + 'F': 2, # Fullwidth +} + + +class TestUnicode(TestCase): + def assertStringsIdentical(self, s1, s2): + self.assertTrue(type(s1) is type(s2), msg='string types differ') + self.assertEqual(s1, s2) + + def test_unicode(self): + self.assertTrue(type('abc') is plu.unicode) + + def test_unichr(self): + self.assertStringsIdentical('\U0010FFFF', plu.unichr(0x10FFFF)) + self.assertStringsIdentical('\uFFFF', plu.unichr(0xFFFF)) + self.assertStringsIdentical('\x20', plu.unichr(0x20)) + + def test_u(self): + self.assertStringsIdentical('Test', plu.u('Test')) + self.assertStringsIdentical('Test', plu.u(b'Test')) + self.assertStringsIdentical('«»', plu.u(b'\xC2\xAB\xC2\xBB')) + self.assertRaises(UnicodeDecodeError, plu.u, b'\xFF') + + def test_tointiter(self): + self.assertEqual([1, 2, 3], list(plu.tointiter(b'\x01\x02\x03'))) + + def test_decode_error(self): + self.assertStringsIdentical('', b'\xFF'.decode('utf-8', 'powerline_decode_error')) + self.assertStringsIdentical('abc', b'abc'.decode('utf-8', 'powerline_decode_error')) + + def test_register_strwidth_error(self): + ename = plu.register_strwidth_error(lambda s: 3) + self.assertStringsIdentical(b'???', 'A'.encode('latin1', ename)) + self.assertStringsIdentical(b'abc', 'abc'.encode('latin1', ename)) + + def test_out_u(self): + self.assertStringsIdentical('abc', plu.out_u('abc')) + self.assertStringsIdentical('abc', plu.out_u(b'abc')) + self.assertRaises(TypeError, plu.out_u, None) + + def test_safe_unicode(self): + self.assertStringsIdentical('abc', plu.safe_unicode('abc')) + self.assertStringsIdentical('abc', plu.safe_unicode(b'abc')) + self.assertStringsIdentical('«»', plu.safe_unicode(b'\xc2\xab\xc2\xbb')) + with replace_attr(plu, 'get_preferred_output_encoding', lambda: 'latin1'): + self.assertStringsIdentical('ÿ', plu.safe_unicode(b'\xFF')) + self.assertStringsIdentical('None', plu.safe_unicode(None)) + + class FailingStr(object): + def __str__(self): + raise NotImplementedError('Fail!') + + self.assertStringsIdentical('Fail!', plu.safe_unicode(FailingStr())) + + def test_FailedUnicode(self): + self.assertTrue(isinstance(plu.FailedUnicode('abc'), plu.unicode)) + self.assertEqual('abc', plu.FailedUnicode('abc')) + + def test_string(self): + self.assertStringsIdentical(str('abc'), plu.string('abc')) + self.assertStringsIdentical(str('abc'), plu.string(b'abc')) + + def test_surrogate_pair_to_character(self): + self.assertEqual(0x1F48E, plu.surrogate_pair_to_character(0xD83D, 0xDC8E)) + + def test_strwidth_ucs_4(self): + self.assertEqual(4, plu.strwidth_ucs_4(width_data, 'abcd')) + self.assertEqual(4, plu.strwidth_ucs_4(width_data, 'AB')) + if sys.maxunicode < 0x10FFFF: + raise SkipTest('Can only test strwidth_ucs_4 in UCS-4 Pythons') + + self.assertEqual(1, plu.strwidth_ucs_4(width_data, '\U0001F063')) + + def test_strwidth_ucs_2(self): + self.assertEqual(4, plu.strwidth_ucs_2(width_data, 'abcd')) + self.assertEqual(4, plu.strwidth_ucs_2(width_data, 'AB')) + if not sys.maxunicode < 0x10FFFF: + raise SkipTest('Can only test strwidth_ucs_2 in UCS-2 Pythons') + self.assertEqual(1, plu.strwidth_ucs_2(width_data, '\ud83c\udc30')) + + +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(r'^[a-f0-9]+$', b)) + finally: + call(['git', 'checkout', '-q', 'master'], cwd=GIT_REPO) + # Test stashing + self.assertEqual(repo.stash(), 0) + + def stash_save(): + with open(os.path.join(GIT_REPO, 'file'), 'w') as f: + f.write('abc') + return call(['git', 'stash', '-u'], cwd=GIT_REPO, stdout=PIPE) + + def stash_drop(): + return call(['git', 'stash', 'drop'], cwd=GIT_REPO, stdout=PIPE) + + def stash_list(): + return call(['git', 'stash', 'list'], cwd=GIT_REPO, stdout=PIPE) + + try: + stash_save() + self.assertEqual(repo.stash(), 1) + stash_save() + self.assertEqual(repo.stash(), 2) + stash_drop() + self.assertEqual(repo.stash(), 1) + stash_drop() + self.assertEqual(repo.stash(), 0) + finally: + while stash_list(): + stash_drop() + + 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(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.modules import main + main() diff --git a/tests/test_python/test_lib_config.py b/tests/test_python/test_lib_config.py new file mode 100644 index 0000000..053462a --- /dev/null +++ b/tests/test_python/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.modules import TestCase +from tests.modules.lib.fsconfig import FSTree + + +FILE_ROOT = os.path.join(os.path.dirname(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.modules import main + main() diff --git a/tests/test_python/test_listers.py b/tests/test_python/test_listers.py new file mode 100644 index 0000000..f2368df --- /dev/null +++ b/tests/test_python/test_listers.py @@ -0,0 +1,159 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import powerline.listers.i3wm as i3wm + +from tests.modules.lib import Args, replace_attr, Pl +from tests.modules import TestCase + + +class TestI3WM(TestCase): + workspaces = [ + Args(name='1: w1', output='LVDS1', focused=False, urgent=False, visible=False), + Args(name='2: w2', output='LVDS1', focused=False, urgent=False, visible=True), + Args(name='3: w3', output='HDMI1', focused=False, urgent=True, visible=True), + Args(name='4: w4', output='DVI01', focused=True, urgent=True, visible=True), + ] + + @staticmethod + def get_workspaces(): + return iter(TestI3WM.workspaces) + + @staticmethod + def get_outputs(pl): + return iter([ + {'name': 'LVDS1'}, + {'name': 'HDMI1'}, + {'name': 'DVI01'}, + ]) + + def test_output_lister(self): + pl = Pl() + with replace_attr(i3wm, 'get_connected_xrandr_outputs', self.get_outputs): + self.assertEqual( + list(i3wm.output_lister(pl=pl, segment_info={'a': 1})), + [ + ({'a': 1, 'output': 'LVDS1'}, {'draw_inner_divider': None}), + ({'a': 1, 'output': 'HDMI1'}, {'draw_inner_divider': None}), + ({'a': 1, 'output': 'DVI01'}, {'draw_inner_divider': None}), + ] + ) + + def test_workspace_lister(self): + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Args(get_workspaces=self.get_workspaces)): + self.assertEqual( + list(i3wm.workspace_lister(pl=pl, segment_info={'a': 1})), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': self.workspaces[0], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': self.workspaces[1], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': self.workspaces[2], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': self.workspaces[3], + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister(pl=pl, segment_info={'a': 1}, output='LVDS1')), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': self.workspaces[0], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': self.workspaces[1], + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1, 'output': 'LVDS1'} + )), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': self.workspaces[0], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': self.workspaces[1], + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1, 'output': 'LVDS1'}, + output=False + )), + [ + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': self.workspaces[0], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'LVDS1', + 'workspace': self.workspaces[1], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': self.workspaces[2], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': self.workspaces[3], + }, {'draw_inner_divider': None}), + ] + ) + + self.assertEqual( + list(i3wm.workspace_lister( + pl=pl, + segment_info={'a': 1}, + only_show=['focused', 'urgent'] + )), + [ + ({ + 'a': 1, + 'output': 'HDMI1', + 'workspace': self.workspaces[2], + }, {'draw_inner_divider': None}), + ({ + 'a': 1, + 'output': 'DVI01', + 'workspace': self.workspaces[3], + }, {'draw_inner_divider': None}), + ] + ) + + +if __name__ == '__main__': + from tests.modules import main + main() diff --git a/tests/test_python/test_logging.py b/tests/test_python/test_logging.py new file mode 100644 index 0000000..d7cfe4a --- /dev/null +++ b/tests/test_python/test_logging.py @@ -0,0 +1,467 @@ +# vim:fileencoding=utf-8:noet + +'''Tests for various logging features''' + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import re +import codecs +import os + +from io import StringIO +from shutil import rmtree + +from powerline import finish_common_config, create_logger + +from tests.modules import TestCase +from tests.modules.lib import replace_attr + + +TIMESTAMP_RE = r'\d{4}-\d\d-\d\d \d\d:\d\d:\d\d,\d{3}' + + +class TestRE(TestCase): + def assertMatches(self, text, regexp): + self.assertTrue( + re.match(regexp, text), + '{0!r} did not match {1!r}'.format(text, regexp), + ) + + +def close_handlers(logger): + for handler in logger.handlers: + handler.close() + + +class TestHandlers(TestRE): + def test_stderr_handler_is_default(self): + out = StringIO() + err = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config) + pl.error('Foo') + close_handlers(logger) + self.assertMatches(err.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(out.getvalue(), '') + + def test_stream_override(self): + out = StringIO() + err = StringIO() + stream = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertMatches(stream.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_explicit_none(self): + out = StringIO() + err = StringIO() + stream = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [None]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertMatches(stream.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_explicit_stream_handler(self): + out = StringIO() + err = StringIO() + stream = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [['logging.StreamHandler', [[]]]]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertEqual(stream.getvalue(), '') + self.assertMatches(err.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(out.getvalue(), '') + + def test_explicit_stream_handler_implicit_stream(self): + out = StringIO() + err = StringIO() + stream = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [['logging.StreamHandler', []]]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertMatches(stream.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_file_handler(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name = 'test_logging-test_file_handler' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': file_name}) + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_file_handler_create_dir(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name = 'test_logging-test_file_handler_create_dir/file' + + self.assertFalse(os.path.isdir(os.path.dirname(file_name))) + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': file_name}) + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + self.assertTrue(os.path.isdir(os.path.dirname(file_name))) + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + rmtree(os.path.dirname(file_name)) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_multiple_files(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name_1 = 'test_logging-test_multiple_files-1' + file_name_2 = file_name_1[:-1] + '2' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [file_name_1, file_name_2]}) + try: + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + for file_name in (file_name_1, file_name_2): + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name_1) + finally: + os.unlink(file_name_2) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_multiple_files_and_stream(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name_1 = 'test_logging-test_multiple_files_and_stream-1' + file_name_2 = file_name_1[:-1] + '2' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [file_name_1, file_name_2, None]}) + try: + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + for file_name in (file_name_1, file_name_2): + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name_1) + finally: + os.unlink(file_name_2) + self.assertMatches(stream.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_handler_args(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name = 'test_logging-test_handler_args' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['RotatingFileHandler', [[file_name]]] + ]}) + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + close_handlers(logger) + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_handler_args_kwargs(self): + out = StringIO() + err = StringIO() + stream = StringIO() + file_name = 'test_logging-test_handler_args_kwargs' + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['RotatingFileHandler', [[file_name], {'maxBytes': 1, 'backupCount': 1}]] + ]}) + try: + try: + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.error('Foo') + pl.error('Bar') + close_handlers(logger) + with codecs.open(file_name, encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$') + with codecs.open(file_name + '.1', encoding='utf-8') as fp: + self.assertMatches(fp.read(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Foo\n$') + finally: + os.unlink(file_name + '.1') + finally: + os.unlink(file_name) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_logger_level(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + stream2 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'WARNING'], + ['logging.StreamHandler', [[stream2]], 'ERROR'], + ]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertMatches(stream1.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:Foo\n' + + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$' + )) + self.assertMatches(stream2.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$') + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_logger_level_not_overriding_default(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'DEBUG'], + ]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.debug('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertMatches(stream1.getvalue(), '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$') + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_top_log_level(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'DEBUG'], + ], 'log_level': 'DEBUG'}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.debug('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertMatches(stream1.getvalue(), ( + '^' + TIMESTAMP_RE + ':DEBUG:__unknown__:Foo\n' + + TIMESTAMP_RE + ':ERROR:__unknown__:Bar\n$' + )) + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_logger_format(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'WARNING', 'FOO'], + ]}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertEqual(stream1.getvalue(), 'FOO\nFOO\n') + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + def test_top_log_format(self): + out = StringIO() + err = StringIO() + stream = StringIO() + stream1 = StringIO() + stream2 = StringIO() + + with replace_attr(sys, 'stdout', out, 'stderr', err): + common_config = finish_common_config('utf-8', {'log_file': [ + ['logging.StreamHandler', [[stream1]], 'WARNING', 'FOO'], + ['logging.StreamHandler', [[stream2]], 'WARNING'], + ], 'log_format': 'BAR'}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('Foo') + pl.error('Bar') + close_handlers(logger) + self.assertEqual(stream2.getvalue(), 'BAR\nBAR\n') + self.assertEqual(stream1.getvalue(), 'FOO\nFOO\n') + self.assertEqual(stream.getvalue(), '') + self.assertEqual(err.getvalue(), '') + self.assertEqual(out.getvalue(), '') + + +class TestPowerlineLogger(TestRE): + def test_args_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('foo {0}', 'Test') + pl.warn('bar {0!r}', 'Test') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:foo Test\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:bar u?\'Test\'\n$' + )) + + def test_prefix_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.prefix = '1' + pl.warn('foo') + pl.prefix = '2' + pl.warn('bar') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:1:foo\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:2:bar\n$' + )) + + def test_kwargs_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('foo {arg}', arg='Test') + pl.warn('bar {arg!r}', arg='Test') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:foo Test\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:bar u?\'Test\'\n$' + )) + + def test_args_kwargs_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.warn('foo {0!r} {arg}', 'Test0', arg='Test') + pl.warn('bar {0} {arg!r}', 'Test0', arg='Test') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':WARNING:__unknown__:foo u?\'Test0\' Test\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:bar Test0 u?\'Test\'\n$' + )) + + def test_exception_formatting(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + try: + raise ValueError('foo') + except ValueError: + pl.exception('Message') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':ERROR:__unknown__:Message\n' + + 'Traceback \\(most recent call last\\):\n' + + '(?: File ".*?", line \\d+, in \\w+\n [^\n]*\n)+' + + 'ValueError: foo\n$' + )) + + def test_levels(self): + stream = StringIO() + + common_config = finish_common_config('utf-8', {'log_level': 'DEBUG'}) + logger, pl, get_module_attr = create_logger(common_config, stream=stream) + pl.debug('1') + pl.info('2') + pl.warn('3') + pl.error('4') + pl.critical('5') + close_handlers(logger) + self.assertMatches(stream.getvalue(), ( + '^' + TIMESTAMP_RE + ':DEBUG:__unknown__:1\n' + + TIMESTAMP_RE + ':INFO:__unknown__:2\n' + + TIMESTAMP_RE + ':WARNING:__unknown__:3\n' + + TIMESTAMP_RE + ':ERROR:__unknown__:4\n' + + TIMESTAMP_RE + ':CRITICAL:__unknown__:5\n$' + )) + + +old_cwd = None + + +def setUpModule(): + global old_cwd + global __file__ + old_cwd = os.getcwd() + __file__ = os.path.abspath(__file__) + os.chdir(os.path.dirname(os.path.dirname(__file__))) + + +def tearDownModule(): + global old_cwd + os.chdir(old_cwd) + + +if __name__ == '__main__': + from tests.modules import main + main() diff --git a/tests/test_python/test_provided_config_files.py b/tests/test_python/test_provided_config_files.py new file mode 100644 index 0000000..fd8b16e --- /dev/null +++ b/tests/test_python/test_provided_config_files.py @@ -0,0 +1,201 @@ +# 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 logging + +import tests.modules.vim as vim_module + +from tests.modules.lib import Args, urllib_read, replace_attr +from tests.modules import TestCase + +from powerline import NotInterceptedError +from powerline.segments.common import wthr + + +VBLOCK = chr(ord('V') - 0x40) +SBLOCK = chr(ord('S') - 0x40) + + +class FailingLogger(logging.Logger): + def exception(self, *args, **kwargs): + super(FailingLogger, self).exception(*args, **kwargs) + raise NotInterceptedError('Unexpected exception occurred') + + +def get_logger(stream=None): + 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 = FailingLogger('powerline') + logger.setLevel(level) + logger.addHandler(handler) + return logger + + +class TestVimConfig(TestCase): + def test_vim(self): + from powerline.vim import VimPowerline + cfg_path = os.path.join(os.path.dirname(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__'), {}), + # No Command-T tests here: requires +ruby or emulation + # No tabline here: tablines are tested separately + ) + 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()) + # See end of the buffers definition above for `- 2` + self.assertEqual(len(buffers), len(local_themes) - 2) + outputs = {} + i = 0 + + with vim_module._with('split'): + with VimPowerline(logger=get_logger()) 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 + 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(os.path.dirname(__file__)), 'vim_sys_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']), logger=get_logger(), run_once=False) as powerline: + powerline.render() + with ShellPowerline(Args(ext=['tmux']), logger=get_logger(), 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, logger=get_logger(), run_once=False) as powerline: + powerline.render(segment_info=segment_info) + with ShellPowerline(args, logger=get_logger(), run_once=False) as powerline: + powerline.render(segment_info=segment_info) + segment_info['local_theme'] = 'select' + with ShellPowerline(args, logger=get_logger(), 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, logger=get_logger(), 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, last_pipe_status=[], jobnum=0, ext=['shell'], renderer_module='.bash', config_override={'ext': {'shell': {'theme': 'default_leftonly'}}}) + with ShellPowerline(args, logger=get_logger(), run_once=False) as powerline: + powerline.render(segment_info={'args': args}) + with ShellPowerline(args, logger=get_logger(), run_once=False) as powerline: + powerline.render(segment_info={'args': args}) + + def test_ipython(self): + from powerline.ipython import IPythonPowerline + + class IpyPowerline(IPythonPowerline): + config_paths = None + config_overrides = None + theme_overrides = {} + + segment_info = Args(prompt_count=1) + + with IpyPowerline(logger=get_logger(), renderer_module='.pre_5') 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(logger=get_logger(), renderer_module='.pre_5') 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(wthr, 'urllib_read', urllib_read): + Powerline(logger=get_logger(), 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(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.modules import main + main() diff --git a/tests/test_python/test_segments.py b/tests/test_python/test_segments.py new file mode 100644 index 0000000..c7a9870 --- /dev/null +++ b/tests/test_python/test_segments.py @@ -0,0 +1,1846 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import os + +from functools import partial +from collections import namedtuple +from time import sleep +from platform import python_implementation + +from powerline.segments import shell, tmux, pdb, i3wm +from powerline.lib.vcs import get_fallback_create_watcher +from powerline.lib.unicode import out_u + +import tests.modules.vim as vim_module + +from tests.modules.lib import (Args, urllib_read, replace_attr, new_module, + replace_module_module, replace_env, Pl) +from tests.modules import TestCase, SkipTest + + +def get_dummy_guess(**kwargs): + if 'directory' in kwargs: + def guess(path, create_watcher): + return Args(branch=lambda: out_u(os.path.basename(path)), **kwargs) + else: + def guess(path, create_watcher): + return Args(branch=lambda: out_u(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_groups': ['exit_fail']} + ]) + segment_info['args'].last_exit_code = 137 + self.assertEqual(shell.last_status(pl=pl, segment_info=segment_info), [ + {'contents': 'SIGKILL', 'highlight_groups': ['exit_fail']} + ]) + self.assertEqual(shell.last_status(pl=pl, segment_info=segment_info, signal_names=False), [ + {'contents': '137', 'highlight_groups': ['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) + segment_info['args'].last_exit_code = 'sigsegv' + self.assertEqual(shell.last_status(pl=pl, segment_info=segment_info), [ + {'contents': 'sigsegv', 'highlight_groups': ['exit_fail']} + ]) + segment_info['args'].last_exit_code = 'sigsegv+core' + self.assertEqual(shell.last_status(pl=pl, segment_info=segment_info), [ + {'contents': 'sigsegv+core', 'highlight_groups': ['exit_fail']} + ]) + + def test_last_pipe_status(self): + pl = Pl() + segment_info = {'args': Args(last_pipe_status=[], last_exit_code=0)} + 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, 0] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), None) + segment_info['args'].last_pipe_status = [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_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': '2', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + ]) + segment_info['args'].last_pipe_status = [2, 0, 0] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': '2', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + ]) + segment_info['args'].last_pipe_status = [137, 0, 0] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': 'SIGKILL', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + ]) + + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info, signal_names=False), [ + {'contents': '137', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + ]) + + segment_info['args'].last_pipe_status = [0, 0, 2] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': '2', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + ]) + segment_info['args'].last_pipe_status = [2] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': '2', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + ]) + segment_info['args'].last_pipe_status = [0, 'sigsegv', 'sigsegv+core'] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': 'sigsegv', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + {'contents': 'sigsegv+core', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True} + ]) + segment_info['args'].last_pipe_status = [0, 'sigsegv', 0] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': 'sigsegv', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True} + ]) + segment_info['args'].last_pipe_status = [0, 'sigsegv+core', 0] + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True}, + {'contents': 'sigsegv+core', 'highlight_groups': ['exit_fail'], 'draw_inner_divider': True}, + {'contents': '0', 'highlight_groups': ['exit_success'], 'draw_inner_divider': True} + ]) + segment_info['args'].last_pipe_status = [] + segment_info['args'].last_exit_code = 5 + self.assertEqual(shell.last_pipe_status(pl=pl, segment_info=segment_info), [ + {'contents': '5', 'highlight_groups': ['exit_fail'], '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_groups': ['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_groups': ['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_groups': ['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_groups': ['continuation'], + }, + { + 'contents': 'cmdsubst', + 'draw_inner_divider': True, + 'highlight_groups': ['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_groups': ['continuation'], + 'width': 'auto', + 'align': 'r', + }, + { + 'contents': 'cmdsubst', + 'draw_inner_divider': True, + 'highlight_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['continuation'], + }, + { + 'contents': 'then', + 'draw_inner_divider': True, + 'highlight_groups': ['continuation'], + }, + { + 'contents': 'then', + 'draw_inner_divider': True, + 'highlight_groups': ['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(shell.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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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) + + +class TestTmux(TestCase): + def test_attached_clients(self): + def get_tmux_output(pl, 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): + @classmethod + def setUpClass(cls): + module = __import__(str('powerline.segments.common.{0}'.format(cls.module_name))) + cls.module = getattr(module.segments.common, str(cls.module_name)) + + +class TestNet(TestCommon): + module_name = 'net' + + def test_hostname(self): + pl = Pl() + with replace_env('SSH_CLIENT', '192.168.0.12 40921 22') as segment_info: + with replace_module_module(self.module, 'socket', gethostname=lambda: 'abc'): + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info), 'abc') + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True), 'abc') + with replace_module_module(self.module, 'socket', gethostname=lambda: 'abc.mydomain'): + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info), 'abc.mydomain') + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info, exclude_domain=True), 'abc') + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True), 'abc.mydomain') + self.assertEqual(self.module.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(self.module, 'socket', gethostname=lambda: 'abc'): + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info), 'abc') + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True), None) + with replace_module_module(self.module, 'socket', gethostname=lambda: 'abc.mydomain'): + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info), 'abc.mydomain') + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info, exclude_domain=True), 'abc') + self.assertEqual(self.module.hostname(pl=pl, segment_info=segment_info, only_if_ssh=True, exclude_domain=True), None) + + def test_external_ip(self): + pl = Pl() + with replace_attr(self.module, 'urllib_read', urllib_read): + self.assertEqual(self.module.external_ip(pl=pl), [{'contents': '127.0.0.1', 'divider_highlight_group': 'background:divider'}]) + + def test_internal_ip(self): + try: + import netifaces + except ImportError: + raise SkipTest('netifaces module is not available') + 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( + self.module, 'netifaces', + interfaces=(lambda: interfaces), + ifaddresses=(lambda interface: addr[interface]), + AF_INET=netifaces.AF_INET, + AF_INET6=netifaces.AF_INET6, + ): + self.assertEqual(self.module.internal_ip(pl=pl), '192.168.100.200') + self.assertEqual(self.module.internal_ip(pl=pl, interface='auto'), '192.168.100.200') + self.assertEqual(self.module.internal_ip(pl=pl, interface='lo'), '127.0.0.1') + self.assertEqual(self.module.internal_ip(pl=pl, interface='teredo'), None) + self.assertEqual(self.module.internal_ip(pl=pl, ipv=4), '192.168.100.200') + self.assertEqual(self.module.internal_ip(pl=pl, interface='auto', ipv=4), '192.168.100.200') + self.assertEqual(self.module.internal_ip(pl=pl, interface='lo', ipv=4), '127.0.0.1') + self.assertEqual(self.module.internal_ip(pl=pl, interface='teredo', ipv=4), None) + self.assertEqual(self.module.internal_ip(pl=pl, ipv=6), 'feff::5446:5eff:fe5a:7777%enp2s0') + self.assertEqual(self.module.internal_ip(pl=pl, interface='auto', ipv=6), 'feff::5446:5eff:fe5a:7777%enp2s0') + self.assertEqual(self.module.internal_ip(pl=pl, interface='lo', ipv=6), '::1') + self.assertEqual(self.module.internal_ip(pl=pl, interface='teredo', ipv=6), 'feff::5446:5eff:fe5a:7777') + interfaces[1:2] = () + self.assertEqual(self.module.internal_ip(pl=pl, ipv=6), 'feff::5446:5eff:fe5a:7777') + interfaces[1:2] = () + self.assertEqual(self.module.internal_ip(pl=pl, ipv=6), '::1') + interfaces[:] = () + self.assertEqual(self.module.internal_ip(pl=pl, ipv=6), None) + + gateways = { + 'default': { + netifaces.AF_INET: ('192.168.100.1', 'enp2s0'), + netifaces.AF_INET6: ('feff::5446:5eff:fe5a:0001', 'enp2s0') + } + } + + with replace_module_module( + self.module, 'netifaces', + interfaces=(lambda: interfaces), + ifaddresses=(lambda interface: addr[interface]), + gateways=(lambda: gateways), + AF_INET=netifaces.AF_INET, + AF_INET6=netifaces.AF_INET6, + ): + # default gateway has specified address family + self.assertEqual(self.module.internal_ip(pl=pl, interface='default_gateway', ipv=4), '192.168.100.200') + self.assertEqual(self.module.internal_ip(pl=pl, interface='default_gateway', ipv=6), 'feff::5446:5eff:fe5a:7777%enp2s0') + # default gateway doesn't have specified address family + gateways['default'] = {} + self.assertEqual(self.module.internal_ip(pl=pl, interface='default_gateway', ipv=4), None) + self.assertEqual(self.module.internal_ip(pl=pl, interface='default_gateway', ipv=6), None) + +# TODO: fix network load +#def test_network_load(self): +# def gb(interface): +# return None +# +# f = [gb] +# +# def _get_bytes(interface): +# return f[0](interface) +# +# pl = Pl() +# +# with replace_attr(self.module, '_get_bytes', _get_bytes): +# self.module.network_load.startup(pl=pl) +# try: +# self.assertEqual(self.module.network_load(pl=pl, interface='eth0'), None) +# sleep(self.module.network_load.interval) +# self.assertEqual(self.module.network_load(pl=pl, interface='eth0'), None) +# while 'prev' not in self.module.network_load.interfaces.get('eth0', {}): +# sleep(0.1) +# self.assertEqual(self.module.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 self.module.network_load.interfaces.get('eth0', {}).get('prev', (None, None))[1]: +# sleep(0.1) +# self.assertEqual(self.module.network_load(pl=pl, interface='eth0'), [ +# {'divider_highlight_group': 'network_load:divider', 'contents': 'DL 1 KiB/s', 'highlight_groups': ['network_load_recv', 'network_load']}, +# {'divider_highlight_group': 'network_load:divider', 'contents': 'UL 2 KiB/s', 'highlight_groups': ['network_load_sent', 'network_load']}, +# ]) +# self.assertEqual(self.module.network_load(pl=pl, interface='eth0', recv_format='r {value}', sent_format='s {value}'), [ +# {'divider_highlight_group': 'network_load:divider', 'contents': 'r 1 KiB/s', 'highlight_groups': ['network_load_recv', 'network_load']}, +# {'divider_highlight_group': 'network_load:divider', 'contents': 's 2 KiB/s', 'highlight_groups': ['network_load_sent', 'network_load']}, +# ]) +# self.assertEqual(self.module.network_load(pl=pl, recv_format='r {value}', sent_format='s {value}', suffix='bps', interface='eth0'), [ +# {'divider_highlight_group': 'network_load:divider', 'contents': 'r 1 Kibps', 'highlight_groups': ['network_load_recv', 'network_load']}, +# {'divider_highlight_group': 'network_load:divider', 'contents': 's 2 Kibps', 'highlight_groups': ['network_load_sent', 'network_load']}, +# ]) +# self.assertEqual(self.module.network_load(pl=pl, recv_format='r {value}', sent_format='s {value}', si_prefix=True, interface='eth0'), [ +# {'divider_highlight_group': 'network_load:divider', 'contents': 'r 1 kB/s', 'highlight_groups': ['network_load_recv', 'network_load']}, +# {'divider_highlight_group': 'network_load:divider', 'contents': 's 2 kB/s', 'highlight_groups': ['network_load_sent', 'network_load']}, +# ]) +# self.assertEqual(self.module.network_load(pl=pl, recv_format='r {value}', sent_format='s {value}', recv_max=0, interface='eth0'), [ +# {'divider_highlight_group': 'network_load:divider', 'contents': 'r 1 KiB/s', 'highlight_groups': ['network_load_recv_gradient', 'network_load_gradient', 'network_load_recv', 'network_load'], 'gradient_level': 100}, +# {'divider_highlight_group': 'network_load:divider', 'contents': 's 2 KiB/s', 'highlight_groups': ['network_load_sent', 'network_load']}, +# ]) +# +# class ApproxEqual(object): +# def __eq__(self, i): +# return abs(i - 50.0) < 1 +# +# self.assertEqual(self.module.network_load(pl=pl, recv_format='r {value}', sent_format='s {value}', sent_max=4800, interface='eth0'), [ +# {'divider_highlight_group': 'network_load:divider', 'contents': 'r 1 KiB/s', 'highlight_groups': ['network_load_recv', 'network_load']}, +# {'divider_highlight_group': 'network_load:divider', 'contents': 's 2 KiB/s', 'highlight_groups': ['network_load_sent_gradient', 'network_load_gradient', 'network_load_sent', 'network_load'], 'gradient_level': ApproxEqual()}, +# ]) +# finally: +# self.module.network_load.shutdown() + + +class TestEnv(TestCommon): + module_name = 'env' + + 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@DOMAIN.COM' + + if hasattr(self.module, 'psutil') and not callable(self.module.psutil.Process.username): + username = property(username) + + segment_info = {'environ': {}} + + def user(*args, **kwargs): + return self.module.user(pl=pl, segment_info=segment_info, *args, **kwargs) + + struct_passwd = namedtuple('struct_passwd', ('pw_name',)) + new_psutil = new_module('psutil', Process=Process) + new_pwd = new_module('pwd', getpwuid=lambda uid: struct_passwd(pw_name='def@DOMAIN.COM')) + new_getpass = new_module('getpass', getuser=lambda: 'def@DOMAIN.COM') + pl = Pl() + with replace_attr(self.module, 'pwd', new_pwd): + with replace_attr(self.module, 'getpass', new_getpass): + with replace_attr(self.module, 'os', new_os): + with replace_attr(self.module, 'psutil', new_psutil): + with replace_attr(self.module, '_geteuid', lambda: 5): + self.assertEqual(user(), [ + {'contents': 'def@DOMAIN.COM', 'highlight_groups': ['user']} + ]) + self.assertEqual(user(hide_user='abc'), [ + {'contents': 'def@DOMAIN.COM', 'highlight_groups': ['user']} + ]) + self.assertEqual(user(hide_domain=False), [ + {'contents': 'def@DOMAIN.COM', 'highlight_groups': ['user']} + ]) + self.assertEqual(user(hide_user='def@DOMAIN.COM'), None) + self.assertEqual(user(hide_domain=True), [ + {'contents': 'def', 'highlight_groups': ['user']} + ]) + with replace_attr(self.module, '_geteuid', lambda: 0): + self.assertEqual(user(), [ + {'contents': 'def', 'highlight_groups': ['superuser', 'user']} + ]) + + 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(self.module, 'os', new_os): + cwd[0] = '/abc/def/ghi/foo/bar' + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']}, + ]) + segment_info['home'] = '/abc/def/ghi' + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']} + ]) + cwd[0] = '/etc' + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(self.module.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_groups': ['cwd:current_folder', 'cwd']}, + ]) + cwd[0] = '/' + self.assertEqual(self.module.cwd(pl=pl, segment_info=segment_info, use_path_separator=False), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': True, 'highlight_groups': ['cwd:current_folder', 'cwd']}, + ]) + self.assertEqual(self.module.cwd(pl=pl, segment_info=segment_info, use_path_separator=True), [ + {'contents': '/', 'divider_highlight_group': 'cwd:divider', 'draw_inner_divider': False, 'highlight_groups': ['cwd:current_folder', 'cwd']}, + ]) + ose = OSError() + ose.errno = 2 + cwd[0] = ose + self.assertEqual(self.module.cwd(pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2), [ + {'contents': '[not found]', 'divider_highlight_group': 'cwd:divider', 'highlight_groups': ['cwd:current_folder', 'cwd'], 'draw_inner_divider': True} + ]) + cwd[0] = OSError() + self.assertRaises(OSError, self.module.cwd, pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2) + cwd[0] = ValueError() + self.assertRaises(ValueError, self.module.cwd, pl=pl, segment_info=segment_info, dir_limit_depth=2, dir_shorten_len=2) + + def test_virtualenv(self): + pl = Pl() + with replace_env('VIRTUAL_ENV', '/abc/def/ghi') as segment_info: + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info), 'ghi') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_conda=True), 'ghi') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True, ignore_conda=True), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignored_names=["aaa"]), "ghi") + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignored_names=["ghi"]), "def") + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignored_names=["def", "ghi"]), "abc") + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignored_names=["abc", "def", "ghi"]), None) + + segment_info['environ'].pop('VIRTUAL_ENV') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_conda=True), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True, ignore_conda=True), None) + + with replace_env('CONDA_DEFAULT_ENV', 'foo') as segment_info: + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info), 'foo') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignored_names=["foo"]), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_conda=True), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True), 'foo') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True, ignore_conda=True), None) + + segment_info['environ'].pop('CONDA_DEFAULT_ENV') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_conda=True), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True, ignore_conda=True), None) + + with replace_env('CONDA_DEFAULT_ENV', 'foo', environ={'VIRTUAL_ENV': '/sbc/def/ghi'}) as segment_info: + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info), 'ghi') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_conda=True), 'ghi') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True), 'foo') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True, ignore_conda=True), None) + + segment_info['environ'].pop('CONDA_DEFAULT_ENV') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info), 'ghi') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_conda=True), 'ghi') + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True), None) + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info, ignore_venv=True, ignore_conda=True), None) + + with replace_env('VIRTUAL_ENV', '/abc/def/venv') as segment_info: + self.assertEqual(self.module.virtualenv(pl=pl, segment_info=segment_info), 'def') + + def test_environment(self): + pl = Pl() + variable = 'FOO' + value = 'bar' + with replace_env(variable, value) as segment_info: + self.assertEqual(self.module.environment(pl=pl, segment_info=segment_info, variable=variable), value) + segment_info['environ'].pop(variable) + self.assertEqual(self.module.environment(pl=pl, segment_info=segment_info, variable=variable), None) + + +class TestVcs(TestCommon): + module_name = 'vcs' + + def test_branch(self): + pl = Pl() + create_watcher = get_fallback_create_watcher() + segment_info = {'getcwd': os.getcwd} + branch = partial(self.module.branch, pl=pl, create_watcher=create_watcher) + with replace_attr(self.module, 'guess', get_dummy_guess(status=lambda: None, directory='/tmp/tests')): + with replace_attr(self.module, 'tree_status', lambda repo, pl: None): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [{ + 'highlight_groups': ['branch'], + 'contents': 'tests', + 'divider_highlight_group': None + }]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [{ + 'contents': 'tests', + 'highlight_groups': ['branch_clean', 'branch'], + 'divider_highlight_group': None + }]) + with replace_attr(self.module, 'guess', get_dummy_guess(status=lambda: 'D ', directory='/tmp/tests')): + with replace_attr(self.module, 'tree_status', lambda repo, pl: 'D '): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [{ + 'highlight_groups': ['branch'], + 'contents': 'tests', + 'divider_highlight_group': None + }]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [{ + 'contents': 'tests', + 'highlight_groups': ['branch_dirty', 'branch'], + 'divider_highlight_group': None + }]) + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [{ + 'highlight_groups': ['branch'], + 'contents': 'tests', + 'divider_highlight_group': None + }]) + with replace_attr(self.module, 'guess', lambda path, create_watcher: None): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), None) + with replace_attr(self.module, 'guess', get_dummy_guess(status=lambda: 'U')): + with replace_attr(self.module, 'tree_status', lambda repo, pl: 'U'): + self.assertEqual(branch(segment_info=segment_info, status_colors=False, ignore_statuses=['U']), [{ + 'highlight_groups': ['branch'], + 'contents': 'tests', + 'divider_highlight_group': None + }]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True, ignore_statuses=['DU']), [{ + 'highlight_groups': ['branch_dirty', 'branch'], + 'contents': 'tests', + 'divider_highlight_group': None + }]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [{ + 'highlight_groups': ['branch_dirty', 'branch'], + 'contents': 'tests', + 'divider_highlight_group': None + }]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True, ignore_statuses=['U']), [{ + 'highlight_groups': ['branch_clean', 'branch'], + 'contents': 'tests', + 'divider_highlight_group': None + }]) + + def test_stash(self): + pl = Pl() + create_watcher = get_fallback_create_watcher() + stash = partial(self.module.stash, pl=pl, create_watcher=create_watcher, segment_info={'getcwd': os.getcwd}) + + def forge_stash(n): + return replace_attr(self.module, 'guess', get_dummy_guess(stash=lambda: n, directory='/tmp/tests')) + + with forge_stash(0): + self.assertEqual(stash(), None) + with forge_stash(1): + self.assertEqual(stash(), [{ + 'highlight_groups': ['stash'], + 'contents': '1', + 'divider_highlight_group': None + }]) + with forge_stash(2): + self.assertEqual(stash(), [{ + 'highlight_groups': ['stash'], + 'contents': '2', + 'divider_highlight_group': None + }]) + + +class TestTime(TestCommon): + module_name = 'time' + + def test_date(self): + pl = Pl() + with replace_attr(self.module, 'datetime', Args(strptime=lambda timezone, fmt: Args(tzinfo=timezone), now=lambda tz:Args(strftime=lambda fmt: fmt + (tz if tz else '')))): + self.assertEqual(self.module.date(pl=pl), [{'contents': '%Y-%m-%d', 'highlight_groups': ['date'], 'divider_highlight_group': None}]) + self.assertEqual(self.module.date(pl=pl, timezone='+0900'), [{'contents': '%Y-%m-%d+0900', 'highlight_groups': ['date'], 'divider_highlight_group': None}]) + self.assertEqual(self.module.date(pl=pl, format='%H:%M', istime=True), [{'contents': '%H:%M', 'highlight_groups': ['time', 'date'], 'divider_highlight_group': 'time:divider'}]) + self.assertEqual(self.module.date(pl=pl, format='%H:%M', istime=True, timezone='-0900'), [{'contents': '%H:%M-0900', 'highlight_groups': ['time', 'date'], 'divider_highlight_group': 'time:divider'}]) + unicode_date = self.module.date(pl=pl, format='\u231a', istime=True) + expected_unicode_date = [{'contents': '\u231a', 'highlight_groups': ['time', 'date'], 'divider_highlight_group': 'time:divider'}] + if python_implementation() == 'PyPy' and sys.version_info >= (3,): + if unicode_date != expected_unicode_date: + raise SkipTest('Dates do not match, see https://bitbucket.org/pypy/pypy/issues/2161/pypy3-strftime-does-not-accept-unicode') + self.assertEqual(unicode_date, expected_unicode_date) + + def test_fuzzy_time(self): + time = Args(hour=0, minute=45) + pl = Pl() + hour_str = ['12', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven'] + minute_str = {'0': 'o\'clock', '5': 'five past', '10': 'ten past','15': + 'quarter past','20': 'twenty past', '25': 'twenty-five past', + '30': 'half past', '35': 'twenty-five to','40': 'twenty to', '45': + 'quarter to', '50': 'ten to', '55': 'five to'} + special_case_str = { + '(23, 58)': '~ midnight', + '(23, 59)': '~ midnight', + '(0, 0)': 'midnight', + '(0, 1)': '~ midnight', + '(0, 2)': '~ midnight', + '(12, 0)': 'twelve o\'clock'} + with replace_attr(self.module, 'datetime', Args(strptime=lambda timezone, fmt: Args(tzinfo=timezone), now=lambda tz: time)): + self.assertEqual(self.module.fuzzy_time(pl=pl, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'quarter to one') + self.assertEqual(self.module.fuzzy_time(pl=pl), 'quarter to one') + time.hour = 23 + time.minute = 59 + self.assertEqual(self.module.fuzzy_time(pl=pl, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), '~ midnight') + self.assertEqual(self.module.fuzzy_time(pl=pl), 'round about midnight') + time.hour = 11 + time.minute = 33 + self.assertEqual(self.module.fuzzy_time(pl=pl, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str),'twenty-five to 12') + self.assertEqual(self.module.fuzzy_time(pl=pl), 'twenty-five to twelve') + time.hour = 12 + time.minute = 0 + self.assertEqual(self.module.fuzzy_time(pl=pl, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'twelve o\'clock') + self.assertEqual(self.module.fuzzy_time(pl=pl), 'noon') + time.hour = 11 + time.minute = 33 + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=False, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'twenty-five to 12') + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=False), 'twenty-five to twelve') + time.hour = 12 + time.minute = 0 + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=False, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'twelve o\'clock') + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=False), 'noon') + time.hour = 11 + time.minute = 33 + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=True, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'twenty‐five to 12') + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=True), 'twenty‐five to twelve') + time.hour = 12 + time.minute = 0 + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=True, hour_str=hour_str, minute_str=minute_str, special_case_str=special_case_str), 'twelve o’clock') + self.assertEqual(self.module.fuzzy_time(pl=pl, unicode_text=True),'noon') + + +class TestSys(TestCommon): + module_name = 'sys' + + def test_uptime(self): + pl = Pl() + with replace_attr(self.module, '_get_uptime', lambda: 259200): + self.assertEqual(self.module.uptime(pl=pl), [{'contents': '3d 0h 00m', 'divider_highlight_group': 'background:divider'}]) + with replace_attr(self.module, '_get_uptime', lambda: 93784): + self.assertEqual(self.module.uptime(pl=pl), [{'contents': '1d 2h 03m', 'divider_highlight_group': 'background:divider'}]) + self.assertEqual(self.module.uptime(pl=pl, shorten_len=4), [{'contents': '1d 2h 03m 04s', 'divider_highlight_group': 'background:divider'}]) + with replace_attr(self.module, '_get_uptime', lambda: 65536): + self.assertEqual(self.module.uptime(pl=pl), [{'contents': '18h 12m 16s', 'divider_highlight_group': 'background:divider'}]) + self.assertEqual(self.module.uptime(pl=pl, shorten_len=2), [{'contents': '18h 12m', 'divider_highlight_group': 'background:divider'}]) + self.assertEqual(self.module.uptime(pl=pl, shorten_len=1), [{'contents': '18h', 'divider_highlight_group': 'background:divider'}]) + + def _get_uptime(): + raise NotImplementedError + + with replace_attr(self.module, '_get_uptime', _get_uptime): + self.assertEqual(self.module.uptime(pl=pl), None) + + def test_system_load(self): + pl = Pl() + with replace_module_module(self.module, 'os', getloadavg=lambda: (7.5, 3.5, 1.5)): + with replace_attr(self.module, '_cpu_count', lambda: 2): + self.assertEqual(self.module.system_load(pl=pl), [ + {'contents': '7.5 ', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + {'contents': '3.5 ', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 75.0}, + {'contents': '1.5', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 0} + ]) + self.assertEqual(self.module.system_load(pl=pl, format='{avg:.0f}', threshold_good=0, threshold_bad=1), [ + {'contents': '8 ', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + {'contents': '4 ', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + {'contents': '2', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 75.0} + ]) + self.assertEqual(self.module.system_load(pl=pl, short=True), [ + {'contents': '7.5', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + ]) + self.assertEqual(self.module.system_load(pl=pl, format='{avg:.0f}', threshold_good=0, threshold_bad=1, short=True), [ + {'contents': '8', 'highlight_groups': ['system_load_gradient', 'system_load'], 'divider_highlight_group': 'background:divider', 'gradient_level': 100}, + ]) + + def test_cpu_load_percent(self): + try: + __import__('psutil') + except ImportError as e: + raise SkipTest('Failed to import psutil: {0}'.format(e)) + pl = Pl() + with replace_module_module(self.module, 'psutil', cpu_percent=lambda **kwargs: 52.3): + self.assertEqual(self.module.cpu_load_percent(pl=pl), [{ + 'contents': '52%', + 'gradient_level': 52.3, + 'highlight_groups': ['cpu_load_percent_gradient', 'cpu_load_percent'], + }]) + self.assertEqual(self.module.cpu_load_percent(pl=pl, format='{0:.1f}%'), [{ + 'contents': '52.3%', + 'gradient_level': 52.3, + 'highlight_groups': ['cpu_load_percent_gradient', 'cpu_load_percent'], + }]) + + +class TestWthr(TestCommon): + module_name = 'wthr' + + def test_weather(self): + pl = Pl() + with replace_attr(self.module, 'urllib_read', urllib_read): + self.assertEqual(self.module.weather(pl=pl), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'SUN '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '20°C', 'gradient_level': 71.42857142857143} + ]) + self.assertEqual(self.module.weather(pl=pl, temp_coldest=0, temp_hottest=100), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'SUN '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '20°C', 'gradient_level': 20} + ]) + self.assertEqual(self.module.weather(pl=pl, temp_coldest=-100, temp_hottest=-50), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'SUN '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '20°C', 'gradient_level': 100} + ]) + self.assertEqual(self.module.weather(pl=pl, icons={'sunny': 'o'}), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'o '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '20°C', 'gradient_level': 71.42857142857143} + ]) + # Test is disabled as no request has more than 1 weather condition associated currently + # self.assertEqual(self.module.weather(pl=pl, icons={'windy': 'x'}), [ + # {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_blustery', 'weather_condition_windy', 'weather_conditions', 'weather'], 'contents': 'x '}, + # {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '14°C', 'gradient_level': 62.857142857142854} + # ]) + self.assertEqual(self.module.weather(pl=pl, unit='F'), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'SUN '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '68°F', 'gradient_level': 100} + ]) + self.assertEqual(self.module.weather(pl=pl, unit='K'), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'SUN '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '293K', 'gradient_level': 100} + ]) + self.assertEqual(self.module.weather(pl=pl, temp_format='{temp:.1e}C'), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'SUN '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '2.0e+01C', 'gradient_level': 71.42857142857143} + ]) + with replace_attr(self.module, 'urllib_read', urllib_read): + self.module.weather.startup(pl=pl, location_query='Meppen,06,DE') + self.assertEqual(self.module.weather(pl=pl), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'SUN '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '20°C', 'gradient_level': 71.42857142857143} + ]) + self.assertEqual(self.module.weather(pl=pl, location_query='Moscow,RU'), [ + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_condition_sunny', 'weather_conditions', 'weather'], 'contents': 'SUN '}, + {'divider_highlight_group': 'background:divider', 'highlight_groups': ['weather_temp_gradient', 'weather_temp', 'weather'], 'contents': '10°C', 'gradient_level': 57.142857142857146} + ]) + self.module.weather.shutdown() + + +class TestI3WM(TestCase): + def test_workspaces(self): + class Conn(object): + def get_tree(self): + return self + + def descendents(self): + nodes_unfocused = [Args(focused = False)] + nodes_focused = [Args(focused = True)] + + workspace_scratch = lambda: Args(name='__i3_scratch') + workspace_noscratch = lambda: Args(name='2: w2') + return [ + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_unfocused), + Args(scratchpad_state='changed', urgent=True, workspace=workspace_noscratch, nodes=nodes_focused), + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_unfocused), + Args(scratchpad_state=None, urgent=False, workspace=workspace_noscratch, nodes=nodes_unfocused), + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_focused), + Args(scratchpad_state=None, urgent=True, workspace=workspace_noscratch, nodes=nodes_unfocused), + ] + + def workspaces(self): + return iter([ + Args(name='1: w1', output='LVDS1', focused=False, urgent=False, visible=False, num=1, leaves=lambda: []), + Args(name='2: w2', output='LVDS1', focused=False, urgent=False, visible=True, num=2, leaves=lambda: []), + Args(name='3: w3', output='HDMI1', focused=False, urgent=True, visible=True, num=3, leaves=lambda: []), + Args(name='4: w4', output='DVI01', focused=True, urgent=True, visible=True, num=None, leaves=lambda: []) + ]) + + def get_workspaces(self): + return iter([ + Args(name='1: w1', output='LVDS1', focused=False, urgent=False, visible=False, num=1, leaves=lambda: []), + Args(name='2: w2', output='LVDS1', focused=False, urgent=False, visible=True, num=2, leaves=lambda: []), + Args(name='3: w3', output='HDMI1', focused=False, urgent=True, visible=True, num=3, leaves=lambda: []), + Args(name='4: w4', output='DVI01', focused=True, urgent=True, visible=True, num=None, leaves=lambda: []) + ]) + + def get_outputs(self): + return iter([ + Args(name='LVDS1', active=True), + Args(name='HDMI1', active=True), + Args(name='DVI01', active=True), + Args(name='HDMI2', active=False), + ]) + + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Conn()): + segment_info = {} + + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info), [ + {'contents': '1: w1', 'highlight_groups': ['workspace']}, + {'contents': '2: w2', 'highlight_groups': ['workspace:visible', 'w_visible', 'workspace']}, + {'contents': '3: w3', 'highlight_groups': ['workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + {'contents': '4: w4', 'highlight_groups': ['workspace:focused', 'w_focused', 'workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=None), [ + {'contents': '1: w1', 'highlight_groups': ['workspace']}, + {'contents': '2: w2', 'highlight_groups': ['workspace:visible', 'w_visible', 'workspace']}, + {'contents': '3: w3', 'highlight_groups': ['workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + {'contents': '4: w4', 'highlight_groups': ['workspace:focused', 'w_focused', 'workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['focused', 'urgent']), [ + {'contents': '3: w3', 'highlight_groups': ['workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + {'contents': '4: w4', 'highlight_groups': ['workspace:focused', 'w_focused', 'workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible']), [ + {'contents': '2: w2', 'highlight_groups': ['workspace:visible', 'w_visible', 'workspace']}, + {'contents': '3: w3', 'highlight_groups': ['workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + {'contents': '4: w4', 'highlight_groups': ['workspace:focused', 'w_focused', 'workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], strip=3), [ + {'contents': 'w2', 'highlight_groups': ['workspace:visible', 'w_visible', 'workspace']}, + {'contents': 'w3', 'highlight_groups': ['workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + {'contents': 'w4', 'highlight_groups': ['workspace:focused', 'w_focused', 'workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['focused', 'urgent'], output='DVI01'), [ + {'contents': '4: w4', 'highlight_groups': ['workspace:focused', 'w_focused', 'workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], output='HDMI1'), [ + {'contents': '3: w3', 'highlight_groups': ['workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], strip=3, output='LVDS1'), [ + {'contents': 'w2', 'highlight_groups': ['workspace:visible', 'w_visible', 'workspace']}, + ]) + segment_info['output'] = 'LVDS1' + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], output='HDMI1'), [ + {'contents': '3: w3', 'highlight_groups': ['workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspaces(pl=pl, segment_info=segment_info, only_show=['visible'], strip=3), [ + {'contents': 'w2', 'highlight_groups': ['workspace:visible', 'w_visible', 'workspace']}, + ]) + + def test_workspace(self): + class Conn(object): + def get_tree(self): + return self + + def descendents(self): + nodes_unfocused = [Args(focused = False)] + nodes_focused = [Args(focused = True)] + + workspace_scratch = lambda: Args(name='__i3_scratch') + workspace_noscratch = lambda: Args(name='2: w2') + return [ + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_unfocused), + Args(scratchpad_state='changed', urgent=True, workspace=workspace_noscratch, nodes=nodes_focused), + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_unfocused), + Args(scratchpad_state=None, urgent=False, workspace=workspace_noscratch, nodes=nodes_unfocused), + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_focused), + Args(scratchpad_state=None, urgent=True, workspace=workspace_noscratch, nodes=nodes_unfocused), + ] + + def workspaces(self): + return iter([ + Args(name='1: w1', output='LVDS1', focused=False, urgent=False, visible=False, num=1, leaves=lambda: []), + Args(name='2: w2', output='LVDS1', focused=False, urgent=False, visible=True, num=2, leaves=lambda: []), + Args(name='3: w3', output='HDMI1', focused=False, urgent=True, visible=True, num=3, leaves=lambda: []), + Args(name='4: w4', output='DVI01', focused=True, urgent=True, visible=True, num=None, leaves=lambda: []) + ]) + + def get_workspaces(self): + return iter([ + Args(name='1: w1', output='LVDS1', focused=False, urgent=False, visible=False, num=1, leaves=lambda: []), + Args(name='2: w2', output='LVDS1', focused=False, urgent=False, visible=True, num=2, leaves=lambda: []), + Args(name='3: w3', output='HDMI1', focused=False, urgent=True, visible=True, num=3, leaves=lambda: []), + Args(name='4: w4', output='DVI01', focused=True, urgent=True, visible=True, num=None, leaves=lambda: []) + ]) + + def get_outputs(self): + return iter([ + Args(name='LVDS1', active=True), + Args(name='HDMI1', active=True), + Args(name='DVI01', active=True), + Args(name='HDMI2', active=False), + ]) + + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Conn()): + segment_info = {} + + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='1: w1'), [ + {'contents': '1: w1', 'highlight_groups': ['workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='3: w3', strip=True), [ + {'contents': 'w3', 'highlight_groups': ['workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='9: w9'), None) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info), [ + {'contents': '4: w4', 'highlight_groups': ['workspace:focused', 'w_focused', 'workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + segment_info['workspace'] = next(Conn().get_workspaces()) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, workspace='4: w4'), [ + {'contents': '4: w4', 'highlight_groups': ['workspace:focused', 'w_focused', 'workspace:urgent', 'w_urgent', 'workspace:visible', 'w_visible', 'workspace']}, + ]) + self.assertEqual(i3wm.workspace(pl=pl, segment_info=segment_info, strip=True), [ + {'contents': 'w1', 'highlight_groups': ['workspace']}, + ]) + + def test_mode(self): + pl = Pl() + self.assertEqual(i3wm.mode(pl=pl, segment_info={'mode': 'default'}), None) + self.assertEqual(i3wm.mode(pl=pl, segment_info={'mode': 'test'}), 'test') + self.assertEqual(i3wm.mode(pl=pl, segment_info={'mode': 'default'}, names={'default': 'test'}), 'test') + self.assertEqual(i3wm.mode(pl=pl, segment_info={'mode': 'test'}, names={'default': 'test', 'test': 't'}), 't') + + def test_scratchpad(self): + class Conn(object): + def get_tree(self): + return self + + def descendants(self): + nodes_unfocused = [Args(focused = False)] + nodes_focused = [Args(focused = True)] + + workspace_scratch = lambda: Args(name='__i3_scratch') + workspace_noscratch = lambda: Args(name='2: www') + return [ + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_unfocused), + Args(scratchpad_state='changed', urgent=True, workspace=workspace_noscratch, nodes=nodes_focused), + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_unfocused), + Args(scratchpad_state=None, urgent=False, workspace=workspace_noscratch, nodes=nodes_unfocused), + Args(scratchpad_state='fresh', urgent=False, workspace=workspace_scratch, nodes=nodes_focused), + Args(scratchpad_state=None, urgent=True, workspace=workspace_noscratch, nodes=nodes_unfocused), + ] + + pl = Pl() + with replace_attr(i3wm, 'get_i3_connection', lambda: Conn()): + self.assertEqual(i3wm.scratchpad(pl=pl), [ + {'contents': 'O', 'highlight_groups': ['scratchpad']}, + {'contents': 'X', 'highlight_groups': ['scratchpad:urgent', 'scratchpad:focused', 'scratchpad:visible', 'scratchpad']}, + {'contents': 'O', 'highlight_groups': ['scratchpad']}, + {'contents': 'X', 'highlight_groups': ['scratchpad:visible', 'scratchpad']}, + {'contents': 'O', 'highlight_groups': ['scratchpad:focused', 'scratchpad']}, + {'contents': 'X', 'highlight_groups': ['scratchpad:urgent', 'scratchpad:visible', 'scratchpad']}, + ]) + self.assertEqual(i3wm.scratchpad(pl=pl, icons={'changed': '-', 'fresh': 'o'}), [ + {'contents': 'o', 'highlight_groups': ['scratchpad']}, + {'contents': '-', 'highlight_groups': ['scratchpad:urgent', 'scratchpad:focused', 'scratchpad:visible', 'scratchpad']}, + {'contents': 'o', 'highlight_groups': ['scratchpad']}, + {'contents': '-', 'highlight_groups': ['scratchpad:visible', 'scratchpad']}, + {'contents': 'o', 'highlight_groups': ['scratchpad:focused', 'scratchpad']}, + {'contents': '-', 'highlight_groups': ['scratchpad:urgent', 'scratchpad:visible', 'scratchpad']}, + ]) + + +class TestMail(TestCommon): + module_name = 'mail' + + def test_email_imap_alert(self): + # TODO + pass + + +class TestPlayers(TestCommon): + module_name = 'players' + + def test_now_playing(self): + # TODO + pass + + +class TestBat(TestCommon): + module_name = 'bat' + + def test_battery(self): + pl = Pl() + + def _get_battery_status(pl): + return 86, False + + with replace_attr(self.module, '_get_battery_status', _get_battery_status): + self.assertEqual(self.module.battery(pl=pl), [{ + 'contents': ' 86%', + 'highlight_groups': ['battery_gradient', 'battery'], + 'gradient_level': 14, + }]) + self.assertEqual(self.module.battery(pl=pl, format='{capacity:.2f}'), [{ + 'contents': '0.86', + 'highlight_groups': ['battery_gradient', 'battery'], + 'gradient_level': 14, + }]) + self.assertEqual(self.module.battery(pl=pl, steps=7), [{ + 'contents': ' 86%', + 'highlight_groups': ['battery_gradient', 'battery'], + 'gradient_level': 14, + }]) + self.assertEqual(self.module.battery(pl=pl, gamify=True), [ + { + 'contents': ' ', + 'draw_inner_divider': False, + 'highlight_groups': ['battery_offline', 'battery_ac_state', 'battery_gradient', 'battery'], + 'gradient_level': 0 + }, + { + 'contents': 'OOOO', + 'draw_inner_divider': False, + 'highlight_groups': ['battery_full', 'battery_gradient', 'battery'], + 'gradient_level': 0 + }, + { + 'contents': 'O', + 'draw_inner_divider': False, + 'highlight_groups': ['battery_empty', 'battery_gradient', 'battery'], + 'gradient_level': 100 + } + ]) + self.assertEqual(self.module.battery(pl=pl, gamify=True, full_heart='+', empty_heart='-', steps='10'), [ + { + 'contents': ' ', + 'draw_inner_divider': False, + 'highlight_groups': ['battery_offline', 'battery_ac_state', 'battery_gradient', 'battery'], + 'gradient_level': 0 + }, + { + 'contents': '++++++++', + 'draw_inner_divider': False, + 'highlight_groups': ['battery_full', 'battery_gradient', 'battery'], + 'gradient_level': 0 + }, + { + 'contents': '--', + 'draw_inner_divider': False, + 'highlight_groups': ['battery_empty', 'battery_gradient', 'battery'], + 'gradient_level': 100 + } + ]) + + def test_battery_with_ac_online(self): + pl = Pl() + + def _get_battery_status(pl): + return 86, True + + with replace_attr(self.module, '_get_battery_status', _get_battery_status): + self.assertEqual(self.module.battery(pl=pl, online='C', offline=' '), [ + { + 'contents': 'C 86%', + 'highlight_groups': ['battery_gradient', 'battery'], + 'gradient_level': 14, + }]) + + def test_battery_with_ac_offline(self): + pl = Pl() + + def _get_battery_status(pl): + return 86, False + + with replace_attr(self.module, '_get_battery_status', _get_battery_status): + self.assertEqual(self.module.battery(pl=pl, online='C', offline=' '), [ + { + 'contents': ' 86%', + 'highlight_groups': ['battery_gradient', 'battery'], + 'gradient_level': 14, + }]) + + +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', 'i\0') 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_groups': ['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_groups': ['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(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_groups': ['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_groups': ['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_groups': ['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_groups': ['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_groups': ['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.vcs, 'guess', get_dummy_guess(status=lambda: None)): + with replace_attr(self.vcs, 'tree_status', lambda repo, pl: None): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [ + {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch'], 'contents': 'foo'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [ + {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch_clean', 'branch'], 'contents': 'foo'} + ]) + with replace_attr(self.vcs, 'guess', get_dummy_guess(status=lambda: 'DU')): + with replace_attr(self.vcs, 'tree_status', lambda repo, pl: 'DU'): + self.assertEqual(branch(segment_info=segment_info, status_colors=False), [ + {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch'], 'contents': 'foo'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [ + {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch_dirty', 'branch'], 'contents': 'foo'} + ]) + with replace_attr(self.vcs, 'guess', get_dummy_guess(status=lambda: 'U')): + with replace_attr(self.vcs, 'tree_status', lambda repo, pl: 'U'): + self.assertEqual(branch(segment_info=segment_info, status_colors=False, ignore_statuses=['U']), [ + {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch'], 'contents': 'foo'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True, ignore_statuses=['DU']), [ + {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch_dirty', 'branch'], 'contents': 'foo'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True), [ + {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch_dirty', 'branch'], 'contents': 'foo'} + ]) + self.assertEqual(branch(segment_info=segment_info, status_colors=True, ignore_statuses=['U']), [ + {'divider_highlight_group': 'branch:divider', 'highlight_groups': ['branch_clean', 'branch'], 'contents': 'foo'} + ]) + + def test_stash(self): + pl = Pl() + create_watcher = get_fallback_create_watcher() + with vim_module._with('buffer', '/foo') as segment_info: + stash = partial(self.vim.stash, pl=pl, create_watcher=create_watcher, segment_info=segment_info) + + def forge_stash(n): + return replace_attr(self.vcs, 'guess', get_dummy_guess(stash=lambda: n)) + + with forge_stash(0): + self.assertEqual(stash(), None) + with forge_stash(1): + self.assertEqual(stash(), [{ + 'divider_highlight_group': 'stash:divider', + 'highlight_groups': ['stash'], + 'contents': '1' + }]) + with forge_stash(2): + self.assertEqual(stash(), [{ + 'divider_highlight_group': 'stash:divider', + 'highlight_groups': ['stash'], + 'contents': '2' + }]) + + 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_groups': ['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_groups': ['trailing_whitespace', 'warning'], + 'contents': '1', + }]) + self.assertEqual(trailing_whitespace(), [{ + 'highlight_groups': ['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_tab(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + self.assertEqual(self.vim.tab(pl=pl, segment_info=segment_info), [{ + 'contents': None, + 'literal_contents': (0, '%1T'), + }]) + self.assertEqual(self.vim.tab(pl=pl, segment_info=segment_info, end=True), [{ + 'contents': None, + 'literal_contents': (0, '%T'), + }]) + + 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_groups': ['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_groups': ['tab_modified_indicator', 'modified_indicator'], + }]) + + def test_csv_col_current(self): + pl = Pl() + segment_info = vim_module._get_segment_info() + + def csv_col_current(**kwargs): + self.vim.csv_cache and self.vim.csv_cache.clear() + return self.vim.csv_col_current(pl=pl, segment_info=segment_info, **kwargs) + + buffer = segment_info['buffer'] + try: + self.assertEqual(csv_col_current(), None) + buffer.options['filetype'] = 'csv' + self.assertEqual(csv_col_current(), None) + buffer[:] = ['1;2;3', '4;5;6'] + vim_module._set_cursor(1, 1) + self.assertEqual(csv_col_current(), [{ + 'contents': '1', 'highlight_groups': ['csv:column_number', 'csv'] + }]) + vim_module._set_cursor(2, 3) + self.assertEqual(csv_col_current(), [{ + 'contents': '2', 'highlight_groups': ['csv:column_number', 'csv'] + }]) + vim_module._set_cursor(2, 3) + self.assertEqual(csv_col_current(display_name=True), [{ + 'contents': '2', 'highlight_groups': ['csv:column_number', 'csv'] + }, { + 'contents': ' (2)', 'highlight_groups': ['csv:column_name', 'csv'] + }]) + buffer[:0] = ['Foo;Bar;Baz'] + vim_module._set_cursor(2, 3) + self.assertEqual(csv_col_current(), [{ + 'contents': '2', 'highlight_groups': ['csv:column_number', 'csv'] + }, { + 'contents': ' (Bar)', 'highlight_groups': ['csv:column_name', 'csv'] + }]) + if sys.version_info < (2, 7): + raise SkipTest('csv module in Python-2.6 does not handle multiline csv files well') + buffer[len(buffer):] = ['1;"bc', 'def', 'ghi', 'jkl";3'] + vim_module._set_cursor(5, 1) + self.assertEqual(csv_col_current(), [{ + 'contents': '2', 'highlight_groups': ['csv:column_number', 'csv'] + }, { + 'contents': ' (Bar)', 'highlight_groups': ['csv:column_name', 'csv'] + }]) + vim_module._set_cursor(7, 6) + self.assertEqual(csv_col_current(), [{ + 'contents': '3', 'highlight_groups': ['csv:column_number', 'csv'] + }, { + 'contents': ' (Baz)', 'highlight_groups': ['csv:column_name', 'csv'] + }]) + self.assertEqual(csv_col_current(name_format=' ({column_name:.1})'), [{ + 'contents': '3', 'highlight_groups': ['csv:column_number', 'csv'] + }, { + 'contents': ' (B)', 'highlight_groups': ['csv:column_name', 'csv'] + }]) + self.assertEqual(csv_col_current(display_name=True, name_format=' ({column_name:.1})'), [{ + 'contents': '3', 'highlight_groups': ['csv:column_number', 'csv'] + }, { + 'contents': ' (B)', 'highlight_groups': ['csv:column_name', 'csv'] + }]) + self.assertEqual(csv_col_current(display_name=False, name_format=' ({column_name:.1})'), [{ + 'contents': '3', 'highlight_groups': ['csv:column_number', 'csv'] + }]) + self.assertEqual(csv_col_current(display_name=False), [{ + 'contents': '3', 'highlight_groups': ['csv:column_number', 'csv'] + }]) + finally: + vim_module._bw(segment_info['bufnr']) + + @classmethod + def setUpClass(cls): + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'vim_sys_path'))) + from powerline.segments import vim + cls.vim = vim + from powerline.segments.common import vcs + cls.vcs = vcs + + @classmethod + def tearDownClass(cls): + sys.path.pop(0) + + +class TestPDB(TestCase): + def test_current_line(self): + pl = Pl() + self.assertEqual(pdb.current_line(pl=pl, segment_info={'curframe': Args(f_lineno=10)}), '10') + + def test_current_file(self): + pl = Pl() + cf = lambda **kwargs: pdb.current_file( + pl=pl, + segment_info={'curframe': Args(f_code=Args(co_filename='/tmp/abc.py'))}, + **kwargs + ) + self.assertEqual(cf(), 'abc.py') + self.assertEqual(cf(basename=True), 'abc.py') + self.assertEqual(cf(basename=False), '/tmp/abc.py') + + def test_current_code_name(self): + pl = Pl() + ccn = lambda **kwargs: pdb.current_code_name( + pl=pl, + segment_info={'curframe': Args(f_code=Args(co_name=''))}, + **kwargs + ) + self.assertEqual(ccn(), '') + + def test_current_context(self): + pl = Pl() + cc = lambda **kwargs: pdb.current_context( + pl=pl, + segment_info={'curframe': Args(f_code=Args(co_name='', co_filename='/tmp/abc.py'))}, + **kwargs + ) + self.assertEqual(cc(), 'abc.py') + + def test_stack_depth(self): + pl = Pl() + sd = lambda **kwargs: pdb.stack_depth( + pl=pl, + segment_info={'pdb': Args(stack=[1, 2, 3]), 'initial_stack_length': 1}, + **kwargs + ) + self.assertEqual(sd(), '2') + self.assertEqual(sd(full_stack=False), '2') + self.assertEqual(sd(full_stack=True), '3') + + +old_cwd = None + + +def setUpModule(): + global old_cwd + global __file__ + old_cwd = os.getcwd() + __file__ = os.path.abspath(__file__) + os.chdir(os.path.dirname(os.path.dirname(__file__))) + + +def tearDownModule(): + global old_cwd + os.chdir(old_cwd) + + +if __name__ == '__main__': + from tests.modules import main + main() diff --git a/tests/test_python/test_selectors.py b/tests/test_python/test_selectors.py new file mode 100644 index 0000000..74ace8d --- /dev/null +++ b/tests/test_python/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.modules.vim as vim_module + +from tests.modules.lib import Pl +from tests.modules 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(os.path.dirname(__file__)), 'vim_sys_path'))) + from powerline.selectors import vim + cls.vim = vim + + @classmethod + def tearDownClass(cls): + sys.path.pop(0) + + +if __name__ == '__main__': + from tests.modules import main + main() diff --git a/tests/test_python/test_watcher.py b/tests/test_python/test_watcher.py new file mode 100644 index 0000000..a246d0b --- /dev/null +++ b/tests/test_python/test_watcher.py @@ -0,0 +1,245 @@ +# 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 errno import ENOENT + +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.modules import TestCase, SkipTest + + +INOTIFY_DIR = 'inotify' + os.path.basename(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)) + + +def set_watcher_tests(l): + byte_tests = (('bytes', True), ('unicode', False)) + + for btn, use_bytes in byte_tests: + def test_inotify_file_watcher(self, use_bytes=use_bytes): + 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') + self.do_test_file_watcher(w, use_bytes) + + def test_uv_file_watcher(self, use_bytes=use_bytes): + 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') + self.do_test_file_watcher(w, use_bytes) + + def test_inotify_tree_watcher(self, use_bytes=use_bytes): + try: + tw = create_tree_watcher(get_fallback_logger(), watcher_type='inotify') + except INotifyError: + raise SkipTest('INotify is not available') + self.do_test_tree_watcher(tw, use_bytes) + + def test_uv_tree_watcher(self, use_bytes=use_bytes): + raise SkipTest('Uv watcher tests are not stable') + try: + tw = create_tree_watcher(get_fallback_logger(), 'uv') + except UvNotFound: + raise SkipTest('Pyuv is not available') + self.do_test_tree_watcher(tw, use_bytes) + + def test_inotify_file_watcher_is_watching(self, use_bytes=use_bytes): + try: + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='inotify') + except INotifyError: + raise SkipTest('INotify is not available') + self.do_test_file_watcher_is_watching(w, use_bytes) + + def test_stat_file_watcher_is_watching(self, use_bytes=use_bytes): + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='stat') + self.do_test_file_watcher_is_watching(w, use_bytes) + + def test_uv_file_watcher_is_watching(self, use_bytes=use_bytes): + try: + w = create_file_watcher(pl=get_fallback_logger(), watcher_type='uv') + except UvNotFound: + raise SkipTest('Pyuv is not available') + self.do_test_file_watcher_is_watching(w, use_bytes) + + for wt in ('uv', 'inotify'): + l['test_{0}_file_watcher_{1}'.format(wt, btn)] = locals()['test_{0}_file_watcher'.format(wt)] + l['test_{0}_tree_watcher_{1}'.format(wt, btn)] = locals()['test_{0}_tree_watcher'.format(wt)] + l['test_{0}_file_watcher_is_watching_{1}'.format(wt, btn)] = ( + locals()['test_{0}_file_watcher_is_watching'.format(wt)]) + l['test_{0}_file_watcher_is_watching_{1}'.format('stat', btn)] = ( + locals()['test_{0}_file_watcher_is_watching'.format('stat')]) + + +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 do_test_file_watcher(self, w, use_bytes=False): + try: + f1, f2, f3 = map(lambda x: os.path.join(INOTIFY_DIR, 'file%d' % x), (1, 2, 3)) + ne = os.path.join(INOTIFY_DIR, 'notexists') + if use_bytes: + f1 = f1.encode('utf-8') + f2 = f2.encode('utf-8') + f3 = f3.encode('utf-8') + ne = ne.encode('utf-8') + with open(f1, 'wb'): + with open(f2, 'wb'): + with open(f3, 'wb'): + pass + 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 do_test_tree_watcher(self, tw, use_bytes=False): + try: + inotify_dir = INOTIFY_DIR + subdir = os.path.join(inotify_dir, 'subdir') + t1 = os.path.join(inotify_dir, 'tree1') + ts1 = os.path.join(subdir, 'tree1') + suffix = '1' + f = os.path.join(subdir, 'f') + if use_bytes: + inotify_dir = inotify_dir.encode('utf-8') + subdir = subdir.encode('utf-8') + t1 = t1.encode('utf-8') + ts1 = ts1.encode('utf-8') + suffix = suffix.encode('utf-8') + f = f.encode('utf-8') + 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') + except INotifyError: + raise SkipTest('INotify is not available') + self.assertTrue(tw(inotify_dir)) + self.assertFalse(tw(inotify_dir)) + changed = partial(self.do_test_for_change, tw, inotify_dir) + open(t1, 'w').close() + changed() + open(ts1, 'w').close() + changed() + os.unlink(ts1) + changed() + os.rmdir(subdir) + changed() + os.mkdir(subdir) + changed() + os.rename(subdir, subdir + suffix) + changed() + shutil.rmtree(subdir + suffix) + changed() + os.mkdir(subdir) + open(f, 'w').close() + changed() + with open(f, 'a') as s: + s.write(' ') + changed() + os.rename(f, f + suffix) + changed() + finally: + clear_dir(inotify_dir) + + def do_test_file_watcher_is_watching(self, w, use_bytes=False): + try: + f1, f2, f3 = map(lambda x: os.path.join(INOTIFY_DIR, 'file%d' % x), (1, 2, 3)) + ne = os.path.join(INOTIFY_DIR, 'notexists') + if use_bytes: + f1 = f1.encode('utf-8') + f2 = f2.encode('utf-8') + f3 = f3.encode('utf-8') + ne = ne.encode('utf-8') + with open(f1, 'wb'): + with open(f2, 'wb'): + with open(f3, 'wb'): + pass + self.assertRaises(OSError, w, ne) + try: + w(ne) + except OSError as e: + self.assertEqual(e.errno, ENOENT) + 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) + + set_watcher_tests(locals()) + + +old_cwd = None + + +def setUpModule(): + global old_cwd + old_cwd = os.getcwd() + os.chdir(os.path.dirname(os.path.dirname(__file__))) + os.mkdir(INOTIFY_DIR) + + +def tearDownModule(): + shutil.rmtree(INOTIFY_DIR) + os.chdir(old_cwd) + + +if __name__ == '__main__': + from tests.modules import main + main() diff --git a/tests/test_shells/bgscript.sh b/tests/test_shells/bgscript.sh new file mode 100755 index 0000000..71886e6 --- /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/inputs/bash b/tests/test_shells/inputs/bash new file mode 100644 index 0000000..b0d1478 --- /dev/null +++ b/tests/test_shells/inputs/bash @@ -0,0 +1,73 @@ +set_theme_option() { + export POWERLINE_THEME_OVERRIDES="${POWERLINE_THEME_OVERRIDES};$1=$2" +} +set_theme() { + export POWERLINE_CONFIG_OVERRIDES="ext.shell.theme=$1" +} +set_theme_option default.segment_data.hostname.args.only_if_ssh false +set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false +ABOVE_LEFT='[{ + "left": [ + { + "function": "powerline.segments.common.env.environment", + "args": {"variable": "DISPLAYED_ENV_VAR"} + } + ] +}]' +ABOVE_FULL='[{ + "left": [ + { + "type": "string", + "name": "background", + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "function": "powerline.segments.common.env.environment", + "args": {"variable": "DISPLAYED_ENV_VAR"} + } + ] +}]' +set_theme default_leftonly +export VIRTUAL_ENV= +source powerline/bindings/bash/powerline.sh +cd "$TEST_ROOT"/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1s +set_theme_option default.segment_data.hostname.display false +set_theme_option default_leftonly.segment_data.hostname.display false +set_theme_option default.segment_data.user.display false +set_theme_option default_leftonly.segment_data.user.display false +echo ' +abc +def +' +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +cd ../'«Unicode!»' +(exit 42)|(exit 43) +set_theme default +set_theme_option default.segments.above "$ABOVE_LEFT" +export DISPLAYED_ENV_VAR=foo +unset DISPLAYED_ENV_VAR +set_theme_option default.segments.above "$ABOVE_FULL" +export DISPLAYED_ENV_VAR=foo +unset DISPLAYED_ENV_VAR +set_theme_option default.segments.above +set_theme_option default.dividers.left.hard \$ABC +false +true is the last line +exit diff --git a/tests/test_shells/inputs/busybox b/tests/test_shells/inputs/busybox new file mode 100644 index 0000000..5d1495a --- /dev/null +++ b/tests/test_shells/inputs/busybox @@ -0,0 +1,37 @@ +set_theme_option() { + export POWERLINE_THEME_OVERRIDES="${POWERLINE_THEME_OVERRIDES};$1=$2" +} +set_theme() { + export POWERLINE_CONFIG_OVERRIDES="ext.shell.theme=$1" +} +set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false +set_theme default_leftonly +. powerline/bindings/shell/powerline.sh +export VIRTUAL_ENV= +cd "$TEST_ROOT"/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1s +set_theme_option default_leftonly.segment_data.hostname.display false +set_theme_option default_leftonly.segment_data.user.display false +echo ' +abc +def +' +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +cd ../'«Unicode!»' +set_theme_option default_leftonly.dividers.left.hard \$ABC +false +true is the last line +exit diff --git a/tests/test_shells/inputs/dash b/tests/test_shells/inputs/dash new file mode 100644 index 0000000..5d1495a --- /dev/null +++ b/tests/test_shells/inputs/dash @@ -0,0 +1,37 @@ +set_theme_option() { + export POWERLINE_THEME_OVERRIDES="${POWERLINE_THEME_OVERRIDES};$1=$2" +} +set_theme() { + export POWERLINE_CONFIG_OVERRIDES="ext.shell.theme=$1" +} +set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false +set_theme default_leftonly +. powerline/bindings/shell/powerline.sh +export VIRTUAL_ENV= +cd "$TEST_ROOT"/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1s +set_theme_option default_leftonly.segment_data.hostname.display false +set_theme_option default_leftonly.segment_data.user.display false +echo ' +abc +def +' +cd "$DIR1" +cd ../"$DIR2" +cd ../'\[\]' +cd ../'%%' +cd ../'#[bold]' +cd ../'(echo)' +cd ../'$(echo)' +cd ../'`echo`' +cd ../'«Unicode!»' +set_theme_option default_leftonly.dividers.left.hard \$ABC +false +true is the last line +exit diff --git a/tests/test_shells/inputs/fish b/tests/test_shells/inputs/fish new file mode 100644 index 0000000..9a20613 --- /dev/null +++ b/tests/test_shells/inputs/fish @@ -0,0 +1,69 @@ +function set_theme_option + set -g -x POWERLINE_THEME_OVERRIDES "$POWERLINE_THEME_OVERRIDES;$argv[1]=$argv[2]" +end +function set_theme + set -g -x POWERLINE_CONFIG_OVERRIDES "ext.shell.theme=$argv" +end +set -g -x POWERLINE_ +set -g ABOVE_LEFT '[{ + "left": [ + { + "function": "powerline.segments.common.env.environment", + "args": {"variable": "DISPLAYED_ENV_VAR"} + } + ] +}]' +set -g ABOVE_FULL '[{ + "left": [ + { + "type": "string", + "name": "background", + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "function": "powerline.segments.common.env.environment", + "args": {"variable": "DISPLAYED_ENV_VAR"} + } + ] +}]' +set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false +set_theme default_leftonly +set fish_function_path "$PWD/powerline/bindings/fish" $fish_function_path +while jobs | grep fish_update_completions + sleep 1 +end +powerline-setup +setenv VIRTUAL_ENV +cd "$TEST_ROOT"/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`' +cd ../'«Unicode!»' +set_theme default +set_theme_option default.segments.above "$ABOVE_LEFT" +set -g -x DISPLAYED_ENV_VAR foo +set -g -x -e DISPLAYED_ENV_VAR +set_theme_option default.segments.above "$ABOVE_FULL" +set -g -x DISPLAYED_ENV_VAR foo +set -g -x -e DISPLAYED_ENV_VAR +set_theme_option default.segments.above '' +set -g fish_key_bindings fish_vi_key_bindings +ii +false +true is the last line +exit diff --git a/tests/test_shells/inputs/ipython b/tests/test_shells/inputs/ipython new file mode 100644 index 0000000..257cba6 --- /dev/null +++ b/tests/test_shells/inputs/ipython @@ -0,0 +1,7 @@ +print ('cd ' + '"$TEST_ROOT"/3rd') # Start of the test marker +bool 42 +bool 44 +class Test(object): +pass + +exit diff --git a/tests/test_shells/inputs/mksh b/tests/test_shells/inputs/mksh new file mode 100644 index 0000000..ca45783 --- /dev/null +++ b/tests/test_shells/inputs/mksh @@ -0,0 +1,38 @@ +set_theme_option() { + export POWERLINE_THEME_OVERRIDES="${POWERLINE_THEME_OVERRIDES};$1=$2" +} +set_theme() { + export POWERLINE_CONFIG_OVERRIDES="ext.shell.theme=$1" +} +set_theme default_leftonly +set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false +. powerline/bindings/shell/powerline.sh +export VIRTUAL_ENV= +cd "$TEST_ROOT"/3rd +cd .git +cd .. +VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +VIRTUAL_ENV= +bgscript.sh & waitpid.sh +false +kill `cat pid` ; sleep 1 +set_theme_option default_leftonly.segment_data.hostname.display false +set_theme_option 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`' +cd ../'«Unicode!»' +set_theme_option default_leftonly.dividers.left.hard \$ABC +false +true is the last line +exit diff --git a/tests/test_shells/inputs/pdb b/tests/test_shells/inputs/pdb new file mode 100644 index 0000000..e9ac498 --- /dev/null +++ b/tests/test_shells/inputs/pdb @@ -0,0 +1,89 @@ +s + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_shells/inputs/rc b/tests/test_shells/inputs/rc new file mode 100644 index 0000000..c88bcf9 --- /dev/null +++ b/tests/test_shells/inputs/rc @@ -0,0 +1,33 @@ +fn set_theme_option { + POWERLINE_THEME_OVERRIDES = $POWERLINE_THEME_OVERRIDES';'$1'='$2 +} +set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false +POWERLINE_CONFIG_OVERRIDES = 'ext.shell.theme=default_leftonly' +. powerline/bindings/rc/powerline.rc +VIRTUAL_ENV = () +cd $TEST_ROOT/3rd +true cd "$TEST_ROOT"/3rd # Test start marker +cd .git +cd .. +VIRTUAL_ENV = '/home/foo/.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 ../'«Unicode!»' +false +set_theme_option default_leftonly.segment_data.hostname.display false +set_theme_option default_leftonly.segment_data.user.display false +echo `{ + echo Continuation! +} +true is the last line +exit diff --git a/tests/test_shells/inputs/tcsh b/tests/test_shells/inputs/tcsh new file mode 100644 index 0000000..c7d722a --- /dev/null +++ b/tests/test_shells/inputs/tcsh @@ -0,0 +1,24 @@ +setenv POWERLINE_THEME_OVERRIDES "default_leftonly.segment_data.hostname.args.only_if_ssh=false" +setenv POWERLINE_CONFIG_OVERRIDES "ext.shell.theme=default_leftonly" +source powerline/bindings/tcsh/powerline.tcsh +unsetenv VIRTUAL_ENV +cd "$TEST_ROOT"/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`' +cd ../'«Unicode\!»' +false +true is the last line +exit diff --git a/tests/test_shells/inputs/zsh b/tests/test_shells/inputs/zsh new file mode 100644 index 0000000..811684e --- /dev/null +++ b/tests/test_shells/inputs/zsh @@ -0,0 +1,90 @@ +unset HOME +unsetopt promptsp notransientrprompt +setopt interactivecomments +setopt autonamedirs +setopt warncreateglobal +function set_theme_option() { + export POWERLINE_THEME_OVERRIDES="${POWERLINE_THEME_OVERRIDES};$1=$2" + powerline-reload-config +} +function set_theme() { + export POWERLINE_CONFIG_OVERRIDES="ext.shell.theme=$1" + powerline-reload-config +} +if test -n "$POWERLINE_NO_ZSH_ZPYTHON" ; then + powerline-reload-config(): +fi +source powerline/bindings/zsh/powerline.zsh +set_theme_option default_leftonly.segment_data.hostname.args.only_if_ssh false +set_theme_option default.segment_data.hostname.args.only_if_ssh false +ABOVE_LEFT='[{ + "left": [ + { + "function": "powerline.segments.common.env.environment", + "args": {"variable": "DISPLAYED_ENV_VAR"} + } + ] +}]' +ABOVE_FULL='[{ + "left": [ + { + "type": "string", + "name": "background", + "draw_hard_divider": false, + "width": "auto" + } + ], + "right": [ + { + "function": "powerline.segments.common.env.environment", + "args": {"variable": "DISPLAYED_ENV_VAR"} + } + ] +}]' +set_theme default_leftonly +export VIRTUAL_ENV= +cd "$TEST_ROOT"/3rd +cd .git +cd .. +VIRTUAL_ENV="/home/USER/.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 ../'«Unicode!»' +cd .. +bindkey -v ; set_theme default + + +echo abc +false +set_theme_option default.segment_data.hostname.display false +set_theme_option default.segment_data.user.display false +select abc in def ghi jkl +do + echo $abc + break +done +1 +cd . +cd . +set_theme_option default.segments.above "$ABOVE_LEFT" +export DISPLAYED_ENV_VAR=foo +unset DISPLAYED_ENV_VAR +set_theme_option default.segments.above "$ABOVE_FULL" +export DISPLAYED_ENV_VAR=foo +unset DISPLAYED_ENV_VAR +set_theme_option default.segments.above +hash -d foo=$PWD:h ; cd . +set_theme_option default.dividers.left.hard \$ABC +true +true is the last line +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 0000000..ffb5865 --- /dev/null +++ b/tests/test_shells/ipython_home/profile_default/ipython_config.py @@ -0,0 +1,22 @@ +from powerline.bindings.ipython.since_7 import PowerlinePrompts +import os +c = get_config() +c.TerminalInteractiveShell.simple_prompt = False +c.TerminalIPythonApp.display_banner = False +c.TerminalInteractiveShell.prompts_class = PowerlinePrompts +c.TerminalInteractiveShell.autocall = 1 +c.Powerline.config_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/outputs/bash.daemon.ok b/tests/test_shells/outputs/bash.daemon.ok new file mode 100644 index 0000000..7b8cd6c --- /dev/null +++ b/tests/test_shells/outputs/bash.daemon.ok @@ -0,0 +1,45 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default.segment_data.hostname.display false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default.segment_data.user.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  tmp  shell  3rd  echo ' +                                   abc +                                   def +                                   ' + +abc +def + +  BRANCH  …  tmp  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`  cd ../'«Unicode!»' +  BRANCH  …  shell  3rd  «Unicode!»  (exit 42)|(exit 43) +  BRANCH  …  shell  3rd  «Unicode!»  42  43  set_theme default + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above "$ABOVE_LEFT" + …  shell  3rd  «Unicode!»     BRANCH export DISPLAYED_ENV_VAR=foo + foo   + …  shell  3rd  «Unicode!»     BRANCH unset DISPLAYED_ENV_VAR + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above "$ABOVE_FULL" +                                                                                                                                                                                                                                                                                                             + …  shell  3rd  «Unicode!»     BRANCH export DISPLAYED_ENV_VAR=foo +                                                                                                                                                                                                                                                                                                       foo  + …  shell  3rd  «Unicode!»     BRANCH unset DISPLAYED_ENV_VAR +                                                                                                                                                                                                                                                                                                             + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.dividers.left.hard \$ABC + …  shell  3rd  «Unicode!» $ABC   BRANCH false diff --git a/tests/test_shells/outputs/bash.nodaemon.ok b/tests/test_shells/outputs/bash.nodaemon.ok new file mode 100644 index 0000000..458c1a1 --- /dev/null +++ b/tests/test_shells/outputs/bash.nodaemon.ok @@ -0,0 +1,45 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default.segment_data.hostname.display false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default.segment_data.user.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  tmp  shell  3rd  echo ' +   abc +   def +   ' + +abc +def + +  BRANCH  …  tmp  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`  cd ../'«Unicode!»' +  BRANCH  …  shell  3rd  «Unicode!»  (exit 42)|(exit 43) +  BRANCH  …  shell  3rd  «Unicode!»  42  43  set_theme default + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above "$ABOVE_LEFT" + …  shell  3rd  «Unicode!»     BRANCH export DISPLAYED_ENV_VAR=foo + foo   + …  shell  3rd  «Unicode!»     BRANCH unset DISPLAYED_ENV_VAR + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above "$ABOVE_FULL" +                                                                                                                                                                                                                                                                                                             + …  shell  3rd  «Unicode!»     BRANCH export DISPLAYED_ENV_VAR=foo +                                                                                                                                                                                                                                                                                                       foo  + …  shell  3rd  «Unicode!»     BRANCH unset DISPLAYED_ENV_VAR +                                                                                                                                                                                                                                                                                                             + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.segments.above + …  shell  3rd  «Unicode!»     BRANCH set_theme_option default.dividers.left.hard \$ABC + …  shell  3rd  «Unicode!» $ABC   BRANCH false diff --git a/tests/test_shells/outputs/busybox.daemon.ok b/tests/test_shells/outputs/busybox.daemon.ok new file mode 100644 index 0000000..446d88e --- /dev/null +++ b/tests/test_shells/outputs/busybox.daemon.ok @@ -0,0 +1,29 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  tmp  shell  3rd  echo ' +                                   abc +                                   def +                                   ' + +abc +def + +  BRANCH  …  tmp  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`  cd ../'«Unicode!»' +  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.dividers.left.hard \$ABC +  BRANCH $ABC…  shell  3rd  «Unicode!» $ABCfalse diff --git a/tests/test_shells/outputs/busybox.nodaemon.ok b/tests/test_shells/outputs/busybox.nodaemon.ok new file mode 100644 index 0000000..afda9a5 --- /dev/null +++ b/tests/test_shells/outputs/busybox.nodaemon.ok @@ -0,0 +1,29 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1]+ Terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  tmp  shell  3rd  echo ' +   abc +   def +   ' + +abc +def + +  BRANCH  …  tmp  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`  cd ../'«Unicode!»' +  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.dividers.left.hard \$ABC +  BRANCH $ABC…  shell  3rd  «Unicode!» $ABCfalse diff --git a/tests/test_shells/outputs/dash.daemon.ok b/tests/test_shells/outputs/dash.daemon.ok new file mode 100644 index 0000000..71ca500 --- /dev/null +++ b/tests/test_shells/outputs/dash.daemon.ok @@ -0,0 +1,28 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +set_theme_option default_leftonly.segment_data.hostname.display false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1   USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  tmp  shell  3rd  echo ' +                                   abc +                                   def +                                   ' + +abc +def + +  BRANCH  …  tmp  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`  cd ../'«Unicode!»' +  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.dividers.left.hard \$ABC +  BRANCH $ABC…  shell  3rd  «Unicode!» $ABCfalse diff --git a/tests/test_shells/outputs/dash.nodaemon.ok b/tests/test_shells/outputs/dash.nodaemon.ok new file mode 100644 index 0000000..c289cd2 --- /dev/null +++ b/tests/test_shells/outputs/dash.nodaemon.ok @@ -0,0 +1,28 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +set_theme_option default_leftonly.segment_data.hostname.display false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1   USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  tmp  shell  3rd  echo ' +   abc +   def +   ' + +abc +def + +  BRANCH  …  tmp  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`  cd ../'«Unicode!»' +  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.dividers.left.hard \$ABC +  BRANCH $ABC…  shell  3rd  «Unicode!» $ABCfalse diff --git a/tests/test_shells/outputs/fish.ok b/tests/test_shells/outputs/fish.ok new file mode 100644 index 0000000..4d208bb --- /dev/null +++ b/tests/test_shells/outputs/fish.ok @@ -0,0 +1,52 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git   +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1   +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1   +  HOSTNAME  USER   BRANCH  …  tmp  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`   +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»   + USER  …  shell  3rd  «Unicode!»   +   BRANCH  +   BRANCH  +   BRANCH  + USER  …  shell  3rd  «Unicode!»   +   BRANCH  +   BRANCH  +   BRANCH  + foo   + USER  …  shell  3rd  «Unicode!»   +   BRANCH  +   BRANCH  +   BRANCH  +                                                                                                                                                                                                                                                                                                            +                                                                                                                                                                                                                                                                                                      foo  +                                                                                                                                                                                                                                                                                                            + USER  …  shell  3rd  «Unicode!»   +   BRANCH  +   BRANCH  +   BRANCH  + INSERT  USER  …  shell  3rd  «Unicode!»   +   BRANCH  + DEFAULT  USER  …  shell  3rd  «Unicode!»   +   BRANCH  + INSERT  USER  …  shell  3rd  «Unicode!»   +   BRANCH  + DEFAULT  USER  …  shell  3rd  «Unicode!»   +   BRANCH  + INSERT  USER  …  shell  3rd  «Unicode!»   +   BRANCH  +   BRANCH  + INSERT  USER  …  shell  3rd  «Unicode!»   +   BRANCH  +   BRANCH  +   BRANCH  diff --git a/tests/test_shells/outputs/ipython.ok b/tests/test_shells/outputs/ipython.ok new file mode 100644 index 0000000..166604d --- /dev/null +++ b/tests/test_shells/outputs/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/outputs/mksh.daemon.ok b/tests/test_shells/outputs/mksh.daemon.ok new file mode 100644 index 0000000..264dff8 --- /dev/null +++ b/tests/test_shells/outputs/mksh.daemon.ok @@ -0,0 +1,32 @@ + +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1 +[1] + Terminated bash -c ... +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  tmp  shell  3rd  echo -n +  BRANCH  …  tmp  shell  3rd  echo ' +                                   abc +                                   def +                                   ' + +abc +def + +  BRANCH  …  tmp  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`  cd ../'«Unicode!»' +  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.dividers.left.hard \$ABC +  BRANCH $ABC…  shell  3rd  «Unicode!» $ABCfalse diff --git a/tests/test_shells/outputs/mksh.nodaemon.ok b/tests/test_shells/outputs/mksh.nodaemon.ok new file mode 100644 index 0000000..d8d9d70 --- /dev/null +++ b/tests/test_shells/outputs/mksh.nodaemon.ok @@ -0,0 +1,32 @@ + +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="$HOME/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1 +[1] + Terminated bash -c ... +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  tmp  shell  3rd  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  tmp  shell  3rd  echo -n +  BRANCH  …  tmp  shell  3rd  echo ' +   abc +   def +   ' + +abc +def + +  BRANCH  …  tmp  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`  cd ../'«Unicode!»' +  BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.dividers.left.hard \$ABC +  BRANCH $ABC…  shell  3rd  «Unicode!» $ABCfalse diff --git a/tests/test_shells/outputs/pdb.module.ok b/tests/test_shells/outputs/pdb.module.ok new file mode 100644 index 0000000..7554dd6 --- /dev/null +++ b/tests/test_shells/outputs/pdb.module.ok @@ -0,0 +1,222 @@ + 1  pdb-script.py:6    +--Call-- +-> class Foo(object): + 2  pdb-script.py:6   pdb-script.py:6 Foo   +-> class Foo(object): + 2  pdb-script.py:6   pdb-script.py:6 Foo   +-> def __init__(self): + 2  pdb-script.py:6   pdb-script.py:7 Foo   +-> @classmethod + 2  pdb-script.py:6   pdb-script.py:13 Foo   +-> @staticmethod + 2  pdb-script.py:6   pdb-script.py:17 Foo   +-> def bra(self): + 2  pdb-script.py:6   pdb-script.py:21 Foo   +--Return-- +-> def bra(self): + 2  pdb-script.py:6   pdb-script.py:21 Foo   +-> def brah(): + 1  pdb-script.py:25    +-> f = Foo() + 1  pdb-script.py:29    +--Call-- +-> def __init__(self): + 2  pdb-script.py:29   pdb-script.py:7 __init__   +-> nop('__init__') + 2  pdb-script.py:29   pdb-script.py:8 __init__   +--Call-- +-> def nop(_): + 3  pdb-script.py:29   pdb-script.py:8 __init__  pdb-script.py:2 nop   +-> pass + 3  pdb-script.py:29   pdb-script.py:8 __init__  pdb-script.py:3 nop   +--Return-- +-> pass + 3  pdb-script.py:29   pdb-script.py:8 __init__  pdb-script.py:3 nop   +-> self.bar() + 2  pdb-script.py:29   pdb-script.py:9 __init__   +--Call-- +-> @classmethod + 3  pdb-script.py:29   pdb-script.py:9 __init__  pdb-script.py:13 bar   +-> nop(cls.__name__) + 3  pdb-script.py:29   pdb-script.py:9 __init__  pdb-script.py:15 bar   +--Call-- +-> def nop(_): + 4  pdb-script.py:9 __init__  pdb-script.py:15 bar  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:9 __init__  pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:9 __init__  pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> nop(cls.__name__) + 3  pdb-script.py:29   pdb-script.py:9 __init__  pdb-script.py:15 bar   +-> self.baz() + 2  pdb-script.py:29   pdb-script.py:10 __init__   +--Call-- +-> @staticmethod + 3  pdb-script.py:29   pdb-script.py:10 __init__  pdb-script.py:17 baz   +-> nop(1) + 3  pdb-script.py:29   pdb-script.py:10 __init__  pdb-script.py:19 baz   +--Call-- +-> def nop(_): + 4  pdb-script.py:10 __init__  pdb-script.py:19 baz  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:10 __init__  pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:10 __init__  pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> nop(1) + 3  pdb-script.py:29   pdb-script.py:10 __init__  pdb-script.py:19 baz   +-> self.bra() + 2  pdb-script.py:29   pdb-script.py:11 __init__   +--Call-- +-> def bra(self): + 3  pdb-script.py:29   pdb-script.py:11 __init__  pdb-script.py:21 bra   +-> nop(self.__class__.__name__) + 3  pdb-script.py:29   pdb-script.py:11 __init__  pdb-script.py:22 bra   +--Call-- +-> def nop(_): + 4  pdb-script.py:11 __init__  pdb-script.py:22 bra  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:11 __init__  pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:11 __init__  pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> nop(self.__class__.__name__) + 3  pdb-script.py:29   pdb-script.py:11 __init__  pdb-script.py:22 bra   +--Return-- +-> self.bra() + 2  pdb-script.py:29   pdb-script.py:11 __init__   +-> Foo.bar() + 1  pdb-script.py:30    +--Call-- +-> @classmethod + 2  pdb-script.py:30   pdb-script.py:13 bar   +-> nop(cls.__name__) + 2  pdb-script.py:30   pdb-script.py:15 bar   +--Call-- +-> def nop(_): + 3  pdb-script.py:30   pdb-script.py:15 bar  pdb-script.py:2 nop   +-> pass + 3  pdb-script.py:30   pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> pass + 3  pdb-script.py:30   pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> nop(cls.__name__) + 2  pdb-script.py:30   pdb-script.py:15 bar   +-> Foo.baz() + 1  pdb-script.py:31    +--Call-- +-> @staticmethod + 2  pdb-script.py:31   pdb-script.py:17 baz   +-> nop(1) + 2  pdb-script.py:31   pdb-script.py:19 baz   +--Call-- +-> def nop(_): + 3  pdb-script.py:31   pdb-script.py:19 baz  pdb-script.py:2 nop   +-> pass + 3  pdb-script.py:31   pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> pass + 3  pdb-script.py:31   pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> nop(1) + 2  pdb-script.py:31   pdb-script.py:19 baz   +-> Foo.bra(f) + 1  pdb-script.py:32    +--Call-- +-> def bra(self): + 2  pdb-script.py:32   pdb-script.py:21 bra   +-> nop(self.__class__.__name__) + 2  pdb-script.py:32   pdb-script.py:22 bra   +--Call-- +-> def nop(_): + 3  pdb-script.py:32   pdb-script.py:22 bra  pdb-script.py:2 nop   +-> pass + 3  pdb-script.py:32   pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> pass + 3  pdb-script.py:32   pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> nop(self.__class__.__name__) + 2  pdb-script.py:32   pdb-script.py:22 bra   +-> f.bar() + 1  pdb-script.py:34    +--Call-- +-> @classmethod + 2  pdb-script.py:34   pdb-script.py:13 bar   +-> nop(cls.__name__) + 2  pdb-script.py:34   pdb-script.py:15 bar   +--Call-- +-> def nop(_): + 3  pdb-script.py:34   pdb-script.py:15 bar  pdb-script.py:2 nop   +-> pass + 3  pdb-script.py:34   pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> pass + 3  pdb-script.py:34   pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> nop(cls.__name__) + 2  pdb-script.py:34   pdb-script.py:15 bar   +-> f.baz() + 1  pdb-script.py:35    +--Call-- +-> @staticmethod + 2  pdb-script.py:35   pdb-script.py:17 baz   +-> nop(1) + 2  pdb-script.py:35   pdb-script.py:19 baz   +--Call-- +-> def nop(_): + 3  pdb-script.py:35   pdb-script.py:19 baz  pdb-script.py:2 nop   +-> pass + 3  pdb-script.py:35   pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> pass + 3  pdb-script.py:35   pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> nop(1) + 2  pdb-script.py:35   pdb-script.py:19 baz   +-> f.bra() + 1  pdb-script.py:36    +--Call-- +-> def bra(self): + 2  pdb-script.py:36   pdb-script.py:21 bra   +-> nop(self.__class__.__name__) + 2  pdb-script.py:36   pdb-script.py:22 bra   +--Call-- +-> def nop(_): + 3  pdb-script.py:36   pdb-script.py:22 bra  pdb-script.py:2 nop   +-> pass + 3  pdb-script.py:36   pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> pass + 3  pdb-script.py:36   pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> nop(self.__class__.__name__) + 2  pdb-script.py:36   pdb-script.py:22 bra   +-> brah() + 1  pdb-script.py:38    +--Call-- +-> def brah(): + 2  pdb-script.py:38   pdb-script.py:25 brah   +-> nop('brah') + 2  pdb-script.py:38   pdb-script.py:26 brah   +--Call-- +-> def nop(_): + 3  pdb-script.py:38   pdb-script.py:26 brah  pdb-script.py:2 nop   +-> pass + 3  pdb-script.py:38   pdb-script.py:26 brah  pdb-script.py:3 nop   +--Return-- +-> pass + 3  pdb-script.py:38   pdb-script.py:26 brah  pdb-script.py:3 nop   +--Return-- +-> nop('brah') + 2  pdb-script.py:38   pdb-script.py:26 brah   +--Return-- +-> brah() + 1  pdb-script.py:38    +--Return-- + 0   diff --git a/tests/test_shells/outputs/pdb.subclass.ok b/tests/test_shells/outputs/pdb.subclass.ok new file mode 100644 index 0000000..d8eba5e --- /dev/null +++ b/tests/test_shells/outputs/pdb.subclass.ok @@ -0,0 +1,217 @@ + 2  :1   pdb-script.py:6    +--Call-- +-> class Foo(object): + 3  :1   pdb-script.py:6   pdb-script.py:6 Foo   +-> class Foo(object): + 3  :1   pdb-script.py:6   pdb-script.py:6 Foo   +-> def __init__(self): + 3  :1   pdb-script.py:6   pdb-script.py:7 Foo   +-> @classmethod + 3  :1   pdb-script.py:6   pdb-script.py:13 Foo   +-> @staticmethod + 3  :1   pdb-script.py:6   pdb-script.py:17 Foo   +-> def bra(self): + 3  :1   pdb-script.py:6   pdb-script.py:21 Foo   +--Return-- +-> def bra(self): + 3  :1   pdb-script.py:6   pdb-script.py:21 Foo   +-> def brah(): + 2  :1   pdb-script.py:25    +-> f = Foo() + 2  :1   pdb-script.py:29    +--Call-- +-> def __init__(self): + 3  :1   pdb-script.py:29   pdb-script.py:7 __init__   +-> nop('__init__') + 3  :1   pdb-script.py:29   pdb-script.py:8 __init__   +--Call-- +-> def nop(_): + 4  pdb-script.py:29   pdb-script.py:8 __init__  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:29   pdb-script.py:8 __init__  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:29   pdb-script.py:8 __init__  pdb-script.py:3 nop   +-> self.bar() + 3  :1   pdb-script.py:29   pdb-script.py:9 __init__   +--Call-- +-> @classmethod + 4  pdb-script.py:29   pdb-script.py:9 __init__  pdb-script.py:13 bar   +-> nop(cls.__name__) + 4  pdb-script.py:29   pdb-script.py:9 __init__  pdb-script.py:15 bar   +--Call-- +-> def nop(_): + 5  pdb-script.py:9 __init__  pdb-script.py:15 bar  pdb-script.py:2 nop   +-> pass + 5  pdb-script.py:9 __init__  pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> pass + 5  pdb-script.py:9 __init__  pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> nop(cls.__name__) + 4  pdb-script.py:29   pdb-script.py:9 __init__  pdb-script.py:15 bar   +-> self.baz() + 3  :1   pdb-script.py:29   pdb-script.py:10 __init__   +--Call-- +-> @staticmethod + 4  pdb-script.py:29   pdb-script.py:10 __init__  pdb-script.py:17 baz   +-> nop(1) + 4  pdb-script.py:29   pdb-script.py:10 __init__  pdb-script.py:19 baz   +--Call-- +-> def nop(_): + 5  pdb-script.py:10 __init__  pdb-script.py:19 baz  pdb-script.py:2 nop   +-> pass + 5  pdb-script.py:10 __init__  pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> pass + 5  pdb-script.py:10 __init__  pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> nop(1) + 4  pdb-script.py:29   pdb-script.py:10 __init__  pdb-script.py:19 baz   +-> self.bra() + 3  :1   pdb-script.py:29   pdb-script.py:11 __init__   +--Call-- +-> def bra(self): + 4  pdb-script.py:29   pdb-script.py:11 __init__  pdb-script.py:21 bra   +-> nop(self.__class__.__name__) + 4  pdb-script.py:29   pdb-script.py:11 __init__  pdb-script.py:22 bra   +--Call-- +-> def nop(_): + 5  pdb-script.py:11 __init__  pdb-script.py:22 bra  pdb-script.py:2 nop   +-> pass + 5  pdb-script.py:11 __init__  pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> pass + 5  pdb-script.py:11 __init__  pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> nop(self.__class__.__name__) + 4  pdb-script.py:29   pdb-script.py:11 __init__  pdb-script.py:22 bra   +--Return-- +-> self.bra() + 3  :1   pdb-script.py:29   pdb-script.py:11 __init__   +-> Foo.bar() + 2  :1   pdb-script.py:30    +--Call-- +-> @classmethod + 3  :1   pdb-script.py:30   pdb-script.py:13 bar   +-> nop(cls.__name__) + 3  :1   pdb-script.py:30   pdb-script.py:15 bar   +--Call-- +-> def nop(_): + 4  pdb-script.py:30   pdb-script.py:15 bar  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:30   pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:30   pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> nop(cls.__name__) + 3  :1   pdb-script.py:30   pdb-script.py:15 bar   +-> Foo.baz() + 2  :1   pdb-script.py:31    +--Call-- +-> @staticmethod + 3  :1   pdb-script.py:31   pdb-script.py:17 baz   +-> nop(1) + 3  :1   pdb-script.py:31   pdb-script.py:19 baz   +--Call-- +-> def nop(_): + 4  pdb-script.py:31   pdb-script.py:19 baz  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:31   pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:31   pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> nop(1) + 3  :1   pdb-script.py:31   pdb-script.py:19 baz   +-> Foo.bra(f) + 2  :1   pdb-script.py:32    +--Call-- +-> def bra(self): + 3  :1   pdb-script.py:32   pdb-script.py:21 bra   +-> nop(self.__class__.__name__) + 3  :1   pdb-script.py:32   pdb-script.py:22 bra   +--Call-- +-> def nop(_): + 4  pdb-script.py:32   pdb-script.py:22 bra  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:32   pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:32   pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> nop(self.__class__.__name__) + 3  :1   pdb-script.py:32   pdb-script.py:22 bra   +-> f.bar() + 2  :1   pdb-script.py:34    +--Call-- +-> @classmethod + 3  :1   pdb-script.py:34   pdb-script.py:13 bar   +-> nop(cls.__name__) + 3  :1   pdb-script.py:34   pdb-script.py:15 bar   +--Call-- +-> def nop(_): + 4  pdb-script.py:34   pdb-script.py:15 bar  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:34   pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:34   pdb-script.py:15 bar  pdb-script.py:3 nop   +--Return-- +-> nop(cls.__name__) + 3  :1   pdb-script.py:34   pdb-script.py:15 bar   +-> f.baz() + 2  :1   pdb-script.py:35    +--Call-- +-> @staticmethod + 3  :1   pdb-script.py:35   pdb-script.py:17 baz   +-> nop(1) + 3  :1   pdb-script.py:35   pdb-script.py:19 baz   +--Call-- +-> def nop(_): + 4  pdb-script.py:35   pdb-script.py:19 baz  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:35   pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:35   pdb-script.py:19 baz  pdb-script.py:3 nop   +--Return-- +-> nop(1) + 3  :1   pdb-script.py:35   pdb-script.py:19 baz   +-> f.bra() + 2  :1   pdb-script.py:36    +--Call-- +-> def bra(self): + 3  :1   pdb-script.py:36   pdb-script.py:21 bra   +-> nop(self.__class__.__name__) + 3  :1   pdb-script.py:36   pdb-script.py:22 bra   +--Call-- +-> def nop(_): + 4  pdb-script.py:36   pdb-script.py:22 bra  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:36   pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:36   pdb-script.py:22 bra  pdb-script.py:3 nop   +--Return-- +-> nop(self.__class__.__name__) + 3  :1   pdb-script.py:36   pdb-script.py:22 bra   +-> brah() + 2  :1   pdb-script.py:38    +--Call-- +-> def brah(): + 3  :1   pdb-script.py:38   pdb-script.py:25 brah   +-> nop('brah') + 3  :1   pdb-script.py:38   pdb-script.py:26 brah   +--Call-- +-> def nop(_): + 4  pdb-script.py:38   pdb-script.py:26 brah  pdb-script.py:2 nop   +-> pass + 4  pdb-script.py:38   pdb-script.py:26 brah  pdb-script.py:3 nop   +--Return-- +-> pass + 4  pdb-script.py:38   pdb-script.py:26 brah  pdb-script.py:3 nop   +--Return-- +-> nop('brah') + 3  :1   pdb-script.py:38   pdb-script.py:26 brah   diff --git a/tests/test_shells/outputs/rc.daemon.ok b/tests/test_shells/outputs/rc.daemon.ok new file mode 100644 index 0000000..c49b9a3 --- /dev/null +++ b/tests/test_shells/outputs/rc.daemon.ok @@ -0,0 +1,24 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV = '/home/foo/.virtenvs/some-virtual-environment' +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV = () +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `{cat pid} ; sleep 1s +  HOSTNAME  USER   BRANCH  …  tmp  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 ../'«Unicode!»' +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»  false +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»  1  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  shell  3rd  «Unicode!»  echo `{ +                                           echo Continuation! +                                          } +Continuation! diff --git a/tests/test_shells/outputs/rc.nodaemon.ok b/tests/test_shells/outputs/rc.nodaemon.ok new file mode 100644 index 0000000..28376cb --- /dev/null +++ b/tests/test_shells/outputs/rc.nodaemon.ok @@ -0,0 +1,24 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV = '/home/foo/.virtenvs/some-virtual-environment' +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV = () +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `{cat pid} ; sleep 1s +  HOSTNAME  USER   BRANCH  …  tmp  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 ../'«Unicode!»' +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»  false +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»  1  set_theme_option default_leftonly.segment_data.hostname.display false + USER   BRANCH  …  shell  3rd  «Unicode!»  set_theme_option default_leftonly.segment_data.user.display false +  BRANCH  …  shell  3rd  «Unicode!»  echo `{ +    echo Continuation! +   } +Continuation! diff --git a/tests/test_shells/outputs/tcsh.ok b/tests/test_shells/outputs/tcsh.ok new file mode 100644 index 0000000..07089bf --- /dev/null +++ b/tests/test_shells/outputs/tcsh.ok @@ -0,0 +1,17 @@ +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git   +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd   +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1   +  HOSTNAME  USER   BRANCH  …  tmp  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`   +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»   diff --git a/tests/test_shells/outputs/zsh.daemon.ok b/tests/test_shells/outputs/zsh.daemon.ok new file mode 100644 index 0000000..32e80d8 --- /dev/null +++ b/tests/test_shells/outputs/zsh.daemon.ok @@ -0,0 +1,52 @@ + +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="/home/USER/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1] + terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  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 ../'«Unicode!»' +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bindkey -v ; set_theme default + INSERT   HOSTNAME  USER  …  tmp  shell  3rd   COMMND   HOSTNAME  USER  …  tmp  shell  3rd   + INSERT   HOSTNAME  USER  …  tmp  shell  3rd   + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  echo abc +abc + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  false + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  set_theme_option default.segment_data.hostname.display false + INSERT  USER  …  tmp  shell  3rd  set_theme_option default.segment_data.user.display false + INSERT  …  tmp  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  …  tmp  shell  3rd  cd . + INSERT  …  tmp  shell  3rd  cd . + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above "$ABOVE_LEFT" + INSERT  …  tmp  shell  3rd  export DISPLAYED_ENV_VAR=foo + foo   + INSERT  …  tmp  shell  3rd  unset DISPLAYED_ENV_VAR + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above "$ABOVE_FULL" +                                                                                                                                                                                                                                                                                                            + INSERT  …  tmp  shell  3rd  export DISPLAYED_ENV_VAR=foo +                                                                                                                                                                                                                                                                                                      foo  + INSERT  …  tmp  shell  3rd  unset DISPLAYED_ENV_VAR +                                                                                                                                                                                                                                                                                                            + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above + INSERT  …  tmp  shell  3rd  hash -d foo=$PWD:h ; cd . + INSERT  ~foo  3rd  set_theme_option default.dividers.left.hard \$ABC + INSERT $ABC~foo  3rd $ABCtrue diff --git a/tests/test_shells/outputs/zsh.nodaemon.ok b/tests/test_shells/outputs/zsh.nodaemon.ok new file mode 100644 index 0000000..3aa285f --- /dev/null +++ b/tests/test_shells/outputs/zsh.nodaemon.ok @@ -0,0 +1,52 @@ + +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="/home/USER/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1] + terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  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 ../'«Unicode!»' +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bindkey -v ; set_theme default + INSERT   HOSTNAME  USER  …  tmp  shell  3rd   COMMND   HOSTNAME  USER  …  tmp  shell  3rd   + INSERT   HOSTNAME  USER  …  tmp  shell  3rd   + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  echo abc +abc + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  false + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  set_theme_option default.segment_data.hostname.display false + INSERT  USER  …  tmp  shell  3rd  set_theme_option default.segment_data.user.display false + INSERT  …  tmp  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  …  tmp  shell  3rd  cd . + INSERT  …  tmp  shell  3rd  cd . + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above "$ABOVE_LEFT" + INSERT  …  tmp  shell  3rd  export DISPLAYED_ENV_VAR=foo + foo   + INSERT  …  tmp  shell  3rd  unset DISPLAYED_ENV_VAR + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above "$ABOVE_FULL" +                                                                                                                                                                                                                                                                                                            + INSERT  …  tmp  shell  3rd  export DISPLAYED_ENV_VAR=foo +                                                                                                                                                                                                                                                                                                      foo  + INSERT  …  tmp  shell  3rd  unset DISPLAYED_ENV_VAR +                                                                                                                                                                                                                                                                                                            + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above + INSERT  …  tmp  shell  3rd  hash -d foo=$PWD:h ; cd . + INSERT  ~foo  3rd  set_theme_option default.dividers.left.hard \$ABC + INSERT $ABC~foo  3rd $ABCtrue diff --git a/tests/test_shells/outputs/zsh.zpython.ok b/tests/test_shells/outputs/zsh.zpython.ok new file mode 100644 index 0000000..32e80d8 --- /dev/null +++ b/tests/test_shells/outputs/zsh.zpython.ok @@ -0,0 +1,52 @@ + +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  cd .git +  HOSTNAME  USER   BRANCH  …  shell  3rd  .git  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV="/home/USER/.virtenvs/some-virtual-environment" +  HOSTNAME  USER  (e) some-virtual-environment   BRANCH  …  tmp  shell  3rd  VIRTUAL_ENV= +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bgscript.sh & waitpid.sh +[1] PID +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  false +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  1  1  kill `cat pid` ; sleep 1s +[1] + terminated bgscript.sh +  HOSTNAME  USER   BRANCH  …  tmp  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 ../'«Unicode!»' +  HOSTNAME  USER   BRANCH  …  shell  3rd  «Unicode!»  cd .. +  HOSTNAME  USER   BRANCH  …  tmp  shell  3rd  bindkey -v ; set_theme default + INSERT   HOSTNAME  USER  …  tmp  shell  3rd   COMMND   HOSTNAME  USER  …  tmp  shell  3rd   + INSERT   HOSTNAME  USER  …  tmp  shell  3rd   + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  echo abc +abc + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  false + INSERT   HOSTNAME  USER  …  tmp  shell  3rd  set_theme_option default.segment_data.hostname.display false + INSERT  USER  …  tmp  shell  3rd  set_theme_option default.segment_data.user.display false + INSERT  …  tmp  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  …  tmp  shell  3rd  cd . + INSERT  …  tmp  shell  3rd  cd . + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above "$ABOVE_LEFT" + INSERT  …  tmp  shell  3rd  export DISPLAYED_ENV_VAR=foo + foo   + INSERT  …  tmp  shell  3rd  unset DISPLAYED_ENV_VAR + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above "$ABOVE_FULL" +                                                                                                                                                                                                                                                                                                            + INSERT  …  tmp  shell  3rd  export DISPLAYED_ENV_VAR=foo +                                                                                                                                                                                                                                                                                                      foo  + INSERT  …  tmp  shell  3rd  unset DISPLAYED_ENV_VAR +                                                                                                                                                                                                                                                                                                            + INSERT  …  tmp  shell  3rd  set_theme_option default.segments.above + INSERT  …  tmp  shell  3rd  hash -d foo=$PWD:h ; cd . + INSERT  ~foo  3rd  set_theme_option default.dividers.left.hard \$ABC + INSERT $ABC~foo  3rd $ABCtrue diff --git a/tests/test_shells/pdb-main.py b/tests/test_shells/pdb-main.py new file mode 100644 index 0000000..37af785 --- /dev/null +++ b/tests/test_shells/pdb-main.py @@ -0,0 +1,24 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import pdb +import os +import sys + +from powerline.bindings.pdb import use_powerline_prompt + + +@use_powerline_prompt +class Pdb(pdb.Pdb): + pass + + +p = Pdb() + + +script = os.path.join(os.path.dirname(__file__), 'pdb-script.py') +with open(script, 'r') as fd: + code = compile(fd.read(), script, 'exec') + + +p.run('exec(code)', globals={'code': code}) diff --git a/tests/test_shells/pdb-script.py b/tests/test_shells/pdb-script.py new file mode 100644 index 0000000..40db5e8 --- /dev/null +++ b/tests/test_shells/pdb-script.py @@ -0,0 +1,38 @@ +# vim:fileencoding=utf-8:noet +def nop(_): + pass + + +class Foo(object): + def __init__(self): + nop('__init__') + self.bar() + self.baz() + self.bra() + + @classmethod + def bar(cls): + nop(cls.__name__) + + @staticmethod + def baz(): + nop(1) + + def bra(self): + nop(self.__class__.__name__) + + +def brah(): + nop('brah') + + +f = Foo() +Foo.bar() +Foo.baz() +Foo.bra(f) + +f.bar() +f.baz() +f.bra() + +brah() diff --git a/tests/test_shells/postproc.py b/tests/test_shells/postproc.py new file mode 100755 index 0000000..7926155 --- /dev/null +++ b/tests/test_shells/postproc.py @@ -0,0 +1,130 @@ +#!/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 +import platform +import re + + +test_root = os.environ['TEST_ROOT'] +test_type = sys.argv[1] +test_client = sys.argv[2] +shell = sys.argv[3] +fname = os.path.join(test_root, '.'.join((shell, test_type, test_client, 'full.log'))) +new_fname = os.path.join(test_root, '.'.join((shell, test_type, test_client, 'log'))) +pid_fname = os.path.join(test_root, '3rd', 'pid') + +is_pypy = platform.python_implementation() == 'PyPy' + + +try: + with open(pid_fname, 'r') as P: + pid = P.read().strip() +except IOError: + pid = None +hostname = socket.gethostname() +user = os.environ['USER'] + +REFS_RE = re.compile(r'^\[\d+ refs\]\n') +IPYPY_DEANSI_RE = re.compile(r'\033(?:\[(?:\?\d+[lh]|[^a-zA-Z]+[a-ln-zA-Z])|[=>])') +ZSH_HL_RE = re.compile(r'\033\[\?\d+[hl]') + +start_str = 'cd "$TEST_ROOT"/3rd' +if shell == 'pdb': + start_str = 'class Foo(object):' + +with codecs.open(fname, 'r', encoding='utf-8') as R: + with codecs.open(new_fname, 'w', encoding='utf-8') as W: + found_cd = False + i = -1 + for line in (R if shell != 'fish' else R.read().split('\n')): + i += 1 + if not found_cd: + found_cd = (start_str in line) + continue + if 'true is the last line' in line: + break + line = line.translate({ + ord('\r'): None + }) + if REFS_RE.match(line): + continue + line = line.replace(hostname, 'HOSTNAME') + line = line.replace(user, 'USER') + if pid is not None: + line = line.replace(pid, 'PID') + if shell == 'zsh': + line = line.replace('\033[0m\033[23m\033[24m\033[J', '') + line = ZSH_HL_RE.subn('', line)[0] + elif 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] + '\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 + elif shell == 'ipython' and is_pypy: + try: + end_idx = line.rindex('\033[0m') + try: + idx = line[:end_idx].rindex('\033[1;1H') + except ValueError: + idx = line[:end_idx].rindex('\033[?25h') + line = line[idx + len('\033[1;1H'):] + except ValueError: + pass + try: + data_end_idx = line.rindex('\033[1;1H') + line = line[:data_end_idx] + '\n' + except ValueError: + pass + if line == '\033[1;1H\n': + continue + was_empty = line == '\n' + line = IPYPY_DEANSI_RE.subn('', line)[0] + if line == '\n' and not was_empty: + line = '' + elif shell == 'rc': + if line == 'read() failed: Connection reset by peer\n': + line = '' + elif shell == 'pdb': + if is_pypy: + if line == '\033[?1h\033=\033[?25l\033[1A\n': + line = '' + line = IPYPY_DEANSI_RE.subn('', line)[0] + if line == '\n': + line = '' + if line.startswith(('>',)): + line = '' + elif line == '-> self.quitting = 1\n': + line = '-> self.quitting = True\n' + elif line == '\n': + line = '' + if line == '-> self.quitting = True\n': + break + W.write(line) diff --git a/tests/test_shells/run_script.py b/tests/test_shells/run_script.py new file mode 100755 index 0000000..2eebca1 --- /dev/null +++ b/tests/test_shells/run_script.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import argparse +import os +import re + +from time import sleep +from subprocess import check_call +from io import BytesIO + +import pexpect + + +def get_argparser(ArgumentParser=argparse.ArgumentParser): + parser = ArgumentParser(description='Run powerline shell test using pexpect') + parser.add_argument('--wait-for-echo', action='store_true', help='Wait until the input is echoed back.') + parser.add_argument('--type', metavar='TYPE', help='Test type (daemon, nodaemon, …).') + parser.add_argument('--client', metavar='CLIENT', help='Type of the client used (C, shell, zpython, …).') + parser.add_argument('--shell', metavar='SHELL', help='Shell name.') + parser.add_argument('command', nargs=argparse.REMAINDER, metavar='COMMAND', + help='Command to run and its argument.') + return parser + + +def main(): + test_root = os.environ['TEST_ROOT'] + parser = get_argparser() + args = parser.parse_args() + + shell = args.shell or args.command[0] + test_type = args.type or shell + test_client = args.client or test_type + + log_file_base = '{0}.{1}.{2}'.format(shell, test_type, test_client) + full_log_file_name = os.path.join(test_root, '{0}.full.log'.format(log_file_base)) + + local_paths = [ + os.path.abspath(os.path.join(test_root, 'path')), + os.path.abspath('scripts'), + ] + + if test_type == 'fish': + local_paths += ['/usr/bin', '/bin'] + + python_paths = os.environ.get('PYTHONPATH', '') + if python_paths: + python_paths = ':' + python_paths + python_paths = os.path.abspath('.') + python_paths + + environ = { + 'LANG': 'en_US.UTF-8', + 'PATH': os.pathsep.join(local_paths), + 'TERM': 'screen-256color', + 'DIR1': os.environ['DIR1'], + 'DIR2': os.environ['DIR2'], + 'XDG_CONFIG_HOME': os.path.abspath(os.path.join(test_root, 'fish_home')), + 'IPYTHONDIR': os.path.abspath(os.path.join(test_root, 'ipython_home')), + 'PYTHONPATH': python_paths, + 'POWERLINE_CONFIG_OVERRIDES': os.environ.get('POWERLINE_CONFIG_OVERRIDES', ''), + 'POWERLINE_THEME_OVERRIDES': os.environ.get('POWERLINE_THEME_OVERRIDES', ''), + 'POWERLINE_CONFIG_PATHS': os.path.abspath(os.path.join('powerline', 'config_files')), + 'POWERLINE_COMMAND_ARGS': os.environ.get('POWERLINE_COMMAND_ARGS', ''), + 'POWERLINE_COMMAND': os.environ.get('POWERLINE_COMMAND', ''), + 'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH', ''), + 'TEST_ROOT': test_root, + } + + os.environ['PATH'] = environ['PATH'] + + if test_type == 'daemon': + environ['POWERLINE_SHELL_CONTINUATION'] = '1' + environ['POWERLINE_SHELL_SELECT'] = '1' + + if test_type != 'zpython' and shell == 'zsh': + environ['POWERLINE_NO_ZSH_ZPYTHON'] = '1' + + sio = BytesIO() + + child = pexpect.spawn( + args.command[0], + args.command[1:], + env=environ, + logfile=sio, + timeout=30, + ) + child.expect(re.compile(b'.*')) + sleep(0.5) + child.setwinsize(1, 300) + + with open(os.path.join('tests', 'test_shells', 'inputs', shell), 'rb') as F: + if not args.wait_for_echo: + child.send(F.read()) + else: + for line in F: + child.send(line) + sleep(1) + # TODO Implement something more smart + + with open(full_log_file_name, 'wb') as LF: + while True: + try: + s = child.read_nonblocking(1000) + except pexpect.TIMEOUT: + break + except pexpect.EOF: + break + else: + LF.write(s) + + child.close(force=True) + + check_call([ + os.path.join(test_root, 'path', 'python'), + os.path.join('tests', 'test_shells', 'postproc.py'), + test_type, test_client, shell + ]) + pidfile = os.path.join(test_root, '3rd', 'pid') + if os.path.exists(pidfile): + os.unlink(pidfile) + + +if __name__ == '__main__': + main() diff --git a/tests/test_shells/test.sh b/tests/test_shells/test.sh new file mode 100755 index 0000000..4494302 --- /dev/null +++ b/tests/test_shells/test.sh @@ -0,0 +1,491 @@ +#!/bin/sh +. tests/shlib/common.sh + +enter_suite shell final + +if test $# -eq 0 ; then + FAST=1 +fi +ONLY_SHELL="$1" +ONLY_TEST_TYPE="$2" +ONLY_TEST_CLIENT="$3" + +export PYTHON + +if test "$ONLY_SHELL" = "--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 "$ROOT/tests/test_shells/outputs/${SH}.${TEST_TYPE}.ok" ; then + diff -a -u "$ROOT/tests/test_shells/outputs/${SH}.${TEST_TYPE}.ok" \ + "$TEST_ROOT/${SH}.${TEST_TYPE}.${TEST_CLIENT}.log" + return $? + elif test -e "$ROOT/tests/test_shells/outputs/${SH}.ok" ; then + diff -a -u "$ROOT/tests/test_shells/outputs/${SH}.ok" \ + "$TEST_ROOT/${SH}.${TEST_TYPE}.${TEST_CLIENT}.log" + return $? + else + cat "$TEST_ROOT/${SH}.${TEST_TYPE}.${TEST_CLIENT}.log" + return 1 + fi +} + +# HACK: get newline for use in strings given that "\n" and $'' do not work. +NL="$(printf '\nE')" +NL="${NL%E}" + +print_full_output() { + TEST_TYPE="$1" + TEST_CLIENT="$2" + SH="$3" + echo "Full output:" + echo '============================================================' + cat "$TEST_ROOT/${SH}.${TEST_TYPE}.${TEST_CLIENT}.full.log" + echo + echo '____________________________________________________________' + if test "$POWERLINE_TEST_NO_CAT_V" != "1" ; then + echo "Full output (cat -v):" + echo '============================================================' + cat -v "$TEST_ROOT/${SH}.${TEST_TYPE}.${TEST_CLIENT}.full.log" + echo + echo '____________________________________________________________' + fi +} + +do_run_test() { + TEST_TYPE="$1" + shift + TEST_CLIENT="$1" + shift + SH="$1" + + local wait_for_echo_arg= + if ( \ + test "${SH}" = "dash" \ + || ( \ + test "${SH}" = "pdb" \ + && ( \ + ( \ + test "$PYTHON_VERSION_MAJOR" -eq 3 \ + && test "$PYTHON_VERSION_MINOR" -eq 2 \ + && test "$PYTHON_IMPLEMENTATION" = "CPython" \ + ) \ + || test "$PYTHON_IMPLEMENTATION" = "PyPy" \ + ) \ + ) \ + || ( \ + test "${SH}" = "ipython" \ + && test "$("${PYTHON}" -mIPython --version | head -n1 | cut -d. -f1)" -ge 5 \ + ) \ + ) ; then + wait_for_echo_arg="--wait-for-echo" + fi + "${PYTHON}" tests/test_shells/run_script.py \ + $wait_for_echo_arg --type=${TEST_TYPE} --client=${TEST_CLIENT} --shell=${SH} \ + "$@" + if ! check_screen_log ${TEST_TYPE} ${TEST_CLIENT} ${SH} ; then + echo '____________________________________________________________' + if test "$POWERLINE_TEST_NO_CAT_V" != "1" ; 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 -n "Failed ${SH}. " + print_full_output ${TEST_TYPE} ${TEST_CLIENT} ${SH} + case "${SH}" in + *ksh) + "$TEST_ROOT/path/${SH}" -c 'echo ${KSH_VERSION}' + ;; + dash) + # ? + ;; + busybox) + busybox --help + ;; + *) + "$TEST_ROOT/path/${SH}" --version + ;; + esac + if which dpkg >/dev/null ; then + dpkg -s ${SH} + fi + return 1 + fi + return 0 +} + +run_test() { + TEST_TYPE="$1" + TEST_CLIENT="$2" + SH="$3" + local attempts=3 + if test -n "$ONLY_SHELL$ONLY_TEST_TYPE$ONLY_TEST_CLIENT" ; then + attempts=1 + fi + while test $attempts -gt 0 ; do + rm -f "$TEST_ROOT/${SH}.${TEST_TYPE}.${TEST_CLIENT}.log" + rm -f "$TEST_ROOT/${SH}.${TEST_TYPE}.${TEST_CLIENT}.full.log" + do_run_test "$@" && return 0 + attempts=$(( attempts - 1 )) + done + return 1 +} + +make_test_root + +git init "$TEST_ROOT/3rd" +git --git-dir="$TEST_ROOT/3rd/.git" checkout -b BRANCH +export DIR1="" +export DIR2="" +mkdir "$TEST_ROOT/3rd/$DIR1" +mkdir "$TEST_ROOT/3rd/$DIR2" +mkdir "$TEST_ROOT"/3rd/'\[\]' +mkdir "$TEST_ROOT"/3rd/'%%' +mkdir "$TEST_ROOT"/3rd/'#[bold]' +mkdir "$TEST_ROOT"/3rd/'(echo)' +mkdir "$TEST_ROOT"/3rd/'$(echo)' +mkdir "$TEST_ROOT"/3rd/'`echo`' +mkdir "$TEST_ROOT"/3rd/'«Unicode!»' + +mkdir "$TEST_ROOT/fish_home" +mkdir "$TEST_ROOT/fish_home/fish" +mkdir "$TEST_ROOT/fish_home/fish/generated_completions" +cp -r "$ROOT/tests/test_shells/ipython_home" "$TEST_ROOT" + +mkdir "$TEST_ROOT/path" +ln -s "$(which "${PYTHON}")" "$TEST_ROOT/path/python" +ln -s "$(which env)" "$TEST_ROOT/path" +ln -s "$(which git)" "$TEST_ROOT/path" +ln -s "$(which sleep)" "$TEST_ROOT/path" +ln -s "$(which cat)" "$TEST_ROOT/path" +ln -s "$(which false)" "$TEST_ROOT/path" +ln -s "$(which true)" "$TEST_ROOT/path" +ln -s "$(which kill)" "$TEST_ROOT/path" +ln -s "$(which echo)" "$TEST_ROOT/path" +ln -s "$(which which)" "$TEST_ROOT/path" +ln -s "$(which dirname)" "$TEST_ROOT/path" +ln -s "$(which wc)" "$TEST_ROOT/path" +ln -s "$(which stty)" "$TEST_ROOT/path" +ln -s "$(which cut)" "$TEST_ROOT/path" +ln -s "$(which bc)" "$TEST_ROOT/path" +ln -s "$(which expr)" "$TEST_ROOT/path" +ln -s "$(which mktemp)" "$TEST_ROOT/path" +ln -s "$(which grep)" "$TEST_ROOT/path" +ln -s "$(which sed)" "$TEST_ROOT/path" +ln -s "$(which rm)" "$TEST_ROOT/path" +ln -s "$(which tr)" "$TEST_ROOT/path" +ln -s "$(which uname)" "$TEST_ROOT/path" +ln -s "$(which test)" "$TEST_ROOT/path" +ln -s "$(which pwd)" "$TEST_ROOT/path" +ln -s "$(which hostname)" "$TEST_ROOT/path" +ln -s "$ROOT/tests/test_shells/bgscript.sh" "$TEST_ROOT/path" +ln -s "$ROOT/tests/test_shells/waitpid.sh" "$TEST_ROOT/path" +if which socat ; then + ln -s "$(which socat)" "$TEST_ROOT/path" +fi +for pexe in powerline powerline-config powerline-render powerline.sh powerline.py ; do + if test -e "$ROOT/scripts/$pexe" ; then + ln -s "$ROOT/scripts/$pexe" "$TEST_ROOT/path" + elif test -e client/$pexe ; then + ln -s "$ROOT/client/$pexe" "$TEST_ROOT/path" + elif which $pexe ; then + ln -s "$(which $pexe)" "$TEST_ROOT/path" + else + echo "Executable $pexe was not found" + exit 1 + fi +done + +ln -s python "$TEST_ROOT/path/pdb" +PDB_PYTHON=pdb +ln -s python "$TEST_ROOT/path/ipython" +IPYTHON_PYTHON=ipython + +if test -z "$POWERLINE_RC_EXE" ; then + if which rc-status >/dev/null ; then + # On Gentoo `rc` executable is from OpenRC. Thus app-shells/rc instals + # `rcsh` executable. + POWERLINE_RC_EXE=rcsh + else + POWERLINE_RC_EXE=rc + fi +fi + +if which "$POWERLINE_RC_EXE" >/dev/null ; then + ln -s "$(which $POWERLINE_RC_EXE)" "$TEST_ROOT/path/rc" +fi + +exes="bash zsh busybox tcsh mksh" + +if test "$TRAVIS" != "true" ; then + # For some reason fish does not work on travis + exes="$exes fish" +fi + +# dash has some problems with job control +#exes="$exes dash" + +for exe in $exes ; do + if which $exe >/dev/null ; then + if test "$exe" = "fish" ; then + fish_version="$(fish --version 2>&1)" + fish_version="${fish_version##* }" + fish_version_major="${fish_version%%.*}" + if test "$fish_version_major" != "$fish_version" ; then + # No dot is in development version compiled by bot-ci + fish_version_minor="${fish_version#*.}" + fish_version_patch="${fish_version_minor#*.}" + fish_version_dev="${fish_version_patch#*-}" + if test "$fish_version_dev" = "$fish_version_patch" ; then + fish_version_dev="" + fi + fish_version_minor="${fish_version_minor%%.*}" + fish_version_patch="${fish_version_patch%%-*}" + if test $fish_version_major -lt 2 || ( \ + test $fish_version_major -eq 2 && (\ + test $fish_version_minor -lt 1 || (\ + test $fish_version_minor -eq 1 && + test $fish_version_patch -lt 2 && \ + test -z "$fish_version_dev" + ) \ + ) \ + ) ; then + continue + fi + fi + fi + ln -s "$(which $exe)" "$TEST_ROOT/path" + fi +done + +mkdir "$TEST_ROOT/home" +export HOME="$TEST_ROOT/home" + +unset ENV + +export ADDRESS="powerline-ipc-test-$$" +export PYTHON +echo "Powerline address: $ADDRESS" + +check_test_client() { + local executable="$1" + local client_type="$2" + local actual_mime_type="$( + file --mime-type --brief --dereference "$TEST_ROOT/path/$executable" \ + | cut -d/ -f1 + )" + local expected_mime_type + case "$client_type" in + C) expected_mime_type="application/x-executable" ;; + python) expected_mime_type="text/x-python" ;; + render) expected_mime_type="text/x-python" ;; + shell) expected_mime_type="text/x-shellscript" ;; + esac + expected_mime_type="${expected_mime_type%/*}" + if test "$expected_mime_type" != "$actual_mime_type" ; then + fail "MIME-$executable" "M" "Expected $executable to have MIME type $expected_mime_type, but got $actual_mime_type" + fi +} + +if ( \ + test -z "${ONLY_SHELL}" \ + || test "${ONLY_SHELL%sh}" != "${ONLY_SHELL}" \ + || test "${ONLY_SHELL}" = "busybox" \ + || test "${ONLY_SHELL}" = "rc" \ +) ; then + scripts/powerline-config shell command + + for TEST_TYPE in "daemon" "nodaemon" ; do + if test -n "$ONLY_TEST_TYPE" && test "$ONLY_TEST_TYPE" != "$TEST_TYPE" + then + continue + fi + if test "$FAST" = 1 ; 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 $$ > "$TEST_ROOT/daemon_pid" + exec "$PYTHON" ./scripts/powerline-daemon -s"$ADDRESS" -f >"$TEST_ROOT/daemon_log" 2>&1 + ' & + fi + echo "> Testing $TEST_TYPE" + I=-1 + for POWERLINE_COMMAND in \ + powerline \ + powerline-render \ + powerline.py \ + 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 + check_test_client "$POWERLINE_COMMAND" $TEST_CLIENT + if test "$TEST_CLIENT" = render && test "$TEST_TYPE" = daemon ; then + continue + fi + I="$(( I + 1 ))" + if test "$TEST_CLIENT" = "C" && ! test -x "$ROOT/scripts/powerline" + then + if which powerline >/dev/null ; then + POWERLINE_COMMAND=powerline + else + continue + fi + fi + if ( \ + test "$TEST_CLIENT" = "shell" \ + && ! test -x "$TEST_ROOT/path/socat" \ + ) ; then + continue + fi + if ( \ + test -n "$ONLY_TEST_CLIENT" \ + && test "$TEST_CLIENT" != "$ONLY_TEST_CLIENT" \ + ) ; then + continue + fi + export POWERLINE_COMMAND_ARGS="--socket $ADDRESS" + export POWERLINE_COMMAND="$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" \ + "rc -i -p" + do + J="$(( J + 1 ))" + if test "$FAST" = 1 ; then + if test $(( (I + J) % $VARIANTS )) -ne $EXETEST ; then + continue + fi + fi + SH="${TEST_COMMAND%% *}" + if test -n "$ONLY_SHELL" && test "$ONLY_SHELL" != "$SH" ; then + continue + fi + if ! test -x "$TEST_ROOT/path/$SH" ; then + continue + fi + echo ">>> $(readlink "$TEST_ROOT/path/$SH")" + if ! run_test $TEST_TYPE $TEST_CLIENT $TEST_COMMAND ; then + fail "$SH-$TEST_TYPE-$TEST_CLIENT:test" F \ + "Failed checking $TEST_COMMAND" + fi + done + done + if test $TEST_TYPE = daemon ; then + "$PYTHON" ./scripts/powerline-daemon -s"$ADDRESS" -k + wait $(cat "$TEST_ROOT/daemon_pid") + if ! test -z "$(cat "$TEST_ROOT/daemon_log")" ; then + echo '____________________________________________________________' + echo "Daemon log:" + echo '============================================================' + cat "$TEST_ROOT/daemon_log" + fail "$SH-$TEST_TYPE-$TEST_CLIENT:log" E \ + "Non-empty daemon log for ${TEST_COMMAND}" + fi + fi + done +fi + +if "$PYTHON" scripts/powerline-daemon -s"$ADDRESS" \ + > "$TEST_ROOT/daemon_log_2" 2>&1 +then + sleep 1 + "$PYTHON" scripts/powerline-daemon -s"$ADDRESS" -k +else + fail "daemon:run" F "Daemon exited with status $?" +fi + +if ! test -z "$(cat "$TEST_ROOT/daemon_log_2")" ; then + echo '____________________________________________________________' + echo "Daemon log (2nd):" + echo '============================================================' + cat "$TEST_ROOT/daemon_log_2" + fail "daemon:log" E "Daemon run with non-empty log" +fi + +if ( test -z "${ONLY_SHELL}" || test "${ONLY_SHELL}" = "zsh" ) \ + && ( test -z "${ONLY_TEST_TYPE}" || test "${ONLY_TEST_TYPE}" = "zpython" ) \ + && "$TEST_ROOT/path/zsh" "$ROOT/tests/test_shells/zsh_test_script.zsh" +then + echo "> zpython" + if ! run_test zpython zpython zsh -f -i ; then + fail "zsh-zpython:test" F "Failed checking zsh -f -i" + fi +fi + +if test -z "${ONLY_SHELL}" || test "${ONLY_SHELL}" = "pdb" ; then + if test "$PYTHON_IMPLEMENTATION" != "PyPy" ; then + if test -z "${ONLY_TEST_TYPE}" || test "${ONLY_TEST_TYPE}" = "subclass" + then + echo "> pdb subclass" + if ! run_test subclass python $PDB_PYTHON \ + "$ROOT/tests/test_shells/pdb-main.py" + then + fail --allow-failure "pdb-subclass:test" F \ + "Failed checking $PDB_PYTHON $ROOT/tests/test_shells/pdb-main.py" + fi + fi + if test -z "${ONLY_TEST_TYPE}" || test "${ONLY_TEST_TYPE}" = "module" ; then + echo "> pdb module" + MODULE="powerline.bindings.pdb" + if test "$PYTHON_MM" = "2.6" ; then + MODULE="powerline.bindings.pdb.__main__" + fi + if ! run_test module python "$PDB_PYTHON" -m"$MODULE" \ + "$ROOT/tests/test_shells/pdb-script.py" + then + fail --allow-failure "pdb-module:test" F \ + "Failed checking $PDB_PYTHON -m$MODULE $ROOT/tests/test_shells/pdb-script" + fi + fi + fi +fi + +if test -z "${ONLY_SHELL}" || test "${ONLY_SHELL}" = "ipython" ; then + if "${PYTHON}" -c "try: import IPython${NL}except ImportError: raise SystemExit(1)" ; then + # Define some overrides which should be ignored by IPython. + export POWERLINE_CONFIG_OVERRIDES='common.term_escape_style=fbterm' + export POWERLINE_THEME_OVERRIDES='in.segments.left=[]' + echo "> ipython" + if ! run_test ipython ipython ${IPYTHON_PYTHON} -mIPython ; then + # Do not allow ipython tests to spoil the build + fail --allow-failure "ipython:test" F "Failed checking ${IPYTHON_PYTHON} -mIPython" + fi + unset POWERLINE_THEME_OVERRIDES + unset POWERLINE_CONFIG_OVERRIDES + fi +fi + +exit_suite diff --git a/tests/test_shells/waitpid.sh b/tests/test_shells/waitpid.sh new file mode 100755 index 0000000..8d98e21 --- /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_test_script.zsh b/tests/test_shells/zsh_test_script.zsh new file mode 100644 index 0000000..3957f56 --- /dev/null +++ b/tests/test_shells/zsh_test_script.zsh @@ -0,0 +1,11 @@ +set -e +set -x +. tests/bot-ci/scripts/common/main.sh +zmodload zpython || zmodload libzpython +zpython 'import zsh' +zpython 'import platform' +zpython 'zsh.setvalue("ZSH_PYTHON_VERSION", platform.python_version())' +zpython 'zsh.setvalue("ZSH_PYTHON_IMPLEMENTATION", platform.python_implementation())' + +[[ $ZSH_PYTHON_IMPLEMENTATION = $PYTHON_IMPLEMENTATION ]] +[[ $ZSH_PYTHON_VERSION = $PYTHON_VERSION ]] diff --git a/tests/test_vim/pyfiles/setup_statusline_catcher.py b/tests/test_vim/pyfiles/setup_statusline_catcher.py new file mode 100644 index 0000000..014a2e9 --- /dev/null +++ b/tests/test_vim/pyfiles/setup_statusline_catcher.py @@ -0,0 +1,18 @@ +# vim:fileencoding=utf-8:noet +import json + +import vim + +from powerline.lib.unicode import u + + +_powerline_old_render = powerline.render # NOQA + + +def _powerline_test_render_function(*args, **kwargs): + ret = _powerline_old_render(*args, **kwargs) + vim.eval('add(g:statusline_values, %s)' % json.dumps(u(ret))) + return ret + + +powerline.render = _powerline_test_render_function # NOQA diff --git a/tests/test_vim/test.sh b/tests/test_vim/test.sh new file mode 100755 index 0000000..ecd0285 --- /dev/null +++ b/tests/test_vim/test.sh @@ -0,0 +1,60 @@ +#!/bin/sh +. tests/shlib/common.sh +. tests/shlib/vterm.sh +. tests/shlib/vim.sh + +enter_suite vim final + +vterm_setup vim + +# Define some overrides. These ones must be ignored and do not affect Vim +# status/tab lines. +export POWERLINE_CONFIG_OVERRIDES='common.default_top_theme=ascii' +export POWERLINE_THEME_OVERRIDES='default.segments.left=[]' + +test_script() { + local vim="$1" ; shift + local script="$1" ; shift + local allow_failure_arg="$1" ; shift + echo "Running script $script with $vim" + if ! test -e "$vim" ; then + return 0 + fi + if ! script="$script" "$vim" -u NONE -c 'source $script' \ + || test -f message.fail + then + local test_name="${script##*/}" + fail $allow_failure_arg "${test_name%.vim}" \ + F "Failed script $script run with $vim" + if test -e message.fail ; then + cat message.fail >&2 + rm message.fail + fi + fi +} + +TEST_SCRIPT_ROOT="$ROOT/tests/test_vim/tests" + +cd "$TEST_ROOT" + +for script in "$TEST_SCRIPT_ROOT"/*.vim ; do + if test "${script%.old.vim}" = "${script}" ; then + test_script "$NEW_VIM" "$script" "" + fi +done + +if test "$PYTHON_VERSION_MAJOR.$PYTHON_VERSION_MINOR" = "2.7" ; then + ALLOW_FAILURE_ARG=--allow-failure +else + ALLOW_FAILURE_ARG= +fi + +if test -e "$OLD_VIM" ; then + for script in "$TEST_SCRIPT_ROOT"/*.old.vim ; do + test_script "$OLD_VIM" "$script" "$ALLOW_FAILURE_ARG" + done +fi + +vterm_shutdown + +exit_suite diff --git a/tests/test_vim/tests/commandt_plugin.vim b/tests/test_vim/tests/commandt_plugin.vim new file mode 100755 index 0000000..9f944b9 --- /dev/null +++ b/tests/test_vim/tests/commandt_plugin.vim @@ -0,0 +1,17 @@ +#!/usr/bin/vim -S +set nocompatible +set columns=80 +execute 'source' fnameescape(expand(':p:h:h').'/vim_utils.vim') +call EnablePlugins('command-t') +call SourcePowerline() +let g:statusline_values = [] +call PyFile('setup_statusline_catcher') +execute 'CommandTBuffer'|call feedkeys("\") +call RunPython('powerline.render = _powerline_old_render') +let g:expected_statusline = '%#Pl_231_16777215_240_5789784_bold# Command-T %#Pl_231_16777215_240_5789784_NONE# %#Pl_231_16777215_240_5789784_bold#BufferFinder %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                                    ' +call CheckMessages() +if index(g:statusline_values, g:expected_statusline) == -1 + call CheckStatuslineValue(get(g:statusline_values, -1, ''), g:expected_statusline) + cquit +endif +qall diff --git a/tests/test_vim/tests/empty_encoding.old.vim b/tests/test_vim/tests/empty_encoding.old.vim new file mode 100755 index 0000000..124a10a --- /dev/null +++ b/tests/test_vim/tests/empty_encoding.old.vim @@ -0,0 +1,32 @@ +#!/usr/bin/vim -S +if has('multi_byte') + if empty(&encoding) + call writefile(['&encoding option value is empty, even though Vim has +multibyte'], 'message.fail') + cquit + endif + qall +endif +if !empty(&encoding) + call writefile(['&encoding option value is not empty, even though Vim does not have +multibyte'], 'message.fail') + cquit +endif + +let g:powerline_config_paths = [expand(':p:h:h:h:h') . '/powerline/config_files'] + +try + source :p:h:h:h:h/powerline/bindings/vim/plugin/powerline.vim +catch + call writefile(['Unexpected exception:', v:exception], 'message.fail') + cquit +endtry +set ls=2 +redrawstatus! +redir => g:messages + messages +redir END +let mess=split(g:messages, "\n") +if len(mess)>1 + call writefile(['Unexpected message(s):']+mess, 'message.fail') + cquit +endif +qall! diff --git a/tests/test_vim/tests/foreign_stl_override.vim b/tests/test_vim/tests/foreign_stl_override.vim new file mode 100644 index 0000000..2a5b8c0 --- /dev/null +++ b/tests/test_vim/tests/foreign_stl_override.vim @@ -0,0 +1,22 @@ +scriptencoding utf-8 +set encoding=utf-8 +let g:powerline_config_paths = [expand(':p:h:h:h:h') . '/powerline/config_files'] +set laststatus=2 +redir => g:messages + try + source :p:h:h:h:h/powerline/bindings/vim/plugin/powerline.vim + redrawstatus! + vsplit + redrawstatus! + setlocal statusline=«» + redrawstatus! + catch + call writefile(['Unexpected exception', v:exception], 'message.fail') + cquit + endtry +redir END +if g:messages =~# '\v\S' + call writefile(['Unexpected messages'] + split(g:messages, "\n", 1), 'message.fail') + cquit +endif +qall! diff --git a/tests/test_vim/tests/invalid_unicode.vim b/tests/test_vim/tests/invalid_unicode.vim new file mode 100644 index 0000000..ac91f3c --- /dev/null +++ b/tests/test_vim/tests/invalid_unicode.vim @@ -0,0 +1,19 @@ +set encoding=utf-8 +let g:powerline_config_paths = [expand(':p:h:h:h:h') . '/powerline/config_files'] +set laststatus=2 +set showtabline=2 +edit `="\xFF"` +redir => g:messages + try + source :p:h:h:h:h/powerline/bindings/vim/plugin/powerline.vim + redrawstatus! + catch + call writefile(['Unexpected exception', v:exception], 'message.fail') + cquit + endtry +redir END +if g:messages =~# '\v\S' + call writefile(['Unexpected messages'] + split(g:messages, "\n", 1), 'message.fail') + cquit +endif +qall! diff --git a/tests/test_vim/tests/local_overrides.vim b/tests/test_vim/tests/local_overrides.vim new file mode 100755 index 0000000..aba14e2 --- /dev/null +++ b/tests/test_vim/tests/local_overrides.vim @@ -0,0 +1,48 @@ +#!/usr/bin/vim -S +set encoding=utf-8 +let g:powerline_config_paths = [expand(':p:h:h: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_vim/tests/nerdtree_plugin.vim b/tests/test_vim/tests/nerdtree_plugin.vim new file mode 100755 index 0000000..761cb5f --- /dev/null +++ b/tests/test_vim/tests/nerdtree_plugin.vim @@ -0,0 +1,11 @@ +#!/usr/bin/vim -S +set nocompatible +set columns=80 +execute 'source' fnameescape(expand(':p:h:h').'/vim_utils.vim') +call EnablePlugins('nerdtree') +call SourcePowerline() +NERDTree /home +redrawstatus +call CheckCurrentStatusline('%#Pl_231_16777215_240_5789784_bold# /home %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                      ') +call CheckMessages() +qall diff --git a/tests/test_vim/tests/plugin_file.vim b/tests/test_vim/tests/plugin_file.vim new file mode 100755 index 0000000..1848933 --- /dev/null +++ b/tests/test_vim/tests/plugin_file.vim @@ -0,0 +1,22 @@ +#!/usr/bin/vim -S +set encoding=utf-8 +let g:powerline_config_paths = [expand(':p:h:h:h:h') . '/powerline/config_files'] +tabedit abc +tabedit def +try + source :p:h:h:h:h/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_vim/tests/tabline.vim b/tests/test_vim/tests/tabline.vim new file mode 100755 index 0000000..ff76dc0 --- /dev/null +++ b/tests/test_vim/tests/tabline.vim @@ -0,0 +1,56 @@ +#!/usr/bin/vim -S +set encoding=utf-8 +let g:powerline_config_paths = [expand(':p:h:h:h:h') . '/powerline/config_files'] +source :p:h:h:h:h/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# '%1T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc %#Pl_244_8421504_236_3158064_NONE# %2T%#Pl_247_10395294_236_3158064_NONE#2 ./def %#Pl_236_3158064_240_5789784_NONE# %3T%#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %T%#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Tabs ' + call writefile(['Unexpected tabline', result], 'message.fail') + cquit +endif + +tabonly! + +try + let result = eval(&tabline[2:]) +catch + call writefile(['Exception while evaluating &tabline (2)', v:exception], 'message.fail') + cquit +endtry + +if result isnot# '%T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc %#Pl_244_8421504_236_3158064_NONE# %#Pl_247_10395294_236_3158064_NONE#2 ./def %#Pl_236_3158064_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Bufs ' + call writefile(['Unexpected tabline (2)', result], 'message.fail') + cquit +endif + +try + vsplit + let result = eval(&tabline[2:]) +catch + call writefile(['Exception while evaluating &tabline (3)', v:exception], 'message.fail') +endtry + +if result isnot# '%T%#Pl_247_10395294_236_3158064_NONE# 1 ./abc %#Pl_244_8421504_236_3158064_NONE# %#Pl_247_10395294_236_3158064_NONE#2 ./def %#Pl_236_3158064_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#3 ./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                         %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Bufs ' + call writefile(['Unexpected tabline (3)', result], 'message.fail') + cquit +endif + +redir END +if g:messages =~ '\S' + call writefile(['Non-empty messages:', g:messages], 'message.fail') + cquit +endif + +qall! diff --git a/tests/test_vim/vim_utils.vim b/tests/test_vim/vim_utils.vim new file mode 100644 index 0000000..6219ec4 --- /dev/null +++ b/tests/test_vim/vim_utils.vim @@ -0,0 +1,88 @@ +let g:powerline_use_var_handler = 1 + +let g:pyfiles_root=expand(':p:h').'/pyfiles' +let g:root=expand(':p:h:h:h') +let g:mf=fnamemodify('message.fail', ':p') + +command -nargs=1 LST :call writefile(, g:mf, 'a') | cquit +command -nargs=1 ERR :LST [] +command -nargs=1 EXC :ERR 'Unexpected exception', , v:exception, v:throwpoint + +function EnablePlugins(...) + let &runtimepath = join(map(copy(a:000), 'escape(g:root."/tests/vim-plugins/".v:val, "\\,")'), ',') + try + runtime! plugin/*.vim + silent doautocmd BufWinEnter + silent doautocmd BufEnter + silent doautocmd VimEnter + catch + EXC EnablePlugins + endtry +endfunction +function RecordStatusline() + let g:statusline = &l:statusline + if g:statusline[:1] is# '%!' + let g:statusline_value=eval(g:statusline[2:]) + else + ERR 'Statusline does not start with %!', g:statusline + endif + return '' +endfunction +function SourcePowerline() + let g:powerline_config_paths = [g:root . '/powerline/config_files'] + try + execute 'source' fnameescape(g:root . '/powerline/bindings/vim/plugin/powerline.vim') + catch + EXC SourcePowerline + endtry +endfunction +function NDiff(actual, expected) + return systemlist(shellescape(g:root.'/tests/bot-ci/scripts/ndiff-strings.py').' '.shellescape(a:actual).' '.shellescape(a:expected)) +endfunction +function CheckStatuslineValue(actual, expected) + if a:actual isnot# a:expected + LST ['Expected different statusline value', a:actual, a:expected] + NDiff(a:actual, a:expected) + endif +endfunction +function CheckRecordedStatuslineValue(expected) + return CheckStatuslineValue(g:statusline_value, a:expected) +endfunction +function GetCurrentStatusline() + if &l:statusline[:1] isnot# '%!' + ERR 'Statusline does not start with %!', &l:statusline + endif + return eval(&l:statusline[2:]) +endfunction +function CheckCurrentStatusline(expected) + return CheckStatuslineValue(GetCurrentStatusline(), a:expected) +endfunction +function CheckMessages() + if !empty(g:powerline_log_messages) + LST ['Unexpected messages in log'] + g:powerline_log_messages + endif + redir => mes + messages + redir END + let mesl = split(mes, "\n")[1:] + if !empty(mesl) + LST ['Unexpected messages'] + split(mes, "\n", 1) + endif +endfunction +function RunPython(s) + if has('python') + execute 'python' a:s + else + execute 'python3' a:s + endif +endfunction +function PyFile(f) + if has('python') + execute 'pyfile' fnameescape(g:pyfiles_root.'/'.a:f.'.py') + else + execute 'py3file' fnameescape(g:pyfiles_root.'/'.a:f.'.py') + endif +endfunction + +for s:c in ['noremap', 'noremap!'] + execute s:c '' '(PowerlineTestRecordStatusline)' 'RecordStatusline()' +endfor diff --git a/tests/vim_sys_path/vim.py b/tests/vim_sys_path/vim.py new file mode 100644 index 0000000..e9dba66 --- /dev/null +++ b/tests/vim_sys_path/vim.py @@ -0,0 +1,7 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import tests.modules.vim as vim + + +globals().update(vim._init()) diff --git a/tools/colors.map b/tools/colors.map new file mode 100644 index 0000000..61d8c4a --- /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 0000000..cf66ef9 --- /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 0000000..290e75e --- /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) diff --git a/tools/purge-PRs.py b/tools/purge-PRs.py new file mode 100755 index 0000000..38f44d9 --- /dev/null +++ b/tools/purge-PRs.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import argparse + +from getpass import getpass + +from github import Github + + +p = argparse.ArgumentParser(description='Powerline release script') +p.add_argument('-u', '--user', type=str, metavar='USER', help='Github username.', required=True) +p.add_argument('-p', '--password', type=str, metavar='PASS', help='Github password. You will be prompted if it is not supplied.') + +if __name__ == '__main__': + args = p.parse_args() + user = args.user + password = args.password or getpass('Password for {0}: '.format(user)) + gh = Github(user, password) + grepo = gh.get_repo('powerline/powerline') + for pr in grepo.get_pulls(): + if pr.base.ref != 'develop': + issue = grepo.get_issue(pr.number) + issue.create_comment('PRs to any branch, but develop, are not accepted.', ) + issue.add_to_labels('s:invalid') + issue.edit(state='closed') -- cgit v1.2.3