diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..64fcd5f3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +*.swp +.git/ +.gitignore +.travis.yml +CHANGELOG.md +Dockerfile +LICENSE +README.md +bash-powerline-screenshot.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ec4610be --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +powerline-shell.py +*.py[co] +config.py +powerline_shell.egg-info/ +/build/ +/dist/ +tags +config.json +powerline-shell.json +.DS_Store +*sublime* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..db47f9ca --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +sudo: required +services: + - docker +script: ./test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..6d9bb97c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,265 @@ +# Changes + +2018-09-15 (version 0.7.0) + +* New generic `stdout` and `env` segments + +2018-06-20 (version 0.6.0) + +* Support for custom themes +* New option for the `time` segment to specify format of the displayed time + ([@dundalek](https://github.com/b-ryan/powerline-shell/pull/383)) + +2018-04-22 (version 0.5.4) + +* Reverted fix for + ([#249](https://github.com/b-ryan/powerline-shell/issues/249)) because it + caused issues on Mac. + +2018-04-21 (version 0.5.3) + +* New theme! (gruvbox) + ([@monicaycli](https://github.com/b-ryan/powerline-shell/pull/388)) + +2018-04-21 (version 0.5.2) + +* Fix hostname colorize bug + ([@comagnaw](https://github.com/b-ryan/powerline-shell/issues/353)) +* Fix issue with prompt bleeding behavior + ([@bytebeast](https://github.com/b-ryan/powerline-shell/issues/249)) +* Better error message when config file cannot be decoded (Closes + [#371](https://github.com/b-ryan/powerline-shell/issues/371)) + +2018-04-13 (version 0.5.1) + +* Fix Python 3 compatibility of `git_stash` segment + +2018-04-10 (version 0.5.0) + +* Patch environment for VCS subprocesses rather than generating a new one +* Fix `cwd` segment so it respects `max_depth` configuration +* Fix Ruby segment for Python 3 compatibility + ([@Blue-Dog-Archolite](https://github.com/b-ryan/powerline-shell/pull/366)) +* Configuration is now expected to be at + `~/.config/powerline-shell/config.json` ([@emansije and + @kc9jud](https://github.com/b-ryan/powerline-shell/pull/334)) +* New `git_stash` segment + ([@apinkney97](https://github.com/b-ryan/powerline-shell/pull/379)) + +2018-02-19 (version 0.4.9) + +* Fix root user segment + ([@TiGR](https://github.com/b-ryan/powerline-shell/pull/362)) +* Fixes and enhancements for SVN segment + ([@emansije](https://github.com/b-ryan/powerline-shell/pull/349)) + +2018-01-29 (version 0.4.8) + +* Bring back the ability to create custom themes + ([@b-ryan](https://github.com/b-ryan/powerline-shell/pull/352)) +* Add the ability to customize the `time` segment in themes + ([@gaurav-nelson](https://github.com/b-ryan/powerline-shell/pull/338)) + +2018-01-15 (version 0.4.7) + +* VCS segments (git, hg, etc.) can now show a symbol identifying what VPS the + current directory uses + ([@emansije](https://github.com/b-ryan/powerline-shell/pull/298)) + +2018-01-11 (version 0.4.6) + +* Fix bug in SVN segment + ([@kc9jud](https://github.com/b-ryan/powerline-shell/pull/347)) + +2017-12-20 (version 0.4.5) + +* Fix `cwd` segment in Bash for Windows + ([@b-ryan](https://github.com/b-ryan/powerline-shell/pull/340)) + +2017-12-19 (version 0.4.4) + +* New options and symbol for the `battery` segment + ([@kc9jud](https://github.com/b-ryan/powerline-shell/pull/332)) +* Update `svn` segment to use threads + ([@kc9jud](https://github.com/b-ryan/powerline-shell/pull/333)) +* Fix `username` segment in cygwin + ([@ericLemanissier](https://github.com/b-ryan/powerline-shell/commits/master)) + +2017-11-25 (version 0.4.3) + +* New option for `cwd` segment that allows the last directory to not be + shortened when `max_dir_size` is used + ([@jceaser](https://github.com/banga/powerline-shell/pull/321)). + +2017-10-16 (version 0.4.2) + +* VCS segments will use ASCII `?` instead of a unicode symbol for new files. + +2017-10-16 (version 0.4.1) + +* Fix cwd bug when `$HOME` ends in a slash + ([@tbodt](https://github.com/banga/powerline-shell/pull/309)) +* Use docker to run tests + ([@aa8y](https://github.com/banga/powerline-shell/pull/297)) + +2017-10-06 (version 0.4.0) + +* tcsh support + +2017-09-30 (version 0.3.1) + +* Fix username segment's background color after "su" command + ([@Fak3](https://github.com/banga/powerline-shell/pull/175)) +* New `battery` segment which shows the percentage your battery is charged and + an icon when your battery is charging. + ([@wattengard](https://github.com/banga/powerline-shell/pull/204)) +* New `aws_profile` segment which shows which AWS profile you are using. + ([@bryangrimes](https://github.com/banga/powerline-shell/pull/223)) + +2017-09-30 (version 0.3.0) + +* Redo Fossil segment to be consistent with git, svn, etc. + ([@emansije](https://github.com/banga/powerline-shell/pull/286)) +* Fix subshell execution in bash described by + [pw3nage](https://github.com/njhartwell/pw3nage) + ([@b-ryan](https://github.com/banga/powerline-shell/pull/282)) +* Change SSH segment to just use the text `SSH` instead of showing a lock + symbol. Closes [#287](https://github.com/banga/powerline-shell/issues/287). + +2017-09-18 (version 0.2.2) + +* Fix python3 issue in uptime segment. Fixes + [#291](https://github.com/banga/powerline-shell/issues/291). + +2017-09-16 (version 0.2.1) + +* Fix issues preventing fish shell from rendering. + +2017-09-13 (version 0.2.0) + +* Add Bazaar segment + ([@emansije](https://github.com/banga/powerline-shell/pull/283)) + * And rename properties of RepoStats for clarity + ([@emansije](https://github.com/banga/powerline-shell/pull/284)) +* Rewrite SVN segment to be consistent with git +* Remove duplicate function in colortrans.py + ([@jmtd](https://github.com/banga/powerline-shell/pull/273)) +* Make python 3 check compatible with older Python versions +* New theme! `solarized_light` + ([@ruturajv](https://github.com/banga/powerline-shell/pull/143) + +2017-09-10 + +* Complete overhaul of the project + ([@b-ryan](https://github.com/banga/powerline-shell/pull/280)) + * There is now a PyPi package + * It's significantly faster now + * Configuration and installation is brand new. See README.md + +2017-06-21 + +* Add `rbenv` segment + ([@dogo](https://github.com/banga/powerline-shell/pull/260)) +* Fix path segment so that current directory is emphasized + ([@inamiy](https://github.com/banga/powerline-shell/pull/235)) + +2017-06-20 + +* Add `newline` segment + ([@ffried](https://github.com/banga/powerline-shell/pull/266)) +* Add `npm_version` segment + ([@WileESpaghetti](https://github.com/banga/powerline-shell/pull/265)) +* Fix issue with conda environments + ([@drorata](https://github.com/banga/powerline-shell/pull/257)) +* Fix jobs segment for Cygwin + ([@themiwi](https://github.com/banga/powerline-shell/pull/256)) + +2017-05-15 + +* Fix the `set_term_title` segment for ZSH + ([@themiwi](https://github.com/banga/powerline-shell/pull/255)) + +2016-04-16 + +* Fix issue around unicode function for python 3 + +2016-04-01 + +* Refactor of the way the git segment manages data about git's state. + ([@b-ryan](https://github.com/milkbikis/powerline-shell/pull/221)) + +2015-12-26 + +* Beginnings of unit testing for segments. Included in this change was a + refactor of the way segments are added to powerline. Now, instead of looking + for a global `powerline` object, `powerline` is passed into the function to + add the segment. Segments will also no longer add the segments by calling the + `add` function themselves. + ([@b-ryan](https://github.com/milkbikis/powerline-shell/pull/212)) +* Python3 fixes for `lib/color_compliment.py`. + ([@ceholden](https://github.com/milkbikis/powerline-shell/pull/220)) + +2015-11-25 + +* `virtual_env` segment now supports environments made with `conda` + ([@ceholden](https://github.com/milkbikis/powerline-shell/pull/198)) + +2015-11-21 + +* Fixes for Python 3 compatibility + ([@b-ryan](https://github.com/milkbikis/powerline-shell/pull/211)) + +2015-11-18 + +* The git segment has gotten a makeover + ([@MartinWetterwald](https://github.com/milkbikis/powerline-shell/pull/136)) +* Fix git segment when git is not on the standard PATH + ([@andrejgl](https://github.com/milkbikis/powerline-shell/pull/153)) +* Fix `--cwd-max-depth` showing duplicates when it's <= 2 + ([@b-ryan](https://github.com/milkbikis/powerline-shell/pull/209)) +* Add padding around `exit_code` segment + ([@phatblat](https://github.com/milkbikis/powerline-shell/pull/205)) + +2015-10-02 + +* New option (`--cwd-max-dir-size`) which allows you to limit each directory + that is displayed to a number of characters. This currently does not apply + if you are using `--cwd-mode plain`. + ([@mart-e](https://github.com/milkbikis/powerline-shell/pull/127)) + +2015-08-26 + +* New `plain` mode of displaying the current working directory which can be + used by adding `--cwd-only plain` to `powerline-shell.py`. + This deprecates the `--cwd-only` option. `--cwd-mode dironly` can be used + instead. ([@paol](https://github.com/milkbikis/powerline-shell/pull/156)) + +2015-08-18 + +* New `time` segment + ([@filipebarros](https://github.com/milkbikis/powerline-shell/pull/107)) + +2015-08-01 + +* Use `print` function for some python3 compatibility + ([@strycore](https://github.com/milkbikis/powerline-shell/pull/195)) + +2015-07-31 + +* The current working directory no longer follows symbolic links +* New `exit_code` segment + ([@disruptek](https://github.com/milkbikis/powerline-shell/pull/129)) + +2015-07-30 + +* Fix ZSH root indicator + ([@nkcfan](https://github.com/milkbikis/powerline-shell/pull/150)) +* Add uptime segment + ([@marcioAlmada](https://github.com/milkbikis/powerline-shell/pull/139)) + +2015-07-27 + +* Use `python2` instead of `python` in hashbangs + ([@Undeterminant](https://github.com/milkbikis/powerline-shell/pull/100)) +* Add `node_version` segment + ([@mmilleruva](https://github.com/milkbikis/powerline-shell/pull/189)) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..71481322 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM python:2-alpine + +MAINTAINER github.com/b-ryan/powerline-shell + +USER root +RUN apk add --no-cache --update \ + bzr \ + fossil \ + git \ + mercurial \ + php5 \ + subversion \ + && \ + rm -rf /var/cache/apk/* + +RUN mkdir /code +WORKDIR /code +COPY requirements-dev.txt . +RUN pip install -r requirements-dev.txt && \ + rm requirements-dev.txt + +RUN bzr whoami "root " && \ + git config --global user.email "root@example.com" && \ + git config --global user.name "root" + +# COPY . ./ +# RUN ./setup.py install + +ENV USER root + +CMD ["nosetests"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..271cb592 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Shrey Banga and contributors + +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/README.md b/README.md index 7a922b7c..f7d274b8 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,347 @@ -Powerline style prompt for Bash (and now, ZSH) -============================================== +# A Powerline style prompt for your shell -A [Powerline](https://github.com/Lokaltog/vim-powerline) like prompt for Bash/ZSH: +A beautiful and useful prompt generator for Bash, ZSH, Fish, and tcsh: -![MacVim+Solarized+Powerline+CtrlP](https://raw.github.com/milkbikis/dotfiles-mac/master/bash-powerline-screenshot.png) +![MacVim+Solarized+Powerline+CtrlP](https://raw.github.com/b-ryan/powerline-shell/master/bash-powerline-screenshot.png) -* Shows some important details about the git branch: - * Displays the current git branch which changes background color when the branch is dirty - * A '+' appears when untracked files are present - * When the local branch differs from the remote, the difference in number of commits is shown along with '⇡' or '⇣' indicating whether a git push or pull is pending -* Changes color if the last command exited with a failure code -* If you're too deep into a directory tree, shortens the displayed path with an ellipsis -* Shows the current Python [virtualenv](http://www.virtualenv.org/) environment -* It's all done in a Python script, so you could go nuts with it +- Shows some important details about the git/svn/hg/fossil branch (see below) +- Changes color if the last command exited with a failure code +- If you're too deep into a directory tree, shortens the displayed path with an ellipsis +- Shows the current Python [virtualenv](http://www.virtualenv.org/) environment +- It's easy to customize and extend. See below for details. -# Setup +The generated prompts are designed to resemble +[powerline](https://github.com/powerline/powerline), but otherwise this project +has no relation to powerline. -* This script uses ANSI color codes to display colors in a terminal. These are notoriously non-portable, so may not work for you out of the box, but try setting your $TERM to xterm-256color, because that works for me. + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -* Patch the font you use for your terminal: see https://github.com/Lokaltog/vim-powerline/wiki/Patched-fonts +- [Version Control](#version-control) +- [Setup](#setup) + - [Bash](#bash) + - [ZSH](#zsh) + - [Fish](#fish) + - [tcsh](#tcsh) +- [Customization](#customization) + - [Config File](#config-file) + - [Adding, Removing and Re-arranging segments](#adding-removing-and-re-arranging-segments) + - [Generic Segments](#generic-segments) + - [Segment Separator](#segment-separator) + - [Themes](#themes) + - [Segment Configuration](#segment-configuration) + - [Contributing new types of segments](#contributing-new-types-of-segments) +- [Troubleshooting](#troubleshooting) -* Clone this repository somewhere: + - git clone https://github.com/milkbikis/powerline-shell +## Version Control -* Create a symlink to the python script in your home: +All of the version control systems supported by powerline shell give you a +quick look into the state of your repo: - ln -s ~/powerline-shell.py +- The current branch is displayed and changes background color when the + branch is dirty. +- When the local branch differs from the remote, the difference in number + of commits is shown along with `⇡` or `⇣` indicating whether a git push + or pull is pending. - If you don't want the symlink, just modify the path in the commands below +If files are modified or in conflict, the situation is summarized with the +following symbols: -* Now add the following to your .bashrc: +- `✎` -- a file has been modified (but not staged for commit, in git) +- `✔` -- a file is staged for commit (git) or added for tracking +- `✼` -- a file has conflicts +- `?` -- a file is untracked - function _update_ps1() { - export PS1="$(~/powerline-shell.py $?)" - } +Each of these will have a number next to it if more than one file matches. - export PROMPT_COMMAND="_update_ps1" +The segment can start with a symbol representing the version control system in +use. To show that symbol, the configuration file must have a variable `vcs` +with an option `show_symbol` set to `true` (see +[Segment Configuration](#segment-configuration)). -* ZSH fans, add the following to your .zshrc: +## Setup - function powerline_precmd() { - export PS1="$(~/powerline-shell.py $? --shell zsh)" - } +This script uses ANSI color codes to display colors in a terminal. These are +notoriously non-portable, so may not work for you out of the box, but try +setting your $TERM to `xterm-256color`. - function install_powerline_precmd() { - for s in "${precmd_functions[@]}"; do - if [ "$s" = "powerline_precmd" ]; then - return - fi - done - precmd_functions+=(powerline_precmd) - } +- Patch the font you use for your terminal: see + [powerline-fonts](https://github.com/Lokaltog/powerline-fonts) + - If you struggle too much to get working fonts in your terminal, you can use + "compatible" mode. + - If you're using old patched fonts, you have to use the older symbols. + Basically reverse [this + commit](https://github.com/milkbikis/powerline-shell/commit/2a84ecc) in + your copy. - install_powerline_precmd +- Install using pip: -* Fish users, redefine `fish_prompt` in ~/.config/fish/config.fish: +``` +pip install powerline-shell +``` - function fish_prompt - ~/powerline-shell.py $status --shell bare - end +(*You can use the +[`--user`](https://pip.pypa.io/en/stable/user_guide/#user-installs) option to +install for just your user, if you'd like. But you may need to fiddle with your +`PATH` to get this working properly.*) + +- Or, install from the git repository: + +``` +git clone https://github.com/b-ryan/powerline-shell +cd powerline-shell +python setup.py install +``` + +- Setup your shell prompt using the instructions for your shell below. + +### Bash + +Add the following to your `.bashrc` file: + +``` +function _update_ps1() { + PS1=$(powerline-shell $?) +} + +if [[ $TERM != linux && ! $PROMPT_COMMAND =~ _update_ps1 ]]; then + PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND" +fi +``` + +**Note:** On macOS, you must add this to one of `.bash_profile`, `.bash_login`, +or `.profile`. macOS will execute the files in the aforementioned order and +will stop execution at the first file it finds. For more information on the +order of precedence, see the section **INVOCATION** in `man bash`. + +### ZSH + +Add the following to your `.zshrc`: + +``` +function powerline_precmd() { + PS1="$(powerline-shell --shell zsh $?)" +} + +function install_powerline_precmd() { + for s in "${precmd_functions[@]}"; do + if [ "$s" = "powerline_precmd" ]; then + return + fi + done + precmd_functions+=(powerline_precmd) +} + +if [ "$TERM" != "linux" -a -x "$(command -v powerline-shell)" ]; then + install_powerline_precmd +fi +``` + +### Fish + +Redefine `fish_prompt` in ~/.config/fish/config.fish: + +``` +function fish_prompt + powerline-shell --shell bare $status +end +``` + +### tcsh + +Add the following to your `.tcshrc`: + +``` +alias precmd 'set prompt="`powerline-shell --shell tcsh $?`"' +``` + +## Customization + +### Config File + +Powerline-shell is customizable through the use of a config file. This file is +expected to be located at `~/.config/powerline-shell/config.json`. You can +generate the default config at this location using: + +``` +mkdir -p ~/.config/powerline-shell && \ +powerline-shell --generate-config > ~/.config/powerline-shell/config.json +``` + +(As an example, my config file is located here: +[here](https://github.com/b-ryan/dotfiles/blob/master/home/config/powerline-shell/config.json)) + +### Adding, Removing and Re-arranging segments + +Once you have generated your config file, you can now start adding or removing +"segments" - the building blocks of your shell. The list of segments available +can be seen +[here](https://github.com/b-ryan/powerline-shell/tree/master/powerline_shell/segments). + +You can also create custom segments. Start by copying an existing segment like +[this](https://github.com/b-ryan/powerline-shell/blob/master/powerline_shell/segments/aws_profile.py). +Make sure to change any relative imports to absolute imports. Ie. change things +like: + +```python +from ..utils import BasicSegment +``` + +to + +```python +from powerline_shell.utils import BasicSegment +``` + +Then change the `add_to_powerline` function to do what you want. You can then +use this segment in your configuration by putting the path to your segment in +the segments section, like: + +```json +"segments": [ + "~/path/to/segment.py" +] +``` + +### Generic Segments + +There are two special segments available. `stdout` accepts an arbitrary command +and the output of the command will be put into your prompt. `env` takes an +environment variable and the value of the variable will be set in your prompt. +For example, your config could look like this: + +``` +{ + "segments": [ + "cwd", + "git", + { + "type": "stdout", + "command": ["echo", "hi"], + "fg_color": 22, + "bg_color": 161 + }, + { + "type": "env", + "var": "DOCKER_MACHINE_NAME" + }, + ] +} +``` + +### Segment Separator + +By default, a unicode character (resembling the > symbol) is used to separate +each segment. This can be changed by changing the "mode" option in the config +file. The available modes are: + +- `patched` - The default. +- `compatible` - Attempts to use characters that may already be available using + your chosen font. +- `flat` - No separator is used between segments, giving each segment a + rectangular appearance (and also saves space). + +### Themes + +The `powerline_shell/themes` directory stores themes for your prompt, which are +basically color values used by segments. The `default.py` defines a default +theme which can be used standalone, and every other theme falls back to it if +they miss colors for any segments. + +If you want to create a custom theme, start by copying one of the existing +themes, like the +[basic](https://github.com/b-ryan/powerline-shell/blob/master/powerline_shell/themes/basic.py). +and update your `~/.config/powerline-shell/config.json`, setting the `"theme"` +to the path of the file. For example your configuration might have: + +``` + "theme": "~/mythemes/my-great-theme.py" +``` + +You can then modify the color codes to your liking. Theme colors are specified +using [Xterm-256 color codes](https://jonasjacek.github.io/colors/). + +A script for testing color combinations is provided at `colortest.py`. Note +that the colors you see may vary depending on your terminal. When designing a +theme, please test your theme on multiple terminals, especially with default +settings. + +### Segment Configuration + +Some segments support additional configuration. The options for the segment are +nested under the name of the segment itself. For example, all of the options +for the `cwd` segment are set in `~/.config/powerline-shell/config.json` like: + +``` +{ + "segments": [...], + "cwd": { + options go here + } + "theme": "theme-name", + "vcs": { + options go here + } +} +``` + +The options for the `cwd` segment are: + +- `mode`: If `plain`, then simple text will be used to show the cwd. If + `dironly`, only the current directory will be shown. Otherwise expands the + cwd into individual directories. +- `max_depth`: Maximum number of directories to show in path. +- `max_dir_size`: Maximum number of characters displayed for each directory in + the path. +- `full_cwd`: If true, the last directory will not be shortened when + `max_dir_size` is used. + +The `hostname` segment provides one option: + +- `colorize`: If true, the hostname will be colorized based on a hash of + itself. + +The `vcs` segment provides one option: + +- `show_symbol`: If `true`, the version control system segment will start with + a symbol representing the specific version control system in use in the + current directory. + +The options for the `battery` segment are: + +- `always_show_percentage`: If true, show percentage when fully charged on AC. +- `low_threshold`: Threshold percentage for low-battery indicator color. + +The options for the `time` segment are: + +- `format`: Format string as used by strftime function, e.g. `%H:%M`. + +### Contributing new types of segments + +The `powerline_shell/segments` directory contains python scripts which are +injected as is into a single file `powerline_shell_base.py`. Each segment +script defines a function that inserts one or more segments into the prompt. If +you want to add a new segment, simply create a new file in the segments +directory. + +Make sure that your script does not introduce new globals which might conflict +with other scripts. Your script should fail silently and run quickly in any +scenario. + +Make sure you introduce new default colors in `themes/default.py` for every new +segment you create. Test your segment with this theme first. + +You should add tests for your segment as best you are able. Unit and +integration tests are both welcome. Run your tests by running the `test.sh` +script. It uses `docker` to manage dependencies and the environment. +Alternatively, you can run the `nosetests` command after installing the +requirements in `requirements-dev.txt`. + +## Troubleshooting + +See the [FAQ](https://github.com/b-ryan/powerline-shell/wiki/FAQ). If you +continue to have issues, please open an +[issue](https://github.com/b-ryan/powerline-shell/issues/new). diff --git a/bash-powerline-screenshot.png b/bash-powerline-screenshot.png new file mode 100644 index 00000000..66a3b244 Binary files /dev/null and b/bash-powerline-screenshot.png differ diff --git a/colortest.py b/colortest.py new file mode 100755 index 00000000..291966f9 --- /dev/null +++ b/colortest.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python2 +import sys + +ESCAPE = chr(27) + +def fg(color): + return ESCAPE + '[38;5;{0}m'.format(color) + +def bg(color): + return ESCAPE + '[48;5;{0}m'.format(color) + +def reset(): + return ESCAPE + '[48;0m' + +if __name__ == "__main__": + if len(sys.argv) < 6: + print 'Usage: colortest.py fg_start fg_end bg_start bg_end test_string' + sys.exit(1) + + fg_start, fg_end, bg_start, bg_end = map(int, sys.argv[1:5]) + test_string = sys.argv[5] + + print ' ' * len(str(bg_start)), + for fg_color in range(fg_start, fg_end + 1): + print ' ' * (len(test_string) - len(str(fg_color))), fg_color, + print + + for bg_color in range(bg_start, bg_end + 1): + print bg_color, bg(bg_color), + for fg_color in range(fg_start, fg_end + 1): + print fg(fg_color), test_string, + print reset() diff --git a/powerline-shell.py b/powerline-shell.py deleted file mode 100755 index ca2f6bfd..00000000 --- a/powerline-shell.py +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -import os -import subprocess -import sys -import re -import argparse - - -def warn(msg): - print '[powerline-bash] ', msg - - -class Color: - # The following link is a pretty good resources for color values: - # http://www.calmar.ws/vim/color-output.png - - PATH_BG = 237 # dark grey - PATH_FG = 250 # light grey - CWD_FG = 254 # nearly-white grey - SEPARATOR_FG = 244 - - REPO_CLEAN_BG = 148 # a light green color - REPO_CLEAN_FG = 0 # black - REPO_DIRTY_BG = 161 # pink/red - REPO_DIRTY_FG = 15 # white - - CMD_PASSED_BG = 236 - CMD_PASSED_FG = 15 - CMD_FAILED_BG = 161 - CMD_FAILED_FG = 15 - - SVN_CHANGES_BG = 148 - SVN_CHANGES_FG = 22 # dark green - - VIRTUAL_ENV_BG = 35 # a mid-tone green - VIRTUAL_ENV_FG = 22 - - -class Powerline: - symbols = { - 'compatible': { - 'separator': u'\u25B6', - 'separator_thin': u'\u276F' - }, - 'patched': { - 'separator': u'\u2B80', - 'separator_thin': u'\u2B81' - } - } - - color_templates = { - 'bash': '\\[\\e%s\\]', - 'zsh': '%%{%s%%}', - 'bare': '%s', - } - - root_indicators = { - 'bash': ' \\$ ', - 'zsh': ' \\$ ', - 'bare': ' $ ', - } - - def __init__(self, mode, shell): - self.shell = shell - self.color_template = self.color_templates[shell] - self.root_indicator = self.root_indicators[shell] - self.reset = self.color_template % '[0m' - self.separator = Powerline.symbols[mode]['separator'] - self.separator_thin = Powerline.symbols[mode]['separator_thin'] - self.segments = [] - - def color(self, prefix, code): - return self.color_template % ('[%s;5;%sm' % (prefix, code)) - - def fgcolor(self, code): - return self.color('38', code) - - def bgcolor(self, code): - return self.color('48', code) - - def append(self, segment): - self.segments.append(segment) - - def draw(self): - shifted = self.segments[1:] + [None] - return (''.join((c.draw(n) for c, n in zip(self.segments, shifted))) - + self.reset).encode('utf-8') - - -class Segment: - def __init__(self, powerline, content, fg, bg, separator=None, - separator_fg=None): - self.powerline = powerline - self.content = content - self.fg = fg - self.bg = bg - self.separator = separator or powerline.separator - self.separator_fg = separator_fg or bg - - def draw(self, next_segment=None): - if next_segment: - separator_bg = self.powerline.bgcolor(next_segment.bg) - else: - separator_bg = self.powerline.reset - - return ''.join(( - self.powerline.fgcolor(self.fg), - self.powerline.bgcolor(self.bg), - self.content, - separator_bg, - self.powerline.fgcolor(self.separator_fg), - self.separator)) - - -def add_cwd_segment(powerline, cwd, maxdepth, cwd_only=False): - #powerline.append(' \\w ', 15, 237) - home = os.getenv('HOME') - cwd = cwd or os.getenv('PWD') - cwd = cwd.decode('utf-8') - - if cwd.find(home) == 0: - cwd = cwd.replace(home, '~', 1) - - if cwd[0] == '/': - cwd = cwd[1:] - - names = cwd.split('/') - if len(names) > maxdepth: - names = names[:2] + [u'\u2026'] + names[2 - maxdepth:] - - if not cwd_only: - for n in names[:-1]: - powerline.append(Segment(powerline, ' %s ' % n, Color.PATH_FG, - Color.PATH_BG, powerline.separator_thin, Color.SEPARATOR_FG)) - powerline.append(Segment(powerline, ' %s ' % names[-1], Color.CWD_FG, - Color.PATH_BG)) - - -def get_hg_status(): - has_modified_files = False - has_untracked_files = False - has_missing_files = False - output = subprocess.Popen(['hg', 'status'], - stdout=subprocess.PIPE).communicate()[0] - for line in output.split('\n'): - if line == '': - continue - elif line[0] == '?': - has_untracked_files = True - elif line[0] == '!': - has_missing_files = True - else: - has_modified_files = True - return has_modified_files, has_untracked_files, has_missing_files - - -def add_hg_segment(powerline, cwd): - branch = os.popen('hg branch 2> /dev/null').read().rstrip() - if len(branch) == 0: - return False - bg = Color.REPO_CLEAN_BG - fg = Color.REPO_CLEAN_FG - has_modified_files, has_untracked_files, has_missing_files = get_hg_status() - if has_modified_files or has_untracked_files or has_missing_files: - bg = Color.REPO_DIRTY_BG - fg = Color.REPO_DIRTY_FG - extra = '' - if has_untracked_files: - extra += '+' - if has_missing_files: - extra += '!' - branch += (' ' + extra if extra != '' else '') - powerline.append(Segment(powerline, ' %s ' % branch, fg, bg)) - return True - - -def get_git_status(): - has_pending_commits = True - has_untracked_files = False - origin_position = "" - output = subprocess.Popen(['git', 'status', '--ignore-submodules'], - stdout=subprocess.PIPE).communicate()[0] - for line in output.split('\n'): - origin_status = re.findall( - r"Your branch is (ahead|behind).*?(\d+) comm", line) - if origin_status: - origin_position = " %d" % int(origin_status[0][1]) - if origin_status[0][0] == 'behind': - origin_position += u'\u21E3' - if origin_status[0][0] == 'ahead': - origin_position += u'\u21E1' - - if line.find('nothing to commit') >= 0: - has_pending_commits = False - if line.find('Untracked files') >= 0: - has_untracked_files = True - return has_pending_commits, has_untracked_files, origin_position - - -def add_git_segment(powerline, cwd): - #cmd = "git branch 2> /dev/null | grep -e '\\*'" - p1 = subprocess.Popen(['git', 'branch'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p2 = subprocess.Popen(['grep', '-e', '\\*'], stdin=p1.stdout, stdout=subprocess.PIPE) - output = p2.communicate()[0].strip() - if not output: - return False - - branch = output.rstrip()[2:] - has_pending_commits, has_untracked_files, origin_position = get_git_status() - branch += origin_position - if has_untracked_files: - branch += ' +' - - bg = Color.REPO_CLEAN_BG - fg = Color.REPO_CLEAN_FG - if has_pending_commits: - bg = Color.REPO_DIRTY_BG - fg = Color.REPO_DIRTY_FG - - powerline.append(Segment(powerline, ' %s ' % branch, fg, bg)) - return True - - -def add_svn_segment(powerline, cwd): - is_svn = subprocess.Popen(['svn', 'status'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - is_svn_output = is_svn.communicate()[1].strip() - if len(is_svn_output) != 0: - return - - '''svn info: - First column: Says if item was added, deleted, or otherwise changed - ' ' no modifications - 'A' Added - 'C' Conflicted - 'D' Deleted - 'I' Ignored - 'M' Modified - 'R' Replaced - 'X' an unversioned directory created by an externals definition - '?' item is not under version control - '!' item is missing (removed by non-svn command) or incomplete - '~' versioned item obstructed by some item of a different kind - ''' - #TODO: Color segment based on above status codes - try: - #cmd = '"svn status | grep -c "^[ACDIMRX\\!\\~]"' - p1 = subprocess.Popen(['svn', 'status'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - p2 = subprocess.Popen(['grep', '-c', '^[ACDIMRX\\!\\~]'], - stdin=p1.stdout, stdout=subprocess.PIPE) - output = p2.communicate()[0].strip() - if len(output) > 0 and int(output) > 0: - changes = output.strip() - powerline.append(Segment(powerline, ' %s ' % changes, - Color.SVN_CHANGES_FG, Color.SVN_CHANGES_BG)) - except OSError: - return False - except subprocess.CalledProcessError: - return False - return True - - -def add_repo_segment(powerline, cwd): - for add_repo_segment in (add_git_segment, add_svn_segment, add_hg_segment): - try: - if add_repo_segment(p, cwd): - return - except subprocess.CalledProcessError: - pass - except OSError: - pass - - -def add_virtual_env_segment(powerline, cwd): - env = os.getenv("VIRTUAL_ENV") - if env is None: - return False - - env_name = os.path.basename(env) - bg = Color.VIRTUAL_ENV_BG - fg = Color.VIRTUAL_ENV_FG - powerline.append(Segment(powerline, ' %s ' % env_name, fg, bg)) - return True - - -def add_root_indicator(powerline, error): - bg = Color.CMD_PASSED_BG - fg = Color.CMD_PASSED_FG - if int(error) != 0: - fg = Color.CMD_FAILED_FG - bg = Color.CMD_FAILED_BG - powerline.append(Segment(powerline, powerline.root_indicator, fg, bg)) - - -def get_valid_cwd(): - """ We check if the current working directory is valid or not. Typically - happens when you checkout a different branch on git that doesn't have - this directory. - We return the original cwd because the shell still considers that to be - the working directory, so returning our guess will confuse people - """ - try: - cwd = os.getcwd() - except: - cwd = os.getenv('PWD') # This is where the OS thinks we are - parts = cwd.split(os.sep) - up = cwd - while parts and not os.path.exists(up): - parts.pop() - up = os.sep.join(parts) - try: - os.chdir(up) - except: - warn("Your current directory is invalid.") - sys.exit(1) - warn("Your current directory is invalid. Lowest valid directory: " + up) - return cwd - -if __name__ == '__main__': - arg_parser = argparse.ArgumentParser() - arg_parser.add_argument('--cwd-only', action='store_true') - arg_parser.add_argument('--mode', action='store', default='patched') - arg_parser.add_argument('--shell', action='store', default='bash') - arg_parser.add_argument('prev_error', nargs='?', default=0) - args = arg_parser.parse_args() - - p = Powerline(mode=args.mode, shell=args.shell) - cwd = get_valid_cwd() - add_virtual_env_segment(p, cwd) - #p.append(Segment(p, ' \\u ', 250, 240)) - #p.append(Segment(p, ' \\h ', 250, 238)) - add_cwd_segment(p, cwd, 5, args.cwd_only) - add_repo_segment(p, cwd) - add_root_indicator(p, args.prev_error) - sys.stdout.write(p.draw()) - -# vim: set expandtab: diff --git a/powerline_shell/__init__.py b/powerline_shell/__init__.py new file mode 100644 index 00000000..ae62704c --- /dev/null +++ b/powerline_shell/__init__.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import print_function +import argparse +import os +import sys +import importlib +import json +from .utils import warn, py3, import_file +import re + + +def _current_dir(): + """Returns the full current working directory as the user would have used + in their shell (ie. without following symbolic links). + + With the introduction of Bash for Windows, we can't use the PWD environment + variable very easily. `os.sep` for windows is `\` but the PWD variable will + use `/`. So just always use the `os` functions for dealing with paths. This + also is fine because the use of PWD below is done to avoid following + symlinks, which Windows doesn't have. + + For non-Windows systems, prefer the PWD environment variable. Python's + `os.getcwd` function follows symbolic links, which is undesirable.""" + if os.name == "nt": + return os.getcwd() + return os.getenv("PWD") or os.getcwd() + + +def get_valid_cwd(): + """Determine and check the current working directory for validity. + + Typically, an directory arises when you checkout a different branch on git + that doesn't have this directory. When an invalid directory is found, a + warning is printed to the screen, but the directory is still returned + as-is, since this is what the shell considers to be the cwd.""" + try: + cwd = _current_dir() + except: + warn("Your current directory is invalid. If you open a ticket at " + + "https://github.com/milkbikis/powerline-shell/issues/new " + + "we would love to help fix the issue.") + sys.stdout.write("> ") + sys.exit(1) + + parts = cwd.split(os.sep) + up = cwd + while parts and not os.path.exists(up): + parts.pop() + up = os.sep.join(parts) + if cwd != up: + warn("Your current directory is invalid. Lowest valid directory: " + + up) + return cwd + + +class Powerline(object): + symbols = { + 'compatible': { + 'lock': 'RO', + 'network': 'SSH', + 'separator': u'\u25B6', + 'separator_thin': u'\u276F' + }, + 'patched': { + 'lock': u'\uE0A2', + 'network': 'SSH', + 'separator': u'\uE0B0', + 'separator_thin': u'\uE0B1' + }, + 'flat': { + 'lock': u'\uE0A2', + 'network': 'SSH', + 'separator': '', + 'separator_thin': '' + }, + } + + color_templates = { + 'bash': r'\[\e%s\]', + 'tcsh': r'%%{\e%s%%}', + 'zsh': '%%{%s%%}', + 'bare': '%s', + } + + def __init__(self, args, config, theme): + self.args = args + self.config = config + self.theme = theme + self.cwd = get_valid_cwd() + mode = config.get("mode", "patched") + self.color_template = self.color_templates[args.shell] + self.reset = self.color_template % '[0m' + self.lock = Powerline.symbols[mode]['lock'] + self.network = Powerline.symbols[mode]['network'] + self.separator = Powerline.symbols[mode]['separator'] + self.separator_thin = Powerline.symbols[mode]['separator_thin'] + self.segments = [] + + def segment_conf(self, seg_name, key, default=None): + return self.config.get(seg_name, {}).get(key, default) + + def color(self, prefix, code): + if code is None: + return '' + elif code == self.theme.RESET: + return self.reset + else: + return self.color_template % ('[%s;5;%sm' % (prefix, code)) + + def fgcolor(self, code): + return self.color('38', code) + + def bgcolor(self, code): + return self.color('48', code) + + def append(self, content, fg, bg, separator=None, separator_fg=None, sanitize=True): + if self.args.shell == "bash" and sanitize: + content = re.sub(r"([`$])", r"\\\1", content) + self.segments.append((content, fg, bg, + separator if separator is not None else self.separator, + separator_fg if separator_fg is not None else bg)) + + def draw(self): + text = (''.join(self.draw_segment(i) for i in range(len(self.segments))) + + self.reset) + ' ' + if py3: + return text + else: + return text.encode('utf-8') + + def draw_segment(self, idx): + segment = self.segments[idx] + next_segment = self.segments[idx + 1] if idx < len(self.segments)-1 else None + + return ''.join(( + self.fgcolor(segment[1]), + self.bgcolor(segment[2]), + segment[0], + self.bgcolor(next_segment[2]) if next_segment else self.reset, + self.fgcolor(segment[4]), + segment[3])) + + +def find_config(): + for location in [ + "powerline-shell.json", + "~/.powerline-shell.json", + os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"), "powerline-shell", "config.json"), + ]: + full = os.path.expanduser(location) + if os.path.exists(full): + return full + +DEFAULT_CONFIG = { + "segments": [ + 'virtual_env', + 'username', + 'hostname', + 'ssh', + 'cwd', + 'git', + 'hg', + 'jobs', + 'root', + ] +} + + +class ModuleNotFoundException(Exception): + pass + + +class CustomImporter(object): + def __init__(self): + self.file_import_count = 0 + + def import_(self, module_prefix, module_or_file, description): + try: + mod = importlib.import_module(module_prefix + module_or_file) + except ImportError: + try: + module_name = "_custom_mod_{0}".format(self.file_import_count) + mod = import_file(module_name, os.path.expanduser(module_or_file)) + self.file_import_count += 1 + except (ImportError, IOError): + msg = "{0} {1} cannot be found".format(description, module_or_file) + raise ModuleNotFoundException( msg) + return mod + + +def main(): + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument('--generate-config', action='store_true', + help='Generate the default config and print it to stdout') + arg_parser.add_argument('--shell', action='store', default='bash', + help='Set this to your shell type', + choices=['bash', 'tcsh', 'zsh', 'bare']) + arg_parser.add_argument('prev_error', nargs='?', type=int, default=0, + help='Error code returned by the last command') + args = arg_parser.parse_args() + + if args.generate_config: + print(json.dumps(DEFAULT_CONFIG, indent=2)) + return 0 + + config_path = find_config() + if config_path: + with open(config_path) as f: + try: + config = json.loads(f.read()) + except Exception as e: + warn("Config file ({0}) could not be decoded! Error: {1}" + .format(config_path, e)) + config = DEFAULT_CONFIG + else: + config = DEFAULT_CONFIG + + custom_importer = CustomImporter() + theme_mod = custom_importer.import_( + "powerline_shell.themes.", + config.get("theme", "default"), + "Theme") + theme = getattr(theme_mod, "Color") + + powerline = Powerline(args, config, theme) + segments = [] + for seg_conf in config["segments"]: + if not isinstance(seg_conf, dict): + seg_conf = {"type": seg_conf} + seg_name = seg_conf["type"] + seg_mod = custom_importer.import_( + "powerline_shell.segments.", + seg_name, + "Segment") + segment = getattr(seg_mod, "Segment")(powerline, seg_conf) + segment.start() + segments.append(segment) + for segment in segments: + segment.add_to_powerline() + sys.stdout.write(powerline.draw()) + return 0 diff --git a/powerline_shell/color_compliment.py b/powerline_shell/color_compliment.py new file mode 100644 index 00000000..f0517115 --- /dev/null +++ b/powerline_shell/color_compliment.py @@ -0,0 +1,35 @@ +from colorsys import hls_to_rgb, rgb_to_hls +# md5 deprecated since Python 2.5 +try: + from md5 import md5 +except ImportError: + from hashlib import md5 +import sys +from .colortrans import * +from .utils import py3 + + +def getOppositeColor(r,g,b): + r, g, b = [x/255.0 for x in [r, g, b]] # convert to float before getting hls value + hls = rgb_to_hls(r,g,b) + opp = list(hls[:]) + opp[0] = (opp[0]+0.2)%1 # shift hue (a.k.a. color) + if opp[1] > 255/2: # for level you want to make sure they + opp[1] -= 255/2 # are quite different so easily readable + else: + opp[1] += 255/2 + if opp[2] > -0.5: # if saturation is low on first color increase second's + opp[2] -= 0.5 + opp = hls_to_rgb(*opp) + m = max(opp) + if m > 255: #colorsys module doesn't give caps to their conversions + opp = [ x*254/m for x in opp] + return tuple([ int(x) for x in opp]) + +def stringToHashToColorAndOpposite(string): + if py3: + string = string.encode('utf-8') + string = md5(string).hexdigest()[:6] # get a random color + color1 = rgbstring2tuple(string) + color2 = getOppositeColor(*color1) + return color1, color2 diff --git a/powerline_shell/colortrans.py b/powerline_shell/colortrans.py new file mode 100755 index 00000000..1c385522 --- /dev/null +++ b/powerline_shell/colortrans.py @@ -0,0 +1,318 @@ +#! /usr/bin/env python + +""" +Code is modified (fairly heavily) by hryanjones@gmail.com from +https://gist.github.com/MicahElliott/719710 + +Convert values between RGB tuples and xterm-256 color codes. + +Nice long listing of all 256 colors and their codes. Useful for +developing console color themes, or even script output schemes. + +Resources: +* http://en.wikipedia.org/wiki/8-bit_color +* http://en.wikipedia.org/wiki/ANSI_escape_code +* /usr/share/X11/rgb.txt + +I'm not sure where this script was inspired from. I think I must have +written it from scratch, though it's been several years now. +""" + +__author__ = 'Micah Elliott http://MicahElliott.com' +__version__ = '0.1' +__copyright__ = 'Copyright (C) 2011 Micah Elliott. All rights reserved.' +__license__ = 'WTFPL http://sam.zoy.org/wtfpl/' + +#--------------------------------------------------------------------- + + +def hexstr2num(hexstr): + return int(hexstr, 16) + +def rgbstring2tuple(s): + return tuple([hexstr2num(h) for h in (s[:2], s[2:4], s[4:])]) + +RGB2SHORT_DICT = { + (0, 0, 0): 16, + (0, 0, 95): 17, + (0, 0, 128): 4, + (0, 0, 135): 18, + (0, 0, 175): 19, + (0, 0, 215): 20, + (0, 0, 255): 12, + (0, 95, 0): 22, + (0, 95, 95): 23, + (0, 95, 135): 24, + (0, 95, 175): 25, + (0, 95, 215): 26, + (0, 95, 255): 27, + (0, 128, 0): 2, + (0, 128, 128): 6, + (0, 135, 0): 28, + (0, 135, 95): 29, + (0, 135, 135): 30, + (0, 135, 175): 31, + (0, 135, 215): 32, + (0, 135, 255): 33, + (0, 175, 0): 34, + (0, 175, 95): 35, + (0, 175, 135): 36, + (0, 175, 175): 37, + (0, 175, 215): 38, + (0, 175, 255): 39, + (0, 215, 0): 40, + (0, 215, 95): 41, + (0, 215, 135): 42, + (0, 215, 175): 43, + (0, 215, 215): 44, + (0, 215, 255): 45, + (0, 255, 0): 46, + (0, 255, 95): 47, + (0, 255, 135): 48, + (0, 255, 175): 49, + (0, 255, 215): 50, + (0, 255, 255): 14, + (8, 8, 8): 232, + (18, 18, 18): 233, + (28, 28, 28): 234, + (38, 38, 38): 235, + (48, 48, 48): 236, + (58, 58, 58): 237, + (68, 68, 68): 238, + (78, 78, 78): 239, + (88, 88, 88): 240, + (95, 0, 0): 52, + (95, 0, 95): 53, + (95, 0, 135): 54, + (95, 0, 175): 55, + (95, 0, 215): 56, + (95, 0, 255): 57, + (95, 95, 0): 58, + (95, 95, 95): 59, + (95, 95, 135): 60, + (95, 95, 175): 61, + (95, 95, 215): 62, + (95, 95, 255): 63, + (95, 135, 0): 64, + (95, 135, 95): 65, + (95, 135, 135): 66, + (95, 135, 175): 67, + (95, 135, 215): 68, + (95, 135, 255): 69, + (95, 175, 0): 70, + (95, 175, 95) : 71, + (95, 175, 135): 72, + (95, 175, 175): 73, + (95, 175, 215): 74, + (95, 175, 255): 75, + (95, 215, 0): 76, + (95, 215, 95) : 77, + (95, 215, 135): 78, + (95, 215, 175): 79, + (95, 215, 215): 80, + (95, 215, 255): 81, + (95, 255, 0): 82, + (95, 255, 95) : 83, + (95, 255, 135): 84, + (95, 255, 175): 85, + (95, 255, 215): 86, + (95, 255, 255): 87, + (98, 98, 98): 241, + (108, 108, 108): 242, + (118, 118, 118): 243, + (128, 0, 0): 1, + (128, 0, 128): 5, + (128, 128, 0): 3, + (128, 128, 128): 244, + (135, 0, 0): 88, + (135, 0, 95): 89, + (135, 0, 135): 90, + (135, 0, 175): 91, + (135, 0, 215): 92, + (135, 0, 255): 93, + (135, 95, 0): 94, + (135, 95, 95): 95, + (135, 95, 135): 96, + (135, 95, 175): 97, + (135, 95, 215): 98, + (135, 95, 255): 99, + (135, 135, 0): 100, + (135, 135, 95): 101, + (135, 135, 135): 102, + (135, 135, 175): 103, + (135, 135, 215): 104, + (135, 135, 255): 105, + (135, 175, 0): 106, + (135, 175, 95): 107, + (135, 175, 135): 108, + (135, 175, 175): 109, + (135, 175, 215): 110, + (135, 175, 255): 111, + (135, 215, 0): 112, + (135, 215, 95): 113, + (135, 215, 135): 114, + (135, 215, 175): 115, + (135, 215, 215): 116, + (135, 215, 255): 117, + (135, 255, 0): 118, + (135, 255, 95): 119, + (135, 255, 135): 120, + (135, 255, 175): 121, + (135, 255, 215): 122, + (135, 255, 255): 123, + (138, 138, 138): 245, + (148, 148, 148): 246, + (158, 158, 158): 247, + (168, 168, 168): 248, + (175, 0, 0): 124, + (175, 0, 95): 125, + (175, 0, 135): 126, + (175, 0, 175): 127, + (175, 0, 215): 128, + (175, 0, 255): 129, + (175, 95, 0): 130, + (175, 95, 95): 131, + (175, 95, 135): 132, + (175, 95, 175): 133, + (175, 95, 215): 134, + (175, 95, 255): 135, + (175, 135, 0): 136, + (175, 135, 95): 137, + (175, 135, 135): 138, + (175, 135, 175): 139, + (175, 135, 215): 140, + (175, 135, 255): 141, + (175, 175, 0): 142, + (175, 175, 95): 143, + (175, 175, 135): 144, + (175, 175, 175): 145, + (175, 175, 215): 146, + (175, 175, 255): 147, + (175, 215, 0): 148, + (175, 215, 95): 149, + (175, 215, 135): 150, + (175, 215, 175): 151, + (175, 215, 215): 152, + (175, 215, 255): 153, + (175, 255, 0): 154, + (175, 255, 95): 155, + (175, 255, 135): 156, + (175, 255, 175): 157, + (175, 255, 215): 158, + (175, 255, 255): 159, + (178, 178, 178): 249, + (188, 188, 188): 250, + (192, 192, 192): 7, + (198, 198, 198): 251, + (208, 208, 208): 252, + (215, 0, 0): 160, + (215, 0, 95): 161, + (215, 0, 135): 162, + (215, 0, 175): 163, + (215, 0, 215): 164, + (215, 0, 255): 165, + (215, 95, 0): 166, + (215, 95, 95): 167, + (215, 95, 135): 168, + (215, 95, 175): 169, + (215, 95, 215): 170, + (215, 95, 255): 171, + (215, 135, 0): 172, + (215, 135, 95): 173, + (215, 135, 135): 174, + (215, 135, 175): 175, + (215, 135, 215): 176, + (215, 135, 255): 177, + (215, 175, 0): 178, + (215, 175, 95): 179, + (215, 175, 135): 180, + (215, 175, 175): 181, + (215, 175, 215): 182, + (215, 175, 255): 183, + (215, 215, 0): 184, + (215, 215, 95): 185, + (215, 215, 135): 186, + (215, 215, 175): 187, + (215, 215, 215): 188, + (215, 215, 255): 189, + (215, 255, 0): 190, + (215, 255, 95): 191, + (215, 255, 135): 192, + (215, 255, 175): 193, + (215, 255, 215): 194, + (215, 255, 255): 195, + (218, 218, 218): 253, + (228, 228, 228): 254, + (238, 238, 238): 255, + (255, 0, 0): 196, + (255, 0, 95): 197, + (255, 0, 135): 198, + (255, 0, 175): 199, + (255, 0, 215): 200, + (255, 0, 255): 13, + (255, 95, 0): 202, + (255, 95, 95): 203, + (255, 95, 135): 204, + (255, 95, 175): 205, + (255, 95, 215): 206, + (255, 95, 255): 207, + (255, 135, 0): 208, + (255, 135, 95): 209, + (255, 135, 135): 210, + (255, 135, 175): 211, + (255, 135, 215): 212, + (255, 135, 255): 213, + (255, 175, 0): 214, + (255, 175, 95): 215, + (255, 175, 135): 216, + (255, 175, 175): 217, + (255, 175, 215): 218, + (255, 175, 255): 219, + (255, 215, 0): 220, + (255, 215, 95): 221, + (255, 215, 135): 222, + (255, 215, 175): 223, + (255, 215, 215): 224, + (255, 215, 255): 225, + (255, 255, 0): 11, + (255, 255, 95): 227, + (255, 255, 135): 228, + (255, 255, 175): 229, + (255, 255, 215): 230, + (255, 255, 255): 231} + +def rgb2short(r, g, b): + """ Find the closest xterm-256 approximation to the given RGB value. + @param r,g,b: each is a number between 0-255 for the Red, Green, and Blue values + @returns: integer between 0 and 255, compatible with xterm. + >>> rgb2short(18, 52, 86) + 23 + >>> rgb2short(255, 255, 255) + 231 + >>> rgb2short(13, 173, 214) # vimeo logo + 38 + """ + incs = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff) + # Break 6-char RGB code into 3 integer vals. + parts = [ r, g, b] + res = [] + for part in parts: + i = 0 + while i < len(incs)-1: + s, b = incs[i], incs[i+1] # smaller, bigger + if s <= part <= b: + s1 = abs(s - part) + b1 = abs(b - part) + if s1 < b1: closest = s + else: closest = b + res.append(closest) + break + i += 1 + #print '***', res + return RGB2SHORT_DICT[tuple(res)] + +#--------------------------------------------------------------------- + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/powerline_shell/segments.py b/powerline_shell/segments.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline_shell/segments/__init__.py b/powerline_shell/segments/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline_shell/segments/aws_profile.py b/powerline_shell/segments/aws_profile.py new file mode 100644 index 00000000..6e9433e6 --- /dev/null +++ b/powerline_shell/segments/aws_profile.py @@ -0,0 +1,12 @@ +from ..utils import BasicSegment +import os + + +class Segment(BasicSegment): + def add_to_powerline(self): + aws_profile = os.environ.get("AWS_PROFILE") or \ + os.environ.get("AWS_DEFAULT_PROFILE") + if aws_profile: + self.powerline.append(" aws:%s " % os.path.basename(aws_profile), + self.powerline.theme.AWS_PROFILE_FG, + self.powerline.theme.AWS_PROFILE_BG) diff --git a/powerline_shell/segments/battery.py b/powerline_shell/segments/battery.py new file mode 100644 index 00000000..24c5f1f5 --- /dev/null +++ b/powerline_shell/segments/battery.py @@ -0,0 +1,37 @@ +from ..utils import BasicSegment, warn +import os + + +class Segment(BasicSegment): + def add_to_powerline(self): + # See discussion in https://github.com/banga/powerline-shell/pull/204 + # regarding the directory where battery info is saved + if os.path.exists("/sys/class/power_supply/BAT0"): + dir_ = "/sys/class/power_supply/BAT0" + elif os.path.exists("/sys/class/power_supply/BAT1"): + dir_ = "/sys/class/power_supply/BAT1" + else: + warn("battery directory could not be found") + return + + with open(os.path.join(dir_, "capacity")) as f: + cap = int(f.read().strip()) + with open(os.path.join(dir_, "status")) as f: + status = f.read().strip() + if status == "Full": + if self.powerline.segment_conf("battery", "always_show_percentage", False): + pwr_fmt = u" {cap:d}% \U0001F50C " + else: + pwr_fmt = u" \U0001F50C " + elif status == "Charging": + pwr_fmt = u" {cap:d}% \u26A1 " + else: + pwr_fmt = " {cap:d}% " + + if cap < self.powerline.segment_conf("battery", "low_threshold", 20): + bg = self.powerline.theme.BATTERY_LOW_BG + fg = self.powerline.theme.BATTERY_LOW_FG + else: + bg = self.powerline.theme.BATTERY_NORMAL_BG + fg = self.powerline.theme.BATTERY_NORMAL_FG + self.powerline.append(pwr_fmt.format(cap=cap), fg, bg) diff --git a/powerline_shell/segments/bzr.py b/powerline_shell/segments/bzr.py new file mode 100644 index 00000000..059c813c --- /dev/null +++ b/powerline_shell/segments/bzr.py @@ -0,0 +1,69 @@ +import subprocess +from ..utils import RepoStats, ThreadedSegment, get_subprocess_env + + +def _get_bzr_branch(): + p = subprocess.Popen(['bzr', 'nick'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=get_subprocess_env()) + branch = p.communicate()[0].decode("utf-8").rstrip('\n') + return branch + + +def parse_bzr_stats(status): + stats = RepoStats() + statustype = "changed" + for statusline in status: + if statusline[:2] == " ": + setattr(stats, statustype, getattr(stats, statustype) + 1) + elif statusline == "added:": + statustype = "staged" + elif statusline == "unknown:": + statustype = "new" + else: # removed, missing, renamed, modified or kind changed + statustype = "changed" + return stats + + +def _get_bzr_status(output): + """This function exists to enable mocking the `bzr status` output in tests. + """ + return output[0].decode("utf-8").splitlines() + + +def build_stats(): + try: + p = subprocess.Popen(['bzr', 'status'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=get_subprocess_env()) + except OSError: + # Popen will throw an OSError if bzr is not found + return (None, None) + pdata = p.communicate() + if p.returncode != 0: + return (None, None) + status = _get_bzr_status(pdata) + stats = parse_bzr_stats(status) + branch = _get_bzr_branch() + return stats, branch + + +class Segment(ThreadedSegment): + def run(self): + self.stats, self.branch = build_stats() + + def add_to_powerline(self): + self.join() + if not self.stats: + return + bg = self.powerline.theme.REPO_CLEAN_BG + fg = self.powerline.theme.REPO_CLEAN_FG + if self.stats.dirty: + bg = self.powerline.theme.REPO_DIRTY_BG + fg = self.powerline.theme.REPO_DIRTY_FG + if self.powerline.segment_conf("vcs", "show_symbol"): + symbol = RepoStats().symbols["bzr"] + " " + else: + symbol = "" + self.powerline.append(" " + symbol + self.branch + " ", fg, bg) + self.stats.add_to_powerline(self.powerline) diff --git a/powerline_shell/segments/cwd.py b/powerline_shell/segments/cwd.py new file mode 100644 index 00000000..a7023178 --- /dev/null +++ b/powerline_shell/segments/cwd.py @@ -0,0 +1,105 @@ +import os +import sys +from ..utils import warn, py3, BasicSegment + +ELLIPSIS = u'\u2026' + + +def replace_home_dir(cwd): + home = os.path.realpath(os.getenv('HOME')) + if cwd.startswith(home): + return '~' + cwd[len(home):] + return cwd + + +def split_path_into_names(cwd): + names = cwd.split(os.sep) + + if names[0] == '': + names = names[1:] + + if not names[0]: + return ['/'] + + return names + + +def requires_special_home_display(powerline, name): + """Returns true if the given directory name matches the home indicator and + the chosen theme should use a special home indicator display.""" + return (name == '~' and powerline.theme.HOME_SPECIAL_DISPLAY) + + +def maybe_shorten_name(powerline, name): + """If the user has asked for each directory name to be shortened, will + return the name up to their specified length. Otherwise returns the full + name.""" + max_size = powerline.segment_conf("cwd", "max_dir_size") + if max_size: + return name[:max_size] + return name + + +def get_fg_bg(powerline, name, is_last_dir): + """Returns the foreground and background color to use for the given name. + """ + if requires_special_home_display(powerline, name): + return (powerline.theme.HOME_FG, powerline.theme.HOME_BG,) + + if is_last_dir: + return (powerline.theme.CWD_FG, powerline.theme.PATH_BG,) + else: + return (powerline.theme.PATH_FG, powerline.theme.PATH_BG,) + + +def add_cwd_segment(powerline): + cwd = powerline.cwd + if not py3: + cwd = cwd.decode("utf-8") + cwd = replace_home_dir(cwd) + + names = split_path_into_names(cwd) + + full_cwd = powerline.segment_conf("cwd", "full_cwd", False) + max_depth = powerline.segment_conf("cwd", "max_depth", 5) + if max_depth <= 0: + warn("Ignoring cwd.max_depth option since it's not greater than 0") + elif len(names) > max_depth: + # https://github.com/milkbikis/powerline-shell/issues/148 + # n_before is the number is the number of directories to put before the + # ellipsis. So if you are at ~/a/b/c/d/e and max depth is 4, it will + # show `~ a ... d e`. + # + # max_depth must be greater than n_before or else you end up repeating + # parts of the path with the way the splicing is written below. + n_before = 2 if max_depth > 2 else max_depth - 1 + names = names[:n_before] + [ELLIPSIS] + names[n_before - max_depth:] + + if powerline.segment_conf("cwd", "mode") == "dironly": + # The user has indicated they only want the current directory to be + # displayed, so chop everything else off + names = names[-1:] + + elif powerline.segment_conf("cwd", "mode") == "plain": + joined = os.path.sep.join(names) + powerline.append(" %s " % (joined,), powerline.theme.CWD_FG, + powerline.theme.PATH_BG) + return + + for i, name in enumerate(names): + is_last_dir = (i == len(names) - 1) + fg, bg = get_fg_bg(powerline, name, is_last_dir) + + separator = powerline.separator_thin + separator_fg = powerline.theme.SEPARATOR_FG + if requires_special_home_display(powerline, name) or is_last_dir: + separator = None + separator_fg = None + + if not (is_last_dir and full_cwd): + name = maybe_shorten_name(powerline, name) + powerline.append(' %s ' % name, fg, bg, separator, separator_fg) + +class Segment(BasicSegment): + def add_to_powerline(self): + add_cwd_segment(self.powerline) diff --git a/powerline_shell/segments/env.py b/powerline_shell/segments/env.py new file mode 100644 index 00000000..29ef048a --- /dev/null +++ b/powerline_shell/segments/env.py @@ -0,0 +1,10 @@ +import os +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + self.powerline.append( + " %s " % os.getenv(self.segment_def["var"]), + self.segment_def.get("fg_color", self.powerline.theme.PATH_FG), + self.segment_def.get("bg_color", self.powerline.theme.PATH_BG)) diff --git a/powerline_shell/segments/exit_code.py b/powerline_shell/segments/exit_code.py new file mode 100644 index 00000000..85153ec3 --- /dev/null +++ b/powerline_shell/segments/exit_code.py @@ -0,0 +1,10 @@ +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + if self.powerline.args.prev_error == 0: + return + fg = self.powerline.theme.CMD_FAILED_FG + bg = self.powerline.theme.CMD_FAILED_BG + self.powerline.append(' %s ' % str(self.powerline.args.prev_error), fg, bg) diff --git a/powerline_shell/segments/fossil.py b/powerline_shell/segments/fossil.py new file mode 100644 index 00000000..f745d846 --- /dev/null +++ b/powerline_shell/segments/fossil.py @@ -0,0 +1,71 @@ +import os +import subprocess +from ..utils import RepoStats, ThreadedSegment, get_subprocess_env + + +def _get_fossil_branch(): + branches = os.popen("fossil branch 2>/dev/null").read().strip().split("\n") + return ''.join([ + i.replace('*','').strip() + for i in branches + if i.startswith('*') + ]) + + +def parse_fossil_stats(status): + stats = RepoStats() + for line in status: + if line.startswith("ADDED"): + stats.staged += 1 + elif line.startswith("EXTRA"): + stats.new += 1 + elif line.startswith("CONFLICT"): + stats.conflicted += 1 + else: + stats.changed += 1 + return stats + + +def _get_fossil_status(): + changes = os.popen("fossil changes 2>/dev/null").read().strip().split("\n") + extra = os.popen("fossil extras 2>/dev/null").read().strip().split("\n") + extra = ["EXTRA " + filename for filename in extra if filename != ""] + status = [line for line in changes + extra if line != ''] + return status + + +def build_stats(): + try: + subprocess.Popen(['fossil'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=get_subprocess_env()).communicate() + except OSError: + # Popen will throw an OSError if fossil is not found + return (None, None) + branch = _get_fossil_branch() + if branch == "": + return (None, None) + status = _get_fossil_status() + if status == []: + return (RepoStats(), branch) + stats = parse_fossil_stats(status) + return stats, branch + + +class Segment(ThreadedSegment): + + def add_to_powerline(self): + self.stats, self.branch = build_stats() + if not self.stats: + return + bg = self.powerline.theme.REPO_CLEAN_BG + fg = self.powerline.theme.REPO_CLEAN_FG + if self.stats.dirty: + bg = self.powerline.theme.REPO_DIRTY_BG + fg = self.powerline.theme.REPO_DIRTY_FG + if self.powerline.segment_conf("vcs", "show_symbol"): + symbol = RepoStats().symbols["fossil"] + " " + else: + symbol = "" + self.powerline.append(" " + symbol + self.branch + " ", fg, bg) + self.stats.add_to_powerline(self.powerline) diff --git a/powerline_shell/segments/git.py b/powerline_shell/segments/git.py new file mode 100644 index 00000000..de2b049e --- /dev/null +++ b/powerline_shell/segments/git.py @@ -0,0 +1,84 @@ +import re +import subprocess +from ..utils import RepoStats, ThreadedSegment, get_git_subprocess_env + + +def parse_git_branch_info(status): + info = re.search('^## (?P\S+?)''(\.{3}(?P\S+?)( \[(ahead (?P\d+)(, )?)?(behind (?P\d+))?\])?)?$', status[0]) + return info.groupdict() if info else None + + +def _get_git_detached_branch(): + p = subprocess.Popen(['git', 'describe', '--tags', '--always'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=get_git_subprocess_env()) + detached_ref = p.communicate()[0].decode("utf-8").rstrip('\n') + if p.returncode == 0: + branch = u'{} {}'.format(RepoStats.symbols['detached'], detached_ref) + else: + branch = 'Big Bang' + return branch + + +def parse_git_stats(status): + stats = RepoStats() + for statusline in status[1:]: + code = statusline[:2] + if code == '??': + stats.new += 1 + elif code in ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'): + stats.conflicted += 1 + else: + if code[1] != ' ': + stats.changed += 1 + if code[0] != ' ': + stats.staged += 1 + + return stats + + +def build_stats(): + try: + p = subprocess.Popen(['git', 'status', '--porcelain', '-b'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=get_git_subprocess_env()) + except OSError: + # Popen will throw an OSError if git is not found + return (None, None) + + pdata = p.communicate() + if p.returncode != 0: + return (None, None) + + status = pdata[0].decode("utf-8").splitlines() + stats = parse_git_stats(status) + branch_info = parse_git_branch_info(status) + + if branch_info: + stats.ahead = branch_info["ahead"] + stats.behind = branch_info["behind"] + branch = branch_info['local'] + else: + branch = _get_git_detached_branch() + return stats, branch + + +class Segment(ThreadedSegment): + def run(self): + self.stats, self.branch = build_stats() + + def add_to_powerline(self): + self.join() + if not self.stats: + return + bg = self.powerline.theme.REPO_CLEAN_BG + fg = self.powerline.theme.REPO_CLEAN_FG + if self.stats.dirty: + bg = self.powerline.theme.REPO_DIRTY_BG + fg = self.powerline.theme.REPO_DIRTY_FG + if self.powerline.segment_conf("vcs", "show_symbol"): + symbol = RepoStats().symbols["git"] + " " + else: + symbol = "" + self.powerline.append(" " + symbol + self.branch + " ", fg, bg) + self.stats.add_to_powerline(self.powerline) diff --git a/powerline_shell/segments/git_stash.py b/powerline_shell/segments/git_stash.py new file mode 100644 index 00000000..594cf386 --- /dev/null +++ b/powerline_shell/segments/git_stash.py @@ -0,0 +1,34 @@ +import subprocess +from ..utils import RepoStats, ThreadedSegment, get_git_subprocess_env + + +def get_stash_count(): + try: + p = subprocess.Popen(['git', 'stash', 'list'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=get_git_subprocess_env()) + except OSError: + return 0 + + pdata = p.communicate() + if p.returncode != 0: + return 0 + + return pdata[0].count(b'\n') + + +class Segment(ThreadedSegment): + def run(self): + self.stash_count = get_stash_count() + + def add_to_powerline(self): + self.join() + if not self.stash_count: + return + + bg = self.powerline.theme.GIT_STASH_BG + fg = self.powerline.theme.GIT_STASH_FG + + sc = self.stash_count if self.stash_count > 1 else '' + stash_str = u' {}{} '.format(sc, RepoStats.symbols['stash']) + self.powerline.append(stash_str, fg, bg) diff --git a/powerline_shell/segments/hg.py b/powerline_shell/segments/hg.py new file mode 100644 index 00000000..be784355 --- /dev/null +++ b/powerline_shell/segments/hg.py @@ -0,0 +1,68 @@ +import subprocess +from ..utils import RepoStats, ThreadedSegment, get_subprocess_env + + +def _get_hg_branch(): + p = subprocess.Popen(["hg", "branch"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=get_subprocess_env()) + branch = p.communicate()[0].decode("utf-8").rstrip('\n') + return branch + + +def parse_hg_stats(status): + stats = RepoStats() + for statusline in status: + if statusline[0] == "A": + stats.staged += 1 + elif statusline[0] == "?": + stats.new += 1 + else: # [M]odified, [R]emoved, (!)missing + stats.changed += 1 + return stats + + +def _get_hg_status(output): + """This function exists to enable mocking the `hg status` output in tests. + """ + return output[0].decode("utf-8").splitlines() + + +def build_stats(): + try: + p = subprocess.Popen(["hg", "status"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=get_subprocess_env()) + except OSError: + # Will be thrown if hg cannot be found + return None, None + pdata = p.communicate() + if p.returncode != 0: + return None, None + status = _get_hg_status(pdata) + stats = parse_hg_stats(status) + branch = _get_hg_branch() + return stats, branch + + +class Segment(ThreadedSegment): + def run(self): + self.stats, self.branch = build_stats() + + def add_to_powerline(self): + self.join() + if not self.stats: + return + bg = self.powerline.theme.REPO_CLEAN_BG + fg = self.powerline.theme.REPO_CLEAN_FG + if self.stats.dirty: + bg = self.powerline.theme.REPO_DIRTY_BG + fg = self.powerline.theme.REPO_DIRTY_FG + if self.powerline.segment_conf("vcs", "show_symbol"): + symbol = RepoStats().symbols["hg"] + " " + else: + symbol = "" + self.powerline.append(" " + symbol + self.branch + " ", fg, bg) + self.stats.add_to_powerline(self.powerline) diff --git a/powerline_shell/segments/hostname.py b/powerline_shell/segments/hostname.py new file mode 100644 index 00000000..894e139e --- /dev/null +++ b/powerline_shell/segments/hostname.py @@ -0,0 +1,25 @@ +from ..utils import BasicSegment +from ..color_compliment import stringToHashToColorAndOpposite +from ..colortrans import rgb2short +from socket import gethostname + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + if powerline.segment_conf("hostname", "colorize"): + hostname = gethostname() + FG, BG = stringToHashToColorAndOpposite(hostname) + FG, BG = (rgb2short(*color) for color in [FG, BG]) + host_prompt = " %s " % hostname.split(".")[0] + powerline.append(host_prompt, FG, BG) + else: + if powerline.args.shell == "bash": + host_prompt = r" \h " + elif powerline.args.shell == "zsh": + host_prompt = " %m " + else: + host_prompt = " %s " % gethostname().split(".")[0] + powerline.append(host_prompt, + powerline.theme.HOSTNAME_FG, + powerline.theme.HOSTNAME_BG) diff --git a/powerline_shell/segments/jobs.py b/powerline_shell/segments/jobs.py new file mode 100644 index 00000000..22d00bee --- /dev/null +++ b/powerline_shell/segments/jobs.py @@ -0,0 +1,35 @@ +import os +import re +import subprocess +import platform +from ..utils import ThreadedSegment + + +class Segment(ThreadedSegment): + + def run(self): + self.num_jobs = 0 + system = platform.system() + if system.startswith("CYGWIN") or system.startswith("MINGW"): + # cygwin ps is a special snowflake... + output_proc = subprocess.Popen(["ps", "-af"], stdout=subprocess.PIPE) + output = [int(l.split()[2].strip()) for l in output_proc.communicate()[0].decode("utf-8").splitlines()[1:]] + self.num_jobs = output.count(os.getppid()) - 1 + else: + # The following logic was tested on: + # - fish, version 3.3.1 + # - GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu) + # - zsh 5.8.1 (x86_64-ubuntu-linux-gnu) + # If you change the behavior to account for another shell's + # behavior, please provide details of the shell version you tested + # on in this comment. + output_proc = subprocess.Popen(["ps", "-a", "-o", "ppid"], stdout=subprocess.PIPE) + output = output_proc.communicate()[0].decode("utf-8") + self.num_jobs = len(re.findall(str(os.getppid()), output)) - 1 + + def add_to_powerline(self): + self.join() + if self.num_jobs > 0: + self.powerline.append(" %d " % self.num_jobs, + self.powerline.theme.JOBS_FG, + self.powerline.theme.JOBS_BG) diff --git a/powerline_shell/segments/newline.py b/powerline_shell/segments/newline.py new file mode 100644 index 00000000..d6c5e9f0 --- /dev/null +++ b/powerline_shell/segments/newline.py @@ -0,0 +1,12 @@ +from ..utils import BasicSegment, warn + + +class Segment(BasicSegment): + def add_to_powerline(self): + if self.powerline.args.shell == "tcsh": + warn("newline segment not supported for tcsh (yet?)") + return + self.powerline.append("\n", + self.powerline.theme.RESET, + self.powerline.theme.RESET, + separator="") diff --git a/powerline_shell/segments/node_version.py b/powerline_shell/segments/node_version.py new file mode 100644 index 00000000..aa195928 --- /dev/null +++ b/powerline_shell/segments/node_version.py @@ -0,0 +1,18 @@ +import subprocess +from ..utils import ThreadedSegment + + +class Segment(ThreadedSegment): + def run(self): + try: + p1 = subprocess.Popen(["node", "--version"], stdout=subprocess.PIPE) + self.version = p1.communicate()[0].decode("utf-8").rstrip() + except OSError: + self.version = None + + def add_to_powerline(self): + self.join() + if not self.version: + return + # FIXME no hard-coded colors + self.powerline.append("node " + self.version, 15, 18) diff --git a/powerline_shell/segments/npm_version.py b/powerline_shell/segments/npm_version.py new file mode 100644 index 00000000..4ef23bcd --- /dev/null +++ b/powerline_shell/segments/npm_version.py @@ -0,0 +1,17 @@ +import subprocess +from ..utils import ThreadedSegment + + +class Segment(ThreadedSegment): + def run(self): + try: + p1 = subprocess.Popen(["npm", "--version"], stdout=subprocess.PIPE) + self.version = p1.communicate()[0].decode("utf-8").rstrip() + except OSError: + self.version = None + + def add_to_powerline(self): + self.join() + if self.version: + # FIXME no hard-coded colors + self.powerline.append("npm " + self.version, 15, 18) diff --git a/powerline_shell/segments/php_version.py b/powerline_shell/segments/php_version.py new file mode 100644 index 00000000..f482cf01 --- /dev/null +++ b/powerline_shell/segments/php_version.py @@ -0,0 +1,21 @@ +import subprocess +from ..utils import ThreadedSegment, decode + + +class Segment(ThreadedSegment): + def run(self): + self.version = None + try: + output = decode( + subprocess.check_output(['php', '-r', 'echo PHP_VERSION;'], + stderr=subprocess.STDOUT)) + self.version = output.split('-')[0] if '-' in output else output + except OSError: + self.version = None + + def add_to_powerline(self): + self.join() + if not self.version: + return + # FIXME no hard-coded colors + self.powerline.append(" " + self.version + " ", 15, 4) diff --git a/powerline_shell/segments/rbenv.py b/powerline_shell/segments/rbenv.py new file mode 100644 index 00000000..4b8c5d37 --- /dev/null +++ b/powerline_shell/segments/rbenv.py @@ -0,0 +1,17 @@ +import subprocess +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + try: + p1 = subprocess.Popen(["rbenv", "local"], stdout=subprocess.PIPE) + version = p1.communicate()[0].decode("utf-8").rstrip() + if len(version) <= 0: + return + powerline.append(' %s ' % version, + powerline.theme.VIRTUAL_ENV_FG, + powerline.theme.VIRTUAL_ENV_BG) + except OSError: + return diff --git a/powerline_shell/segments/read_only.py b/powerline_shell/segments/read_only.py new file mode 100644 index 00000000..c4fc60ea --- /dev/null +++ b/powerline_shell/segments/read_only.py @@ -0,0 +1,11 @@ +import os +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + if not os.access(powerline.cwd, os.W_OK): + powerline.append(' %s ' % powerline.lock, + powerline.theme.READONLY_FG, + powerline.theme.READONLY_BG) diff --git a/powerline_shell/segments/root.py b/powerline_shell/segments/root.py new file mode 100644 index 00000000..7779eacf --- /dev/null +++ b/powerline_shell/segments/root.py @@ -0,0 +1,18 @@ +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + root_indicators = { + 'bash': ' \\$ ', + 'tcsh': ' %# ', + 'zsh': ' %# ', + 'bare': ' $ ', + } + bg = powerline.theme.CMD_PASSED_BG + fg = powerline.theme.CMD_PASSED_FG + if powerline.args.prev_error != 0: + fg = powerline.theme.CMD_FAILED_FG + bg = powerline.theme.CMD_FAILED_BG + powerline.append(root_indicators[powerline.args.shell], fg, bg, sanitize=False) diff --git a/powerline_shell/segments/ruby_version.py b/powerline_shell/segments/ruby_version.py new file mode 100644 index 00000000..dc566fda --- /dev/null +++ b/powerline_shell/segments/ruby_version.py @@ -0,0 +1,22 @@ +import os +import subprocess +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + + try: + p1 = subprocess.Popen(['ruby', '-v'], stdout=subprocess.PIPE) + p2 = subprocess.Popen(['sed', "s/ (.*//"], stdin=p1.stdout, stdout=subprocess.PIPE) + ruby_and_gemset = p2.communicate()[0].decode('utf-8').rstrip() + + gem_set = os.environ.get('GEM_HOME', '@').split('@') + + if len(gem_set) > 1: + ruby_and_gemset += "@{}".format(gem_set.pop()) + + powerline.append(ruby_and_gemset, 15, 1) + except OSError: + return diff --git a/powerline_shell/segments/set_term_title.py b/powerline_shell/segments/set_term_title.py new file mode 100644 index 00000000..fedc0773 --- /dev/null +++ b/powerline_shell/segments/set_term_title.py @@ -0,0 +1,22 @@ +import os +import socket +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + term = os.getenv('TERM') + if not (('xterm' in term) or ('rxvt' in term)): + return + if powerline.args.shell == 'bash': + set_title = '\\[\\e]0;\\u@\\h: \\w\\a\\]' + elif powerline.args.shell == 'zsh': + set_title = '%{\033]0;%n@%m: %~\007%}' + else: + set_title = '\033]0;%s@%s: %s\007' % ( + os.getenv('USER'), + socket.gethostname().split('.')[0], + powerline.cwd, + ) + powerline.append(set_title, None, None, '') diff --git a/powerline_shell/segments/ssh.py b/powerline_shell/segments/ssh.py new file mode 100644 index 00000000..f64c967d --- /dev/null +++ b/powerline_shell/segments/ssh.py @@ -0,0 +1,11 @@ +import os +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + if os.getenv('SSH_CLIENT'): + powerline = self.powerline + powerline.append(' %s ' % powerline.network, + powerline.theme.SSH_FG, + powerline.theme.SSH_BG) diff --git a/powerline_shell/segments/stdout.py b/powerline_shell/segments/stdout.py new file mode 100644 index 00000000..9286245e --- /dev/null +++ b/powerline_shell/segments/stdout.py @@ -0,0 +1,17 @@ +import subprocess +from ..utils import ThreadedSegment + + +class Segment(ThreadedSegment): + def run(self): + cmd = self.segment_def["command"] + self.output = subprocess.check_output(cmd).decode("utf-8").strip() + # TODO handle OSError + # TODO handle no command defined or malformed + + def add_to_powerline(self): + self.join() + self.powerline.append( + " %s " % self.output, + self.segment_def.get("fg_color", self.powerline.theme.PATH_FG), + self.segment_def.get("bg_color", self.powerline.theme.PATH_BG)) diff --git a/powerline_shell/segments/svn.py b/powerline_shell/segments/svn.py new file mode 100644 index 00000000..6869cf9c --- /dev/null +++ b/powerline_shell/segments/svn.py @@ -0,0 +1,70 @@ +import subprocess +from ..utils import ThreadedSegment, RepoStats, get_subprocess_env + + +def _get_svn_revision(): + p = subprocess.Popen(["svn", "info", "--xml"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=get_subprocess_env()) + for line in p.communicate()[0].decode("utf-8").splitlines(): + if "revision" in line: + revision = line.split("=")[1].split('"')[1] + break + return revision + + +def parse_svn_stats(status): + stats = RepoStats() + for line in status: + if line[0] == "?": + stats.new += 1 + elif line[0] == "C": + stats.conflicted += 1 + elif line[0] in ["A", "D", "I", "M", "R", "!", "~"]: + stats.changed += 1 + return stats + + +def _get_svn_status(output): + """This function exists to enable mocking the `svn status` output in tests. + """ + return output[0].decode("utf-8").splitlines() + + +def build_stats(): + try: + p = subprocess.Popen(['svn', 'status'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=get_subprocess_env()) + except OSError: + # Popen will throw an OSError if svn is not found + return None, None + pdata = p.communicate() + if p.returncode != 0 or pdata[1][:22] == b'svn: warning: W155007:': + return None, None + status = _get_svn_status(pdata) + stats = parse_svn_stats(status) + revision = _get_svn_revision() + return stats, revision + + +class Segment(ThreadedSegment): + def run(self): + self.stats, self.revision = build_stats() + + def add_to_powerline(self): + self.join() + if not self.stats: + return + bg = self.powerline.theme.REPO_CLEAN_BG + fg = self.powerline.theme.REPO_CLEAN_FG + if self.stats.dirty: + bg = self.powerline.theme.REPO_DIRTY_BG + fg = self.powerline.theme.REPO_DIRTY_FG + if self.powerline.segment_conf("vcs", "show_symbol"): + symbol = " " + RepoStats().symbols["svn"] + else: + symbol = "" + self.powerline.append(symbol + " rev " + self.revision + " ", fg, bg) + self.stats.add_to_powerline(self.powerline) diff --git a/powerline_shell/segments/time.py b/powerline_shell/segments/time.py new file mode 100644 index 00000000..686b1dd8 --- /dev/null +++ b/powerline_shell/segments/time.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import +from ..utils import BasicSegment +import time + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + format = powerline.segment_conf('time', 'format') + if format: + time_ = ' %s ' % time.strftime(format) + elif powerline.args.shell == 'bash': + time_ = ' \\t ' + elif powerline.args.shell == 'zsh': + time_ = ' %* ' + else: + time_ = ' %s ' % time.strftime('%H:%M:%S') + powerline.append(time_, + powerline.theme.TIME_FG, + powerline.theme.TIME_BG) diff --git a/powerline_shell/segments/uptime.py b/powerline_shell/segments/uptime.py new file mode 100644 index 00000000..1c030611 --- /dev/null +++ b/powerline_shell/segments/uptime.py @@ -0,0 +1,20 @@ +import subprocess +import re +from ..utils import BasicSegment, decode + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + try: + output = decode(subprocess.check_output(['uptime'], stderr=subprocess.STDOUT)) + raw_uptime = re.search('(?<=up).+(?=,\s+\d+\s+user)', output).group(0) + day_search = re.search('\d+(?=\s+day)', output) + days = '' if not day_search else '%sd ' % day_search.group(0) + hour_search = re.search('\d{1,2}(?=\:)', raw_uptime) + hours = '' if not hour_search else '%sh ' % hour_search.group(0) + minutes = re.search('(?<=\:)\d{1,2}|\d{1,2}(?=\s+min)', raw_uptime).group(0) + uptime = u' %s%s%sm \u2191 ' % (days, hours, minutes) + powerline.append(uptime, powerline.theme.CWD_FG, powerline.theme.PATH_BG) + except OSError: + return diff --git a/powerline_shell/segments/username.py b/powerline_shell/segments/username.py new file mode 100644 index 00000000..f351afcc --- /dev/null +++ b/powerline_shell/segments/username.py @@ -0,0 +1,21 @@ +from ..utils import BasicSegment +import os +import getpass + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + if powerline.args.shell == "bash": + user_prompt = r" \u " + elif powerline.args.shell == "zsh": + user_prompt = " %n " + else: + user_prompt = " %s " % os.getenv("USER") + + if getpass.getuser() == "root": + bgcolor = powerline.theme.USERNAME_ROOT_BG + else: + bgcolor = powerline.theme.USERNAME_BG + + powerline.append(user_prompt, powerline.theme.USERNAME_FG, bgcolor) diff --git a/powerline_shell/segments/virtual_env.py b/powerline_shell/segments/virtual_env.py new file mode 100644 index 00000000..b7821098 --- /dev/null +++ b/powerline_shell/segments/virtual_env.py @@ -0,0 +1,18 @@ +import os +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + env = os.getenv('VIRTUAL_ENV') \ + or os.getenv('CONDA_ENV_PATH') \ + or os.getenv('CONDA_DEFAULT_ENV') + if os.getenv('VIRTUAL_ENV') \ + and os.path.basename(env) == '.venv': + env = os.path.basename(os.path.dirname(env)) + if not env: + return + env_name = os.path.basename(env) + bg = self.powerline.theme.VIRTUAL_ENV_BG + fg = self.powerline.theme.VIRTUAL_ENV_FG + self.powerline.append(" " + env_name + " ", fg, bg) diff --git a/powerline_shell/themes/__init__.py b/powerline_shell/themes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/powerline_shell/themes/basic.py b/powerline_shell/themes/basic.py new file mode 100644 index 00000000..bd4d2f9e --- /dev/null +++ b/powerline_shell/themes/basic.py @@ -0,0 +1,45 @@ +from powerline_shell.themes.default import DefaultColor + + +class Color(DefaultColor): + """Basic theme which only uses colors in 0-15 range""" + USERNAME_FG = 8 + USERNAME_BG = 15 + USERNAME_ROOT_BG = 1 + + HOSTNAME_FG = 8 + HOSTNAME_BG = 7 + + HOME_SPECIAL_DISPLAY = False + PATH_BG = 8 # dark grey + PATH_FG = 7 # light grey + CWD_FG = 15 # white + SEPARATOR_FG = 7 + + READONLY_BG = 1 + READONLY_FG = 15 + + REPO_CLEAN_BG = 2 # green + REPO_CLEAN_FG = 0 # black + REPO_DIRTY_BG = 1 # red + REPO_DIRTY_FG = 15 # white + + JOBS_FG = 14 + JOBS_BG = 8 + + CMD_PASSED_BG = 8 + CMD_PASSED_FG = 15 + CMD_FAILED_BG = 11 + CMD_FAILED_FG = 0 + + SVN_CHANGES_BG = REPO_DIRTY_BG + SVN_CHANGES_FG = REPO_DIRTY_FG + + VIRTUAL_ENV_BG = 2 + VIRTUAL_ENV_FG = 0 + + AWS_PROFILE_FG = 14 + AWS_PROFILE_BG = 8 + + TIME_FG = 8 + TIME_BG = 7 diff --git a/powerline_shell/themes/default.py b/powerline_shell/themes/default.py new file mode 100644 index 00000000..da0c0034 --- /dev/null +++ b/powerline_shell/themes/default.py @@ -0,0 +1,84 @@ +class DefaultColor(object): + """ + This class should have the default colors for every segment. + Please test every new segment with this theme first. + """ + # RESET is not a real color code. It is used as in indicator + # within the code that any foreground / background color should + # be cleared + RESET = -1 + + USERNAME_FG = 250 + USERNAME_BG = 240 + USERNAME_ROOT_BG = 124 + + HOSTNAME_FG = 250 + HOSTNAME_BG = 238 + + HOME_SPECIAL_DISPLAY = True + HOME_BG = 31 # blueish + HOME_FG = 15 # white + PATH_BG = 237 # dark grey + PATH_FG = 250 # light grey + CWD_FG = 254 # nearly-white grey + SEPARATOR_FG = 244 + + READONLY_BG = 124 + READONLY_FG = 254 + + SSH_BG = 166 # medium orange + SSH_FG = 254 + + REPO_CLEAN_BG = 148 # a light green color + REPO_CLEAN_FG = 0 # black + REPO_DIRTY_BG = 161 # pink/red + REPO_DIRTY_FG = 15 # white + + JOBS_FG = 39 + JOBS_BG = 238 + + CMD_PASSED_BG = 236 + CMD_PASSED_FG = 15 + CMD_FAILED_BG = 161 + CMD_FAILED_FG = 15 + + SVN_CHANGES_BG = 148 + SVN_CHANGES_FG = 22 # dark green + + GIT_AHEAD_BG = 240 + GIT_AHEAD_FG = 250 + GIT_BEHIND_BG = 240 + GIT_BEHIND_FG = 250 + GIT_STAGED_BG = 22 + GIT_STAGED_FG = 15 + GIT_NOTSTAGED_BG = 130 + GIT_NOTSTAGED_FG = 15 + GIT_UNTRACKED_BG = 52 + GIT_UNTRACKED_FG = 15 + GIT_CONFLICTED_BG = 9 + GIT_CONFLICTED_FG = 15 + + GIT_STASH_BG = 221 + GIT_STASH_FG = 0 + + VIRTUAL_ENV_BG = 35 # a mid-tone green + VIRTUAL_ENV_FG = 00 + + BATTERY_NORMAL_BG = 22 + BATTERY_NORMAL_FG = 7 + BATTERY_LOW_BG = 196 + BATTERY_LOW_FG = 7 + + AWS_PROFILE_FG = 39 + AWS_PROFILE_BG = 238 + + TIME_FG = 250 + TIME_BG = 238 + + +class Color(DefaultColor): + """ + This subclass is required when the user chooses to use 'default' theme. + Because the segments require a 'Color' class for every theme. + """ + pass diff --git a/powerline_shell/themes/gruvbox.py b/powerline_shell/themes/gruvbox.py new file mode 100644 index 00000000..1326ea46 --- /dev/null +++ b/powerline_shell/themes/gruvbox.py @@ -0,0 +1,111 @@ +from powerline_shell.themes.default import DefaultColor + +""" +absolute colors based on +https://github.com/morhetz/gruvbox/blob/master/colors/gruvbox.vim +""" +dark0 = 235 +dark1 = 237 +dark2 = 239 +dark3 = 241 +dark4 = 243 + +light0 = 229 +light1 = 223 +light2 = 250 +light3 = 248 +light4 = 246 + +dark_gray = 245 +light_gray = 244 + +neutral_red = 124 +neutral_green = 106 +neutral_yellow = 172 +neutral_blue = 66 +neutral_purple = 132 +neutral_aqua = 72 +neutral_orange = 166 + +bright_red = 167 +bright_green = 142 +bright_yellow = 214 +bright_blue = 109 +bright_purple = 175 +bright_aqua = 108 +bright_orange = 208 + +faded_red = 88 +faded_green = 100 +faded_yellow = 136 +faded_blue = 24 +faded_purple = 96 +faded_aqua = 66 +faded_orange = 130 + +class Color(DefaultColor): + USERNAME_ROOT_BG = faded_red + USERNAME_BG = dark2 + USERNAME_FG = bright_purple + + HOSTNAME_BG = dark1 + HOSTNAME_FG = bright_purple + + HOME_SPECIAL_DISPLAY = True + HOME_BG = neutral_blue + HOME_FG = light2 + PATH_BG = dark3 + PATH_FG = light3 + CWD_FG = light2 + SEPARATOR_FG = dark_gray + + READONLY_BG = bright_red + READONLY_FG = light0 + + SSH_BG = faded_purple + SSH_FG = light0 + + REPO_CLEAN_BG = faded_green + REPO_CLEAN_FG = dark1 + REPO_DIRTY_BG = faded_orange + REPO_DIRTY_FG = light0 + + JOBS_FG = neutral_aqua + JOBS_BG = dark1 + + CMD_PASSED_FG = light4 + CMD_PASSED_BG = dark1 + CMD_FAILED_FG = light0 + CMD_FAILED_BG = neutral_red + + SVN_CHANGES_FG = REPO_DIRTY_FG + SVN_CHANGES_BG = REPO_DIRTY_BG + + GIT_AHEAD_BG = dark2 + GIT_AHEAD_FG = light3 + GIT_BEHIND_BG = dark2 + GIT_BEHIND_FG = light3 + GIT_STAGED_BG = neutral_green + GIT_STAGED_FG = light0 + GIT_NOTSTAGED_BG = neutral_orange + GIT_NOTSTAGED_FG = light0 + GIT_UNTRACKED_BG = faded_red + GIT_UNTRACKED_FG = light0 + GIT_CONFLICTED_BG = neutral_red + GIT_CONFLICTED_FG = light0 + GIT_STASH_BG = neutral_yellow + GIT_STASH_FG = dark0 + + VIRTUAL_ENV_BG = faded_green + VIRTUAL_ENV_FG = light0 + + BATTERY_NORMAL_BG = neutral_green + BATTERY_NORMAL_FG = dark2 + BATTERY_LOW_BG = neutral_red + BATTERY_LOW_FG = light1 + + AWS_PROFILE_FG = neutral_aqua + AWS_PROFILE_BG = dark1 + + TIME_FG = light2 + TIME_BG = dark4 diff --git a/powerline_shell/themes/nord.py b/powerline_shell/themes/nord.py new file mode 100644 index 00000000..2c006bcb --- /dev/null +++ b/powerline_shell/themes/nord.py @@ -0,0 +1,89 @@ +from powerline_shell.themes.default import DefaultColor + +""" +colors from https://www.nordtheme.com/docs/colors-and-palettes +""" + +night0 = 236 # nord0 +night1 = 237 # nord1 +night2 = 238 # nord2 +night3 = 239 # nord3 +snow0 = 253 # nord4 +snow1 = 254 # nord5 +snow2 = 255 # nord6 +frost0 = 109 # nord7 +frost1 = 111 # nord8 +frost2 = 110 # nord9 +frost3 = 68 # nord10 +red = 167 # nord11 +orange = 173 # nord12 +yellow = 179 # nord13 +green = 150 # nord14 +purple = 139 # nord15 + +class Color(DefaultColor): + USERNAME_BG = night3 + USERNAME_FG = snow0 + + HOSTNAME_FG = snow0 + HOSTNAME_BG = night0 + + HOME_BG = frost2 + HOME_FG = snow2 + PATH_BG = night0 + PATH_FG = snow0 + CWD_FG = snow0 + SEPARATOR_FG = night3 + + READONLY_BG = red + READONLY_FG = snow2 + + SSH_BG = orange + SSH_FG = snow2 + + REPO_CLEAN_BG = green + REPO_CLEAN_FG = night1 + REPO_DIRTY_BG = red + REPO_DIRTY_FG = snow2 + + JOBS_FG = frost3 + JOBS_BG = night0 + + CMD_PASSED_BG = night0 + CMD_PASSED_FG = snow2 + CMD_FAILED_BG = yellow + CMD_FAILED_FG = snow2 + + SVN_CHANGES_BG = REPO_DIRTY_FG + SVN_CHANGES_FG = REPO_DIRTY_BG + + GIT_AHEAD_BG = night3 + GIT_AHEAD_FG = snow0 + GIT_BEHIND_BG = night3 + GIT_BEHIND_FG = snow0 + + GIT_STAGED_BG = frost0 + GIT_STAGED_FG = night1 + GIT_NOTSTAGED_BG = orange + GIT_NOTSTAGED_FG = snow2 + GIT_UNTRACKED_BG = purple + GIT_UNTRACKED_FG = snow2 + GIT_CONFLICTED_BG = red + GIT_CONFLICTED_FG = snow2 + + GIT_STASH_BG = yellow + GIT_STASH_FG = night1 + + VIRTUAL_ENV_BG = green + VIRTUAL_ENV_FG = night1 + + BATTERY_NORMAL_BG = green + BATTERY_NORMAL_FG = night1 + BATTERY_LOW_BG = red + BATTERY_LOW_FG = snow2 + + AWS_PROFILE_FG = frost3 + AWS_PROFILE_BG = night0 + + TIME_BG = night3 + TIME_FG = snow0 diff --git a/powerline_shell/themes/solarized_dark.py b/powerline_shell/themes/solarized_dark.py new file mode 100644 index 00000000..ec0f41bf --- /dev/null +++ b/powerline_shell/themes/solarized_dark.py @@ -0,0 +1,44 @@ +from powerline_shell.themes.default import DefaultColor + + +class Color(DefaultColor): + USERNAME_FG = 15 + USERNAME_BG = 4 + USERNAME_ROOT_BG = 1 + + HOSTNAME_FG = 15 + HOSTNAME_BG = 10 + + HOME_SPECIAL_DISPLAY = False + PATH_FG = 7 + PATH_BG = 10 + CWD_FG = 15 + SEPARATOR_FG = 14 + + READONLY_BG = 1 + READONLY_FG = 7 + + REPO_CLEAN_FG = 14 + REPO_CLEAN_BG = 0 + REPO_DIRTY_FG = 3 + REPO_DIRTY_BG = 0 + + JOBS_FG = 4 + JOBS_BG = 8 + + CMD_PASSED_FG = 15 + CMD_PASSED_BG = 2 + CMD_FAILED_FG = 15 + CMD_FAILED_BG = 1 + + SVN_CHANGES_FG = REPO_DIRTY_FG + SVN_CHANGES_BG = REPO_DIRTY_BG + + VIRTUAL_ENV_BG = 15 + VIRTUAL_ENV_FG = 2 + + AWS_PROFILE_FG = 7 + AWS_PROFILE_BG = 2 + + TIME_FG = 15 + TIME_BG = 10 diff --git a/powerline_shell/themes/solarized_light.py b/powerline_shell/themes/solarized_light.py new file mode 100644 index 00000000..d5a14d6f --- /dev/null +++ b/powerline_shell/themes/solarized_light.py @@ -0,0 +1,41 @@ +from powerline_shell.themes.default import DefaultColor + + +class Color(DefaultColor): + USERNAME_FG = 15 + USERNAME_BG = 4 + USERNAME_ROOT_BG = 1 + + HOSTNAME_FG = 15 + HOSTNAME_BG = 10 + + HOME_SPECIAL_DISPLAY = False + PATH_FG = 10 + PATH_BG = 7 + CWD_FG = 0 + SEPARATOR_FG = 14 + + READONLY_BG = 1 + READONLY_FG = 7 + + REPO_CLEAN_FG = 0 + REPO_CLEAN_BG = 15 + REPO_DIRTY_FG = 1 + REPO_DIRTY_BG = 15 + + JOBS_FG = 4 + JOBS_BG = 7 + + CMD_PASSED_FG = 15 + CMD_PASSED_BG = 2 + CMD_FAILED_FG = 15 + CMD_FAILED_BG = 1 + + SVN_CHANGES_FG = REPO_DIRTY_FG + SVN_CHANGES_BG = REPO_DIRTY_BG + + VIRTUAL_ENV_BG = 15 + VIRTUAL_ENV_FG = 2 + + TIME_FG = 15 + TIME_BG = 10 diff --git a/powerline_shell/themes/washed.py b/powerline_shell/themes/washed.py new file mode 100644 index 00000000..d2935095 --- /dev/null +++ b/powerline_shell/themes/washed.py @@ -0,0 +1,44 @@ +from powerline_shell.themes.default import DefaultColor + + +class Color(DefaultColor): + USERNAME_FG = 8 + USERNAME_BG = 251 + USERNAME_ROOT_BG = 209 + + HOSTNAME_FG = 8 + HOSTNAME_BG = 7 + + HOME_SPECIAL_DISPLAY = False + PATH_BG = 15 + PATH_FG = 8 + CWD_FG = 8 + SEPARATOR_FG = 251 + + READONLY_BG = 209 + READONLY_FG = 15 + + REPO_CLEAN_BG = 150 # pale green + REPO_CLEAN_FG = 235 + REPO_DIRTY_BG = 203 # pale red + REPO_DIRTY_FG = 15 + + JOBS_FG = 14 + JOBS_BG = 8 + + CMD_PASSED_BG = 7 + CMD_PASSED_FG = 8 + CMD_FAILED_BG = 9 + CMD_FAILED_FG = 15 + + SVN_CHANGES_BG = REPO_DIRTY_BG + SVN_CHANGES_FG = REPO_DIRTY_FG + + VIRTUAL_ENV_BG = 150 + VIRTUAL_ENV_FG = 0 + + AWS_PROFILE_FG = 0 + AWS_PROFILE_BG = 7 + + TIME_FG = 8 + TIME_BG = 7 diff --git a/powerline_shell/utils.py b/powerline_shell/utils.py new file mode 100644 index 00000000..76c45d03 --- /dev/null +++ b/powerline_shell/utils.py @@ -0,0 +1,152 @@ +import sys +import os +import threading + +py3 = sys.version_info[0] == 3 + +if py3: + def unicode_(x): + return str(x) + def decode(x): + return x.decode("utf-8") +else: + unicode_ = unicode + decode = unicode + + +class RepoStats(object): + symbols = { + 'detached': u'\u2693', + 'ahead': u'\u2B06', + 'behind': u'\u2B07', + 'staged': u'\u2714', + 'changed': u'\u270E', + 'new': u'?', + 'conflicted': u'\u273C', + 'stash': u'\u2398', + 'git': u'\uE0A0', + 'hg': u'\u263F', + 'bzr': u'\u2B61\u20DF', + 'fossil': u'\u2332', + 'svn': u'\u2446' + } + + def __init__(self, ahead=0, behind=0, new=0, changed=0, staged=0, conflicted=0): + self.ahead = ahead + self.behind = behind + self.new = new + self.changed = changed + self.staged = staged + self.conflicted = conflicted + + def __eq__(self, other): + return ( + self.ahead == other.ahead and + self.behind == other.behind and + self.new == other.new and + self.changed == other.changed and + self.staged == other.staged and + self.conflicted == other.conflicted + ) + + @property + def dirty(self): + qualifiers = [ + self.new, + self.changed, + self.staged, + self.conflicted, + ] + return sum(qualifiers) > 0 + + def __getitem__(self, _key): + return getattr(self, _key) + + def n_or_empty(self, _key): + """Given a string name of one of the properties of this class, returns + the value of the property as a string when the value is greater than + 1. When it is not greater than one, returns an empty string. + + As an example, if you want to show an icon for new files, but you only + want a number to appear next to the icon when there are more than one + new file, you can do: + + segment = repo_stats.n_or_empty("new") + icon_string + """ + return unicode_(self[_key]) if int(self[_key]) > 1 else u'' + + def add_to_powerline(self, powerline): + def add(_key, fg, bg): + if self[_key]: + s = u" {}{} ".format(self.n_or_empty(_key), self.symbols[_key]) + powerline.append(s, fg, bg) + color = powerline.theme + add('ahead', color.GIT_AHEAD_FG, color.GIT_AHEAD_BG) + add('behind', color.GIT_BEHIND_FG, color.GIT_BEHIND_BG) + add('staged', color.GIT_STAGED_FG, color.GIT_STAGED_BG) + add('changed', color.GIT_NOTSTAGED_FG, color.GIT_NOTSTAGED_BG) + add('new', color.GIT_UNTRACKED_FG, color.GIT_UNTRACKED_BG) + add('conflicted', color.GIT_CONFLICTED_FG, color.GIT_CONFLICTED_BG) + + +def warn(msg): + print('[powerline-bash] ', msg) + + +class BasicSegment(object): + def __init__(self, powerline, segment_def): + self.powerline = powerline + self.segment_def = segment_def # type: dict + + def start(self): + pass + + +class ThreadedSegment(threading.Thread): + def __init__(self, powerline, segment_def): + super(ThreadedSegment, self).__init__() + self.powerline = powerline + self.segment_def = segment_def # type: dict + + +def import_file(module_name, path): + # An implementation of https://stackoverflow.com/a/67692/683436 + if py3 and sys.version_info[1] >= 5: + import importlib.util + spec = importlib.util.spec_from_file_location(module_name, path) + if not spec: + raise ImportError() + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + elif py3: + from importlib.machinery import SourceFileLoader + return SourceFileLoader(module_name, path).load_module() + else: + import imp + return imp.load_source(module_name, path) + + +def get_PATH(): + """Normally gets the PATH from the OS. This function exists to enable + easily mocking the PATH in tests. + """ + return os.getenv("PATH") + + +def get_subprocess_env(**envs): + defaults = { + # https://github.com/milkbikis/powerline-shell/pull/153 + "PATH": get_PATH(), + } + defaults.update(envs) + env = dict(os.environ) + env.update(defaults) + return env + + +def get_git_subprocess_env(): + # LANG is specified to ensure git always uses a language we are expecting. + # Otherwise we may be unable to parse the output. + return get_subprocess_env(LANG="C") + diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..59d5eef7 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +nose>=1.3.7 +mock>=1.3.0 +sh>=1.11 +parameterized>=0.6.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..1eee7db0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +universal = 1 + +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..87482e21 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +from setuptools import setup, find_packages + +setup( + name="powerline-shell", + version="0.7.0", + description="A pretty prompt for your shell", + author="Buck Ryan", + author_email="buck@buckryan.com", + license="MIT", + url="https://github.com/b-ryan/powerline-shell", + classifiers=[ + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + ], + packages=[ + "powerline_shell", + "powerline_shell.segments", + "powerline_shell.themes", + ], + install_requires=[ + "argparse", + ], + entry_points=""" + [console_scripts] + powerline-shell=powerline_shell:main + """, +) diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..7bfee70c --- /dev/null +++ b/test.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu +docker build -t powerline-shell . +docker run --rm --interactive --tty \ + --volume $PWD:/code \ + powerline-shell "$@" diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/color_compliment_test.py b/test/color_compliment_test.py new file mode 100644 index 00000000..47f72d07 --- /dev/null +++ b/test/color_compliment_test.py @@ -0,0 +1,49 @@ +import unittest +from parameterized import parameterized + +from powerline_shell.color_compliment import getOppositeColor + + +def build_inputs(): + + # Build 768 hex/rgb values to test against getOppositeColor + input_bytes = map(hex, xrange(pow(2,8))) + + input_list = [] + for x in input_bytes: + + #Building hex range of [00-ff]:00:00 + combined1 = hex((int(x,16)<<16)| ((int(input_bytes[0],16)<<8)|int(input_bytes[0],16))) + test_input1 = tuple((int(x,16), int(input_bytes[0],16), int(input_bytes[0],16))) + + #Building hex range of 00:[00-ff]:00 + combined2 = hex((int(input_bytes[0],16)<<16)| ((int(x,16)<<8)|int(input_bytes[0],16))) + test_input2 = tuple((int(input_bytes[0],16), int(x,16), int(input_bytes[0],16))) + + #Building hex range of 00:00:[00-ff] + combined3 = hex((int(input_bytes[0],16)<<16)| ((int(input_bytes[0],16)<<8)|int(x,16))) + test_input3 = tuple((int(input_bytes[0],16), int(input_bytes[0],16), int(x,16))) + + input_list.append(tuple((combined1, test_input1))) + input_list.append(tuple((combined2, test_input2))) + input_list.append(tuple((combined3, test_input3))) + + return input_list + +class getOppositeColorTestCase(unittest.TestCase): + + ''' + Test only runs against 768 combinations of rgb values. + Trying to run parameterized unittest against 16.77M rgb values + was near impossible (need lots of memory and time). Of the 768 + values tested, the test has proven to catch 192 exceptions (and 3 + ZeroDivisionError exceptions from rgb_to_hls). This can be tested + by commenting out the first line of getOppositeColor, which + converts the rgb values to float. + ''' + + @parameterized.expand(build_inputs) + + def test_rgb_input_get_opposite_not_negative(self, name, test_input): + negative = -1 + self.assertNotIn(negative, getOppositeColor(*test_input), u'{0:#08x} returns negative number in rgb tuple'.format(int(name,16))) diff --git a/test/cwd_test.py b/test/cwd_test.py new file mode 100644 index 00000000..ed52dc36 --- /dev/null +++ b/test/cwd_test.py @@ -0,0 +1,53 @@ +import unittest +import mock +import os +import tempfile +import shutil +import powerline_shell as p + + +class CwdTest(unittest.TestCase): + + def setUp(self): + self.dirname = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.dirname) + + @mock.patch('os.getenv') + @mock.patch('powerline_shell.warn') + def test_normal(self, warn, getenv): + getenv.return_value = self.dirname + self.assertEqual(p.get_valid_cwd(), self.dirname) + self.assertEqual(warn.call_count, 0) + + @mock.patch('os.getenv') + @mock.patch('powerline_shell.warn') + def test_nonexistent_warns(self, warn, getenv): + subdir = os.path.join(self.dirname, 'subdir') + getenv.return_value = subdir + self.assertEqual(p.get_valid_cwd(), subdir) + self.assertEqual(warn.call_count, 1) + + @mock.patch('os.getenv') + @mock.patch('powerline_shell.warn') + def test_falls_back_to_getcwd(self, warn, getenv): + getenv.return_value = None + os.chdir(self.dirname) + self.assertEqual(p.get_valid_cwd(), self.dirname) + self.assertEqual(warn.call_count, 0) + + @mock.patch('os.getenv') + @mock.patch('powerline_shell.warn') + def test_nonexistent_getcwd_warns(self, warn, getenv): + subdir = os.path.join(self.dirname, 'subdir') + getenv.return_value = None + + os.mkdir(subdir) + os.chdir(subdir) + os.rmdir(subdir) + + with self.assertRaises(SystemExit) as e: + p.get_valid_cwd() + + self.assertEqual(warn.call_count, 1) diff --git a/test/repo_stats_test.py b/test/repo_stats_test.py new file mode 100644 index 00000000..123259ba --- /dev/null +++ b/test/repo_stats_test.py @@ -0,0 +1,25 @@ +import unittest +from powerline_shell.utils import RepoStats + + +class RepoStatsTest(unittest.TestCase): + + def setUp(self): + self.repo_stats = RepoStats() + self.repo_stats.changed = 1 + self.repo_stats.conflicted = 4 + + def test_dirty(self): + self.assertTrue(self.repo_stats.dirty) + + def test_simple(self): + self.assertEqual(self.repo_stats.new, 0) + + def test_n_or_empty__empty(self): + self.assertEqual(self.repo_stats.n_or_empty("changed"), u"") + + def test_n_or_empty__n(self): + self.assertEqual(self.repo_stats.n_or_empty("conflicted"), u"4") + + def test_index(self): + self.assertEqual(self.repo_stats["changed"], 1) diff --git a/test/segments_test/__init__.py b/test/segments_test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/segments_test/bzr_test.py b/test/segments_test/bzr_test.py new file mode 100644 index 00000000..73dbebae --- /dev/null +++ b/test/segments_test/bzr_test.py @@ -0,0 +1,82 @@ +import unittest +import mock +import tempfile +import shutil +import sh +import powerline_shell.segments.bzr as bzr +from powerline_shell.utils import RepoStats +from ..testing_utils import dict_side_effect_fn + + +test_cases = ( + (["unknown:"," new-file"], RepoStats(new=1)), + (["added:"," added-file"], RepoStats(staged=1)), + (["modified:"," modified-file"], RepoStats(changed=1)), + (["removed:"," removed-file"], RepoStats(changed=1)), + (["missing:"," missing-file"], RepoStats(changed=1)), + (["renamed:"," renamed-file"], RepoStats(changed=1)), + (["kind changed:"," kind-changed-file"], RepoStats(changed=1)) +) + + +class BzrTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + self.powerline.segment_conf.side_effect = dict_side_effect_fn({ + ("vcs", "show_symbol"): False, + }) + + self.dirname = tempfile.mkdtemp() + sh.cd(self.dirname) + sh.bzr("init-repo", ".") + sh.mkdir("trunk") + sh.cd("trunk") + sh.bzr("init") + + self.segment = bzr.Segment(self.powerline, {}) + + def tearDown(self): + shutil.rmtree(self.dirname) + + def _add_and_commit(self, filename): + sh.touch(filename) + sh.bzr("add", filename) + sh.bzr("commit", "-m", "add file " + filename) + + def _checkout_new_branch(self, branch): + sh.cd("..") + sh.bzr("branch", "trunk", branch) + sh.cd(branch) + + @mock.patch("powerline_shell.utils.get_PATH") + def test_bzr_not_installed(self, get_PATH): + get_PATH.return_value = "" # so bzr can't be found + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_non_bzr_directory(self): + shutil.rmtree(".bzr") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_trunk(self): + self._add_and_commit("foo") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], " trunk ") + + def test_different_branch(self): + self._add_and_commit("foo") + self._checkout_new_branch("bar") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], " bar ") + + @mock.patch('powerline_shell.segments.bzr._get_bzr_status') + def test_all(self, check_output): + for stdout, result in test_cases: + stats = bzr.parse_bzr_stats(stdout) + self.assertEquals(result, stats) diff --git a/test/segments_test/fossil_test.py b/test/segments_test/fossil_test.py new file mode 100644 index 00000000..34f914af --- /dev/null +++ b/test/segments_test/fossil_test.py @@ -0,0 +1,75 @@ +import unittest +import mock +import tempfile +import shutil +import sh +import powerline_shell.segments.fossil as fossil +from powerline_shell.utils import RepoStats +from ..testing_utils import dict_side_effect_fn + +test_cases = { + "EXTRA new-file": RepoStats(new=1), + "EDITED modified-file": RepoStats(changed=1), + "CONFLICT conflicted-file": RepoStats(conflicted=1), + "ADDED added-file": RepoStats(staged=1), +} + + +class FossilTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + self.powerline.segment_conf.side_effect = dict_side_effect_fn({ + ("vcs", "show_symbol"): False, + }) + + self.dirname = tempfile.mkdtemp() + sh.cd(self.dirname) + sh.fossil("init", "test.fossil") + sh.fossil("open", "test.fossil") + + self.segment = fossil.Segment(self.powerline, {}) + + def tearDown(self): + shutil.rmtree(self.dirname) + + def _add_and_commit(self, filename): + sh.touch(filename) + sh.fossil("add", filename) + sh.fossil("commit", "-m", "add file " + filename) + + def _checkout_new_branch(self, branch): + sh.fossil("branch", "new", branch, "trunk") + sh.fossil("checkout", branch) + + @mock.patch("powerline_shell.utils.get_PATH") + def test_fossil_not_installed(self, get_PATH): + get_PATH.return_value = "" # so fossil can't be found + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_non_fossil_directory(self): + sh.fossil("close", "--force") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_standard(self): + self._add_and_commit("foo") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], " trunk ") + + def test_different_branch(self): + self._add_and_commit("foo") + self._checkout_new_branch("bar") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], " bar ") + + @mock.patch('powerline_shell.segments.fossil._get_fossil_status') + def test_all(self, check_output): + for stdout, result in test_cases.items(): + stats = fossil.parse_fossil_stats([stdout]) + self.assertEquals(result, stats) diff --git a/test/segments_test/git_stash_test.py b/test/segments_test/git_stash_test.py new file mode 100644 index 00000000..1b228cd6 --- /dev/null +++ b/test/segments_test/git_stash_test.py @@ -0,0 +1,77 @@ +import unittest +import mock +import tempfile +import shutil +import sh +import powerline_shell.segments.git_stash as git_stash +from powerline_shell.utils import RepoStats + + +class GitStashTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + self.dirname = tempfile.mkdtemp() + sh.cd(self.dirname) + sh.git("init", ".") + + self.segment = git_stash.Segment(self.powerline, {}) + + def tearDown(self): + shutil.rmtree(self.dirname) + + def _add_and_commit(self, filename): + sh.touch(filename) + sh.git("add", filename) + sh.git("commit", "-m", "add file " + filename) + + def _overwrite_file(self, filename, content): + sh.echo(content, _out=filename) + + def _stash(self): + sh.git("stash") + + @mock.patch('powerline_shell.utils.get_PATH') + def test_git_not_installed(self, get_PATH): + get_PATH.return_value = "" # so git can't be found + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_non_git_directory(self): + shutil.rmtree(".git") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_no_stashes(self): + self._add_and_commit("foo") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_one_stash(self): + self._add_and_commit("foo") + self._overwrite_file("foo", "some new content") + self._stash() + self.segment.start() + self.segment.add_to_powerline() + expected = u' {} '.format(RepoStats.symbols["stash"]) + self.assertEqual(self.powerline.append.call_args[0][0], expected) + + def test_multiple_stashes(self): + self._add_and_commit("foo") + + self._overwrite_file("foo", "some new content") + self._stash() + + self._overwrite_file("foo", "some different content") + self._stash() + + self._overwrite_file("foo", "more different content") + self._stash() + + self.segment.start() + self.segment.add_to_powerline() + expected = u' 3{} '.format(RepoStats.symbols["stash"]) + self.assertEqual(self.powerline.append.call_args[0][0], expected) diff --git a/test/segments_test/git_test.py b/test/segments_test/git_test.py new file mode 100644 index 00000000..373fd68f --- /dev/null +++ b/test/segments_test/git_test.py @@ -0,0 +1,80 @@ +import unittest +import mock +import tempfile +import shutil +import sh +import powerline_shell.segments.git as git +from ..testing_utils import dict_side_effect_fn + + +class GitTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + self.powerline.segment_conf.side_effect = dict_side_effect_fn({ + ("vcs", "show_symbol"): False, + }) + + self.dirname = tempfile.mkdtemp() + sh.cd(self.dirname) + sh.git("init", ".") + + self.segment = git.Segment(self.powerline, {}) + + def tearDown(self): + shutil.rmtree(self.dirname) + + def _add_and_commit(self, filename): + sh.touch(filename) + sh.git("add", filename) + sh.git("commit", "-m", "add file " + filename) + + def _checkout_new_branch(self, branch): + sh.git("checkout", "-b", branch) + + def _get_commit_hash(self): + return sh.git("rev-parse", "HEAD") + + @mock.patch('powerline_shell.utils.get_PATH') + def test_git_not_installed(self, get_PATH): + get_PATH.return_value = "" # so git can't be found + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_non_git_directory(self): + shutil.rmtree(".git") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_big_bang(self): + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], ' Big Bang ') + + def test_master_branch(self): + self._add_and_commit("foo") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], ' master ') + + def test_different_branch(self): + self._add_and_commit("foo") + self._checkout_new_branch("bar") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], ' bar ') + + def test_detached(self): + self._add_and_commit("foo") + commit_hash = self._get_commit_hash() + self._add_and_commit("bar") + sh.git("checkout", "HEAD^") + self.segment.start() + self.segment.add_to_powerline() + + # In detached mode, we output a unicode symbol and then the shortened + # commit hash. + self.assertIn(self.powerline.append.call_args[0][0].split()[1], + commit_hash) diff --git a/test/segments_test/hg_test.py b/test/segments_test/hg_test.py new file mode 100644 index 00000000..004eac9c --- /dev/null +++ b/test/segments_test/hg_test.py @@ -0,0 +1,75 @@ +import unittest +import mock +import tempfile +import shutil +import sh +import powerline_shell.segments.hg as hg +from powerline_shell.utils import RepoStats +from ..testing_utils import dict_side_effect_fn + + +test_cases = { + "? new-file": RepoStats(new=1), + "M modified-file": RepoStats(changed=1), + "R removed-file": RepoStats(changed=1), + "! missing-file": RepoStats(changed=1), + "A added-file": RepoStats(staged=1), +} + + +class HgTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + self.powerline.segment_conf.side_effect = dict_side_effect_fn({ + ("vcs", "show_symbol"): False, + }) + + self.dirname = tempfile.mkdtemp() + sh.cd(self.dirname) + sh.hg("init", ".") + + self.segment = hg.Segment(self.powerline, {}) + + def tearDown(self): + shutil.rmtree(self.dirname) + + def _add_and_commit(self, filename): + sh.touch(filename) + sh.hg("add", filename) + sh.hg("commit", "-m", "add file " + filename) + + def _checkout_new_branch(self, branch): + sh.hg("branch", branch) + + @mock.patch("powerline_shell.utils.get_PATH") + def test_hg_not_installed(self, get_PATH): + get_PATH.return_value = "" # so hg can"t be found + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_non_hg_directory(self): + shutil.rmtree(".hg") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) + + def test_standard(self): + self._add_and_commit("foo") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], " default ") + + def test_different_branch(self): + self._add_and_commit("foo") + self._checkout_new_branch("bar") + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0], " bar ") + + @mock.patch('powerline_shell.segments.hg._get_hg_status') + def test_all(self, check_output): + for stdout, result in test_cases.items(): + stats = hg.parse_hg_stats([stdout]) + self.assertEquals(result, stats) diff --git a/test/segments_test/hostname_test.py b/test/segments_test/hostname_test.py new file mode 100644 index 00000000..05ae1f1b --- /dev/null +++ b/test/segments_test/hostname_test.py @@ -0,0 +1,21 @@ +import unittest +import mock +import powerline_shell.segments.hostname as hostname +from powerline_shell.themes.default import Color +from argparse import Namespace + + +class HostnameTest(unittest.TestCase): + def setUp(self): + self.powerline = mock.MagicMock() + self.powerline.theme = Color + self.segment = hostname.Segment(self.powerline, {}) + + def test_colorize(self): + self.powerline.segment_conf.return_value = True + self.segment.start() + self.segment.add_to_powerline() + args = self.powerline.append.call_args[0] + self.assertNotEqual(args[0], r" \h ") + self.assertNotEqual(args[1], Color.HOSTNAME_FG) + self.assertNotEqual(args[2], Color.HOSTNAME_BG) diff --git a/test/segments_test/svn_test.py b/test/segments_test/svn_test.py new file mode 100644 index 00000000..d7d821f8 --- /dev/null +++ b/test/segments_test/svn_test.py @@ -0,0 +1,32 @@ +import tempfile +import unittest +import shutil +import mock +import sh +import powerline_shell.segments.svn as svn +from ..testing_utils import dict_side_effect_fn + + +class SvnTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + self.powerline.segment_conf.side_effect = dict_side_effect_fn({ + ("vcs", "show_symbol"): False, + }) + + self.dirname = tempfile.mkdtemp() + sh.cd(self.dirname) + # sh.svn("init", ".") + + self.segment = svn.Segment(self.powerline, {}) + + def tearDown(self): + shutil.rmtree(self.dirname) + + @mock.patch("powerline_shell.utils.get_PATH") + def test_svn_not_installed(self, get_PATH): + get_PATH.return_value = "" # so svn can't be found + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_count, 0) diff --git a/test/segments_test/uptime_test.py b/test/segments_test/uptime_test.py new file mode 100644 index 00000000..676b193e --- /dev/null +++ b/test/segments_test/uptime_test.py @@ -0,0 +1,30 @@ +import unittest +import mock +import powerline_shell.segments.uptime as uptime + +test_cases = { + # linux test cases + "00:00:00 up 1:00, 2 users, load average: 0,00, 0,00, 0,00": "1h", + "00:00:00 up 10:00, 2 users, load average: 0,00, 0,00, 0,00": "10h", + "00:00:00 up 1 days, 1:00, 2 users, load average: 0,00, 0,00, 0,00": "1d", + "00:00:00 up 12 days, 1:00, 2 users, load average: 0,00, 0,00, 0,00": "12d", + "00:00:00 up 120 days, 49 min, 2 users, load average: 0,00, 0,00, 0,00": "120d", + + # mac test cases + "00:00:00 up 23 3 day(s), 10:00, 2 users, load average: 0,00, 0,00, 0,00": "3d", +} + + +class UptimeTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + self.segment = uptime.Segment(self.powerline, {}) + + @mock.patch('subprocess.check_output') + def test_all(self, check_output): + for stdout, result in test_cases.items(): + check_output.return_value = stdout + self.segment.start() + self.segment.add_to_powerline() + self.assertEqual(self.powerline.append.call_args[0][0].split()[0], result) diff --git a/test/testing_utils.py b/test/testing_utils.py new file mode 100644 index 00000000..e61e4c2b --- /dev/null +++ b/test/testing_utils.py @@ -0,0 +1,4 @@ +def dict_side_effect_fn(dict_): + def func(*args): + return dict_[args] + return func