diff --git a/.gitignore b/.gitignore index 738abd7..ec4610b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +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 0000000..a587362 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: python +script: | + ./setup.py install + pip install -r requirements-dev.txt + nosetests diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ea61c5d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,157 @@ +# Changes + +Unreleased + +* 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)) + +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/README.md b/README.md index 4631be2..c2c5b96 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,90 @@ -A Powerline style prompt for your shell -======================================= +# A Powerline style prompt for your shell -A [Powerline](https://github.com/Lokaltog/vim-powerline) like prompt for Bash, ZSH and Fish: +A [Powerline](https://github.com/Lokaltog/vim-powerline) like prompt for Bash, +ZSH and Fish: -![MacVim+Solarized+Powerline+CtrlP](https://raw.github.com/milkbikis/dotfiles-mac/master/bash-powerline-screenshot.png) +![MacVim+Solarized+Powerline+CtrlP](https://raw.github.com/banga/powerline-shell/master/bash-powerline-screenshot.png) -* 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. +- 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. -### Version Control + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Version Control](#version-control) +- [Setup](#setup) + - [Bash](#bash) + - [ZSH](#zsh) + - [Fish](#fish) +- [Customization](#customization) + - [Config File](#config-file) + - [Adding, Removing and Re-arranging segments](#adding-removing-and-re-arranging-segments) + - [Segment Separator](#segment-separator) + - [Themes](#themes) + - [Segment Configuration](#segment-configuration) + - [Contributing new types of segments](#contributing-new-types-of-segments) +- [Troubleshooting](#troubleshooting) + + + +## Version Control All of the version control systems supported by powerline shell give you a quick look into the state of your repo: -* The current branch is displayed and changes background color when the +- 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 +- 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 In addition, git has a few extra symbols: -* `✎` -- a file has been modified, but not staged for commit -* `✔` -- a file is staged for commit -* `✼` -- a file has conflicts +- `✎` -- a file has been modified, but not staged for commit +- `✔` -- a file is staged for commit +- `✼` -- a file has conflicts FIXME - * A `+` appears when untracked files are present (except for git, which - uses `?` instead) + +- A `+` appears when untracked files are present (except for git, which uses + `?` instead) Each of these will have a number next to it if more than one file matches. -# Setup +## Setup 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. -* Patch the font you use for your terminal: see https://github.com/Lokaltog/powerline-fonts +- 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 - * 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 - -* Clone this repository somewhere: - - git clone https://github.com/milkbikis/powerline-shell - -* Copy `config.py.dist` to `config.py` and edit it to configure the segments you want. Then run - - ./install.py - - * This will generate `powerline-shell.py` - -* (optional) Create a symlink to this python script in your home: - - ln -s ~/powerline-shell.py - - * If you don't want the symlink, just modify the path in the commands below - -* For python2.6 you have to install argparse - - pip install argparse - -### All Shells: -There are a few optional arguments which can be seen by running `powerline-shell.py --help`. +- Install using pip: ``` - --cwd-mode {fancy,plain,dironly} - How to display the current directory - --cwd-max-depth CWD_MAX_DEPTH - Maximum number of directories to show in path - --cwd-max-dir-size CWD_MAX_DIR_SIZE - Maximum number of letters displayed for each directory - in the path - --colorize-hostname Colorize the hostname based on a hash of itself. - --mode {patched,compatible,flat} - The characters used to make separators between - segments +pip install powerline-shell ``` -### Bash: +- Setup your shell prompt using the instructions for your shell below. + +### Bash + Add the following to your `.bashrc` (or `.profile` on Mac): ``` function _update_ps1() { - PS1="$(~/powerline-shell.py $? 2> /dev/null)" + PS1="$(powerline-shell $?)" } if [ "$TERM" != "linux" ]; then @@ -95,12 +92,13 @@ if [ "$TERM" != "linux" ]; then fi ``` -### ZSH: +### ZSH + Add the following to your `.zshrc`: ``` function powerline_precmd() { - PS1="$(~/powerline-shell.py $? --shell zsh 2> /dev/null)" + PS1="$(powerline-shell --shell zsh $?)" } function install_powerline_precmd() { @@ -117,32 +115,126 @@ if [ "$TERM" != "linux" ]; then fi ``` -### Fish: +### Fish + Redefine `fish_prompt` in ~/.config/fish/config.fish: ``` function fish_prompt - ~/powerline-shell.py $status --shell bare ^/dev/null + powerline-shell --shell bare $status end ``` -# Customization +## Customization + +### Config File + +Powerline-shell is customizable through the use of a config file. This file is +expected to be located at `~/.powerline-shell.json`. You can generate the +default config at this location using: + +``` +powerline-shell --generate-config > ~/.powerline-shell.json +``` + +(You can see an example config file +[here](https://github.com/b-ryan/dotfiles/blob/master/home/powerline-shell.json)) ### Adding, Removing and Re-arranging segments -The `config.py` file defines which segments are drawn and in which order. Simply -comment out and rearrange segment names to get your desired arrangement. Every -time you change `config.py`, run `install.py`, which will generate a new -`powerline-shell.py` customized to your configuration. You should see the new -prompt immediately. +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 +are: + +- `bzr` - Details about the current Bazaar repo. +- `cwd` - Shows your current working directory. See [Segment + Configuration](#segment-configuration) for some options. +- `exit_code` - When the previous command ends in a non-zero status, shows the + value of the exist status in red. +- `fossil` - Details about the current Fossil repo. +- `git` - Details about the current Git repo. +- `hg` - Details about the current Mercurial repo. +- `hostname` - Current machine's hostname. +- `jobs` - Number of background jobs currently running. +- `newline` - Inserts a newline into the prompt. +- `node_version` - `node --version` +- `npm_version` - `npm --version` +- `php_version` - Version of php on the machine +- `rbenv` - `rbenv local` +- `read_only` - Shows a lock icon if the current directory is read-only. +- `root` - Shows a `#` if logged in as root, `$` otherwise. +- `ruby_version` - `ruby --version` +- `set_term_title` - If able, sets the title of your terminal to include some + useful info. +- `ssh` - If logged into over SSH, shows a network icon. +- `svn` - Details about the current SVN repo. +- `time` - Shows the current time +- `uptime` - Uptime of the current machine +- `username` - Name of the logged-in user +- `virtual_env` - Shows the name of the current virtual env or conda env. + +### 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. Create new themes by copying any other +existing theme and changing the values. To use a theme, set the `theme` +variable in `~/.powerline-shell.json` to the name of your theme. + +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 `~/.powerline-shell.py` like: + +``` +{ + "segments": [...], + "cwd": { + 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 + +The `hostname` segment provides one option: + +- `colorize`: If true, the hostname will be colorized based on a hash of + itself. ### Contributing new types of segments -The `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 and add its name to -the `config.py` file at the appropriate location. +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 @@ -153,102 +245,10 @@ 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 with the `nosetests` command -after install the requirements in `dev_requirements.txt`. +after install the requirements in `requirements-dev.txt`. -### Themes +## Troubleshooting -The `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. Create new themes by copying any other existing theme and -changing the values. To use a theme, set the `THEME` variable in `config.py` to -the name of your theme. - -A script for testing color combinations is provided at `themes/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. - -# Changes - -2016-01-14 - -* AWS profle support added per the `AWS_DEFAULT_PROFILE` setup described in [the AWS docs](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) -* `$ export AWS_DEFAULT_PROFILE=` - - -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)) +See the [FAQ](https://github.com/banga/powerline-shell/wiki/FAQ). If you +continue to have issues, please open an +[issue](https://github.com/banga/powerline-shell/issues/new). diff --git a/bash-powerline-screenshot.png b/bash-powerline-screenshot.png new file mode 100644 index 0000000..66a3b24 Binary files /dev/null and b/bash-powerline-screenshot.png differ diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 7638a89..0000000 --- a/circle.yml +++ /dev/null @@ -1,5 +0,0 @@ -dependencies: - pre: - - sudo pip install -r dev_requirements.txt - - git config --global user.email "tester@example.com" - - git config --global user.name "Tester McGee" diff --git a/themes/colortest.py b/colortest.py similarity index 100% rename from themes/colortest.py rename to colortest.py diff --git a/config.py.dist b/config.py.dist deleted file mode 100644 index a1e7627..0000000 --- a/config.py.dist +++ /dev/null @@ -1,57 +0,0 @@ -# This is the configuration file for your powerline-shell prompt -# Every time you make a change to this file, run install.py to apply changes -# -# For instructions on how to use the powerline-shell.py script, see the README - -# Add, remove or rearrange these segments to customize what you see on the shell -# prompt. Any segment you add must be present in the segments/ directory - -SEGMENTS = [ -# Set the terminal window title to user@host:dir -# 'set_term_title', - -# Show current virtual environment (see http://www.virtualenv.org/) - 'virtual_env', - -# Show the current user's username as in ordinary prompts - 'username', - -# Show the machine's hostname. Mostly used when ssh-ing into other machines - 'hostname', - -# Show a padlock when ssh-ing from another machine - 'ssh', - -# Show the current directory. If the path is too long, the middle part is -# replaced with ellipsis ('...') - 'cwd', - -# Show a padlock if the current user has no write access to the current -# directory - 'read_only', - -# Show the current git branch and status - 'git', - -# Show the current mercurial branch and status - 'hg', - -# Show the current svn branch and status - 'svn', - -# Show the current fossil branch and status - 'fossil', - -# Show number of running jobs - 'jobs', - -# Show the last command's exit code if it was non-zero -# 'exit_code', - -# Shows a '#' if the current user is root, '$' otherwise -# Also, changes color if the last command exited with a non-zero error code - 'root', -] - -# Change the colors used to draw individual segments in your prompt -THEME = 'default' diff --git a/install.py b/install.py deleted file mode 100755 index bf36f41..0000000 --- a/install.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function -import os -import stat - -try: - import config -except ImportError: - print('Created personal config.py for your customizations') - import shutil - shutil.copyfile('config.py.dist', 'config.py') - import config - -TEMPLATE_FILE = 'powerline_shell_base.py' -OUTPUT_FILE = 'powerline-shell.py' -SEGMENTS_DIR = 'segments' -THEMES_DIR = 'themes' - -def load_source(srcfile): - try: - return ''.join(open(srcfile).readlines()) + '\n\n' - except IOError: - print('Could not open', srcfile) - return '' - -if __name__ == "__main__": - source = load_source(TEMPLATE_FILE) - source += load_source(os.path.join(THEMES_DIR, 'default.py')) - - if config.THEME != 'default': - source += load_source(os.path.join(THEMES_DIR, config.THEME + '.py')) - - for segment in config.SEGMENTS: - source += load_source(os.path.join(SEGMENTS_DIR, segment + '.py')) - - # assumes each segment file will have a function called - # add_segment__[segment] that accepts the powerline object - source += 'add_{}_segment(powerline)\n'.format(segment) - - source += 'sys.stdout.write(powerline.draw())\n' - - try: - open(OUTPUT_FILE, 'w').write(source) - st = os.stat(OUTPUT_FILE) - os.chmod(OUTPUT_FILE, st.st_mode | stat.S_IEXEC) - print(OUTPUT_FILE, 'saved successfully') - except IOError: - print('ERROR: Could not write to powerline-shell.py. Make sure it is writable') - exit(1) diff --git a/powerline_shell_base.py b/powerline_shell/__init__.py old mode 100755 new mode 100644 similarity index 59% rename from powerline_shell_base.py rename to powerline_shell/__init__.py index 8b95fb3..ec25c0d --- a/powerline_shell_base.py +++ b/powerline_shell/__init__.py @@ -1,94 +1,14 @@ #!/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 re -py3 = sys.version_info.major == 3 - - -def warn(msg): - print('[powerline-bash] ', msg) - - -class Powerline: - symbols = { - 'compatible': { - 'lock': 'RO', - 'network': 'SSH', - 'separator': u'\u25B6', - 'separator_thin': u'\u276F' - }, - 'patched': { - 'lock': u'\uE0A2', - 'network': u'\uE0A2', - 'separator': u'\uE0B0', - 'separator_thin': u'\uE0B1' - }, - 'flat': { - 'lock': '', - 'network': '', - 'separator': '', - 'separator_thin': '' - }, - } - - color_templates = { - 'bash': '\\[\\e%s\\]', - 'zsh': '%%{%s%%}', - 'bare': '%s', - } - - def __init__(self, args, cwd): - self.args = args - self.cwd = cwd - mode, shell = args.mode, args.shell - self.color_template = self.color_templates[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 color(self, prefix, code): - if code is None: - return '' - 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): - 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 get_valid_cwd(): """ We check if the current working directory is valid or not. Typically @@ -120,26 +40,153 @@ def get_valid_cwd(): return cwd -if __name__ == "__main__": +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': '\\[\\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): + 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] + if self.args.shell == "bash": + sanitized = re.sub(r"([`$])", r"\\\1", segment[0]) + else: + sanitized = segment[0] + next_segment = self.segments[idx + 1] if idx < len(self.segments)-1 else None + + return ''.join(( + self.fgcolor(segment[1]), + self.bgcolor(segment[2]), + sanitized, + 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", + ]: + 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', + ] +} + + +def main(): arg_parser = argparse.ArgumentParser() - arg_parser.add_argument('--cwd-mode', action='store', - help='How to display the current directory', default='fancy', - choices=['fancy', 'plain', 'dironly']) - arg_parser.add_argument('--cwd-only', action='store_true', - help='Deprecated. Use --cwd-mode=dironly') - arg_parser.add_argument('--cwd-max-depth', action='store', type=int, - default=5, help='Maximum number of directories to show in path') - arg_parser.add_argument('--cwd-max-dir-size', action='store', type=int, - help='Maximum number of letters displayed for each directory in the path') - arg_parser.add_argument('--colorize-hostname', action='store_true', - help='Colorize the hostname based on a hash of itself.') - arg_parser.add_argument('--mode', action='store', default='patched', - help='The characters used to make separators between segments', - choices=['patched', 'compatible', 'flat']) + 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', 'zsh', 'bare']) + help='Set this to your shell type', + choices=['bash', 'zsh', 'bare']) arg_parser.add_argument('prev_error', nargs='?', type=int, default=0, - help='Error code returned by the last command') + help='Error code returned by the last command') args = arg_parser.parse_args() - powerline = Powerline(args, get_valid_cwd()) + 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: + config = json.loads(f.read()) + else: + config = DEFAULT_CONFIG + + theme_name = config.get("theme", "default") + mod = importlib.import_module("powerline_shell.themes." + theme_name) + theme = getattr(mod, "Color") + + powerline = Powerline(args, config, theme) + segments = [] + for seg_name in config["segments"]: + mod = importlib.import_module("powerline_shell.segments." + seg_name) + segment = getattr(mod, "Segment")(powerline) + segment.start() + segments.append(segment) + for segment in segments: + segment.add_to_powerline() + sys.stdout.write(powerline.draw()) + return 0 diff --git a/lib/color_compliment.py b/powerline_shell/color_compliment.py old mode 100755 new mode 100644 similarity index 73% rename from lib/color_compliment.py rename to powerline_shell/color_compliment.py index 9e022dd..6a64dc4 --- a/lib/color_compliment.py +++ b/powerline_shell/color_compliment.py @@ -1,4 +1,3 @@ -#! /usr/bin/env python from colorsys import hls_to_rgb, rgb_to_hls # md5 deprecated since Python 2.5 try: @@ -6,19 +5,13 @@ try: except ImportError: from hashlib import md5 import sys - -# Original, non-relative import errors on Python3 from .colortrans import * - -py3 = sys.version_info[0] == 3 +from .utils import py3 def getOppositeColor(r,g,b): hls = rgb_to_hls(r,g,b) - #print "hls is" - #print hls opp = list(hls[:]) - #opp[0] = (opp[0]+0.5)%1 # reverse hue (a.k.a. color), reversing tends to be jarring 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 @@ -26,7 +19,6 @@ def getOppositeColor(r,g,b): opp[1] += 255/2 if opp[2] > -0.5: # if saturation is low on first color increase second's opp[2] -= 0.5 - #print opp opp = hls_to_rgb(*opp) m = max(opp) if m > 255: #colorsys module doesn't give caps to their conversions @@ -34,8 +26,6 @@ def getOppositeColor(r,g,b): return tuple([ int(x) for x in opp]) def stringToHashToColorAndOpposite(string): - # Python3: Unicode string must be encoded before digest - # Python2.7: works either way, but check in case breaks earlier py2 if py3: string = string.encode('utf-8') string = md5(string).hexdigest()[:6] # get a random color diff --git a/lib/colortrans.py b/powerline_shell/colortrans.py similarity index 99% rename from lib/colortrans.py rename to powerline_shell/colortrans.py index 0e2922c..1c38552 100755 --- a/lib/colortrans.py +++ b/powerline_shell/colortrans.py @@ -281,10 +281,6 @@ RGB2SHORT_DICT = { (255, 255, 215): 230, (255, 255, 255): 231} - -def hexstr2num(hexstr): - return int(hexstr, 16) - 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 diff --git a/lib/__init__.py b/powerline_shell/segments.py similarity index 100% rename from lib/__init__.py rename to powerline_shell/segments.py diff --git a/segments/__init__.py b/powerline_shell/segments/__init__.py similarity index 100% rename from segments/__init__.py rename to powerline_shell/segments/__init__.py diff --git a/powerline_shell/segments/battery.py b/powerline_shell/segments/battery.py new file mode 100644 index 0000000..d9acd41 --- /dev/null +++ b/powerline_shell/segments/battery.py @@ -0,0 +1,31 @@ +from ..utils import BasicSegment, warn +import os + +LOW_BATTERY_THRESHOLD = 20 +# See discussion in https://github.com/banga/powerline-shell/pull/204 regarding +# the directory where battery info is saved +DIR_OPTIONS = ["/sys/class/power_supply/BAT0", + "/sys/class/power_supply/BAT1"] + + +class Segment(BasicSegment): + def add_to_powerline(self): + 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 = f.read().strip() + with open(os.path.join(dir_, "status")) as f: + status = f.read().strip() + pwr = u" \u26A1 " if status == "Charging" else u" " + if int(cap) < LOW_BATTERY_THRESHOLD: + 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(" " + cap + "%" + pwr, fg, bg) diff --git a/powerline_shell/segments/bzr.py b/powerline_shell/segments/bzr.py new file mode 100644 index 0000000..c90dfa8 --- /dev/null +++ b/powerline_shell/segments/bzr.py @@ -0,0 +1,72 @@ +import os +import subprocess +from ..utils import RepoStats, ThreadedSegment + + +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 bzr_subprocess_env(): + return {"PATH": get_PATH()} + + +def _get_bzr_branch(): + p = subprocess.Popen(['bzr', 'nick'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=bzr_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 build_stats(): + try: + p = subprocess.Popen(['bzr', 'status'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=bzr_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 = pdata[0].decode("utf-8").splitlines() + 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 + + self.powerline.append(" " + self.branch + " ", fg, bg) + self.stats.add_to_powerline(self.powerline) diff --git a/segments/cwd.py b/powerline_shell/segments/cwd.py similarity index 62% rename from segments/cwd.py rename to powerline_shell/segments/cwd.py index 3c80b70..f99eb13 100644 --- a/segments/cwd.py +++ b/powerline_shell/segments/cwd.py @@ -1,4 +1,6 @@ import os +import sys +from ..utils import warn, py3, BasicSegment ELLIPSIS = u'\u2026' @@ -22,27 +24,32 @@ def split_path_into_names(cwd): return names -def requires_special_home_display(name): +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 Color.HOME_SPECIAL_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.""" - if powerline.args.cwd_max_dir_size: - return name[:powerline.args.cwd_max_dir_size] + max_size = powerline.segment_conf("cwd", "max_dir_size") + if max_size: + return name[:max_size] return name -def get_fg_bg(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(name): - return (Color.HOME_FG, Color.HOME_BG,) - return (Color.PATH_FG, Color.PATH_BG,) + 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): @@ -51,15 +58,15 @@ def add_cwd_segment(powerline): cwd = cwd.decode("utf-8") cwd = replace_home_dir(cwd) - if powerline.args.cwd_mode == 'plain': - powerline.append(' %s ' % (cwd,), Color.CWD_FG, Color.PATH_BG) + if powerline.segment_conf("cwd", "mode") == 'plain': + powerline.append(' %s ' % (cwd,), powerline.theme.CWD_FG, powerline.theme.PATH_BG) return names = split_path_into_names(cwd) - max_depth = powerline.args.cwd_max_depth + max_depth = powerline.segment_conf("cwd", "max_depth", 5) if max_depth <= 0: - warn("Ignoring --cwd-max-depth argument since it's not greater than 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 @@ -71,20 +78,25 @@ def add_cwd_segment(powerline): n_before = 2 if max_depth > 2 else max_depth - 1 names = names[:n_before] + [ELLIPSIS] + names[n_before - max_depth:] - if (powerline.args.cwd_mode == 'dironly' or powerline.args.cwd_only): + 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:] for i, name in enumerate(names): - fg, bg = get_fg_bg(name) + is_last_dir = (i == len(names) - 1) + fg, bg = get_fg_bg(powerline, name, is_last_dir) separator = powerline.separator_thin - separator_fg = Color.SEPARATOR_FG - is_last_dir = (i == len(names) - 1) - if requires_special_home_display(name) or is_last_dir: + separator_fg = powerline.theme.SEPARATOR_FG + if requires_special_home_display(powerline, name) or is_last_dir: separator = None separator_fg = None powerline.append(' %s ' % maybe_shorten_name(powerline, 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/exit_code.py b/powerline_shell/segments/exit_code.py new file mode 100644 index 0000000..85153ec --- /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 0000000..3c464f9 --- /dev/null +++ b/powerline_shell/segments/fossil.py @@ -0,0 +1,79 @@ +import os +import subprocess +from ..utils import RepoStats, ThreadedSegment + + +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 fossil_subprocess_env(): + return {"PATH": get_PATH()} + + +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=fossil_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 + + self.powerline.append(" " + self.branch + " ", fg, bg) + self.stats.add_to_powerline(self.powerline) diff --git a/segments/git.py b/powerline_shell/segments/git.py similarity index 57% rename from segments/git.py rename to powerline_shell/segments/git.py index d33a9d3..72ef7a1 100644 --- a/segments/git.py +++ b/powerline_shell/segments/git.py @@ -1,16 +1,8 @@ import re import subprocess import os +from ..utils import RepoStats, ThreadedSegment -GIT_SYMBOLS = { - 'detached': u'\u2693', - 'ahead': u'\u2B06', - 'behind': u'\u2B07', - 'staged': u'\u2714', - 'notstaged': u'\u270E', - 'untracked': u'\u2753', - 'conflicted': u'\u273C' -} def get_PATH(): """Normally gets the PATH from the OS. This function exists to enable @@ -18,6 +10,7 @@ def get_PATH(): """ return os.getenv("PATH") + def git_subprocess_env(): return { # LANG is specified to ensure git always uses a language we are expecting. @@ -43,74 +36,68 @@ def _get_git_detached_branch(): env=git_subprocess_env()) detached_ref = p.communicate()[0].decode("utf-8").rstrip('\n') if p.returncode == 0: - branch = u'{} {}'.format(GIT_SYMBOLS['detached'], detached_ref) + branch = u'{} {}'.format(RepoStats.symbols['detached'], detached_ref) else: branch = 'Big Bang' return branch def parse_git_stats(status): - stats = {'untracked': 0, 'notstaged': 0, 'staged': 0, 'conflicted': 0} + stats = RepoStats() for statusline in status[1:]: code = statusline[:2] if code == '??': - stats['untracked'] += 1 + stats.new += 1 elif code in ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'): - stats['conflicted'] += 1 + stats.conflicted += 1 else: if code[1] != ' ': - stats['notstaged'] += 1 + stats.changed += 1 if code[0] != ' ': - stats['staged'] += 1 + stats.staged += 1 return stats -def _n_or_empty(_dict, _key): - return _dict[_key] if int(_dict[_key]) > 1 else u'' - - -def add_git_segment(powerline): +def build_stats(): try: p = subprocess.Popen(['git', 'status', '--porcelain', '-b'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=git_subprocess_env()) except OSError: # Popen will throw an OSError if git is not found - return + return (None, None) pdata = p.communicate() if p.returncode != 0: - return + return (None, None) status = pdata[0].decode("utf-8").splitlines() - - branch_info = parse_git_branch_info(status) stats = parse_git_stats(status) - dirty = (True if sum(stats.values()) > 0 else False) + 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 - bg = Color.REPO_CLEAN_BG - fg = Color.REPO_CLEAN_FG - if dirty: - bg = Color.REPO_DIRTY_BG - fg = Color.REPO_DIRTY_FG - powerline.append(' %s ' % branch, fg, bg) +class Segment(ThreadedSegment): + def run(self): + self.stats, self.branch = build_stats() - def _add(_dict, _key, fg, bg): - if _dict[_key]: - _str = u' {}{} '.format(_n_or_empty(_dict, _key), GIT_SYMBOLS[_key]) - powerline.append(_str, fg, bg) + 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 branch_info: - _add(branch_info, 'ahead', Color.GIT_AHEAD_FG, Color.GIT_AHEAD_BG) - _add(branch_info, 'behind', Color.GIT_BEHIND_FG, Color.GIT_BEHIND_BG) - _add(stats, 'staged', Color.GIT_STAGED_FG, Color.GIT_STAGED_BG) - _add(stats, 'notstaged', Color.GIT_NOTSTAGED_FG, Color.GIT_NOTSTAGED_BG) - _add(stats, 'untracked', Color.GIT_UNTRACKED_FG, Color.GIT_UNTRACKED_BG) - _add(stats, 'conflicted', Color.GIT_CONFLICTED_FG, Color.GIT_CONFLICTED_BG) + self.powerline.append(" " + self.branch + " ", fg, bg) + self.stats.add_to_powerline(self.powerline) diff --git a/powerline_shell/segments/hg.py b/powerline_shell/segments/hg.py new file mode 100644 index 0000000..05cb2f7 --- /dev/null +++ b/powerline_shell/segments/hg.py @@ -0,0 +1,79 @@ +import os +import subprocess +from ..utils import ThreadedSegment +import subprocess + + +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 _subprocess_env(): + return {"PATH": get_PATH()} + + +def get_hg_status(): + has_modified_files = False + has_untracked_files = False + has_missing_files = False + + p = subprocess.Popen(["hg", "status"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=_subprocess_env()) + output = p.communicate()[0].decode("utf-8") + + 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 build_stats(): + try: + p = subprocess.Popen(["hg", "branch"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=_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 + + branch = pdata[0].decode("utf-8").strip() + return branch, get_hg_status() + + +class Segment(ThreadedSegment): + def run(self): + self.branch, self.status = build_stats() + + def add_to_powerline(self): + self.join() + if not self.branch or not self.status: + return + bg = self.powerline.theme.REPO_CLEAN_BG + fg = self.powerline.theme.REPO_CLEAN_FG + has_modified, has_untracked, has_missing = self.status + if has_modified or has_untracked or has_missing: + bg = self.powerline.theme.REPO_DIRTY_BG + fg = self.powerline.theme.REPO_DIRTY_FG + extra = "" + if has_untracked: + extra += "+" + if has_missing: + extra += "!" + self.branch += " " + extra + return self.powerline.append(" %s " % self.branch, fg, bg) diff --git a/powerline_shell/segments/hostname.py b/powerline_shell/segments/hostname.py new file mode 100644 index 0000000..894e139 --- /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 0000000..e5d0551 --- /dev/null +++ b/powerline_shell/segments/jobs.py @@ -0,0 +1,31 @@ +import os +import re +import subprocess +import platform +from ..utils import ThreadedSegment + + +class Segment(ThreadedSegment): + def run(self): + self.num_jobs = 0 + if platform.system().startswith('CYGWIN'): + # cygwin ps is a special snowflake... + output_proc = subprocess.Popen(['ps', '-af'], stdout=subprocess.PIPE) + output = map(lambda l: int(l.split()[2].strip()), + output_proc.communicate()[0].decode("utf-8").splitlines()[1:]) + self.num_jobs = output.count(os.getppid()) - 1 + else: + pppid_proc = subprocess.Popen(['ps', '-p', str(os.getppid()), '-oppid='], + stdout=subprocess.PIPE) + pppid = pppid_proc.communicate()[0].decode("utf-8").strip() + 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(pppid), 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 0000000..f25cb46 --- /dev/null +++ b/powerline_shell/segments/newline.py @@ -0,0 +1,9 @@ +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + 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 0000000..aa19592 --- /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 0000000..4ef23bc --- /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 0000000..52f1899 --- /dev/null +++ b/powerline_shell/segments/php_version.py @@ -0,0 +1,19 @@ +import subprocess +from ..utils import ThreadedSegment + + +class Segment(ThreadedSegment): + def run(self): + try: + output = 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 0000000..4b8c5d3 --- /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 0000000..149b113 --- /dev/null +++ b/powerline_shell/segments/read_only.py @@ -0,0 +1,12 @@ +import os +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + cwd = powerline.cwd or os.getenv('PWD') + if not os.access(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 0000000..5b31c60 --- /dev/null +++ b/powerline_shell/segments/root.py @@ -0,0 +1,17 @@ +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + root_indicators = { + 'bash': ' \\$ ', + '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) diff --git a/powerline_shell/segments/ruby_version.py b/powerline_shell/segments/ruby_version.py new file mode 100644 index 0000000..0e7bee1 --- /dev/null +++ b/powerline_shell/segments/ruby_version.py @@ -0,0 +1,19 @@ +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) + version = p2.communicate()[0].decode("utf-8").rstrip() + if os.environ.has_key("GEM_HOME"): + gem = os.environ["GEM_HOME"].split("@") + if len(gem) > 1: + version += " " + gem[1] + powerline.append(version, 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 0000000..9743959 --- /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 or os.getenv('PWD'), + ) + 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 0000000..f64c967 --- /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/svn.py b/powerline_shell/segments/svn.py new file mode 100644 index 0000000..995d04d --- /dev/null +++ b/powerline_shell/segments/svn.py @@ -0,0 +1,29 @@ +import subprocess +from ..utils import BasicSegment, RepoStats + + +class Segment(BasicSegment): + def add_to_powerline(self): + is_svn = subprocess.Popen(["svn", "status"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + is_svn_output = is_svn.communicate()[1].decode("utf-8").strip() + if len(is_svn_output) != 0: + return + + try: + p1 = subprocess.Popen(["svn", "status"], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + except OSError: + return + + stdout = p1.communicate()[0] + stats = RepoStats() + for line in stdout.splitlines(): + 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 + + 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 0000000..ab4c911 --- /dev/null +++ b/powerline_shell/segments/time.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import +from ..utils import BasicSegment +import time + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + if 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.HOSTNAME_FG, + powerline.theme.HOSTNAME_BG) diff --git a/powerline_shell/segments/uptime.py b/powerline_shell/segments/uptime.py new file mode 100644 index 0000000..96e4c26 --- /dev/null +++ b/powerline_shell/segments/uptime.py @@ -0,0 +1,20 @@ +import subprocess +import re +from ..utils import BasicSegment + + +class Segment(BasicSegment): + def add_to_powerline(self): + powerline = self.powerline + try: + output = subprocess.check_output(['uptime'], stderr=subprocess.STDOUT).decode("utf-8") + 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 0000000..9e4d9d8 --- /dev/null +++ b/powerline_shell/segments/username.py @@ -0,0 +1,21 @@ +from ..utils import BasicSegment +import os +import pwd + + +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 pwd.getpwuid(os.getuid())[0] == "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 0000000..ffbefec --- /dev/null +++ b/powerline_shell/segments/virtual_env.py @@ -0,0 +1,15 @@ +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 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 0000000..e69de29 diff --git a/themes/basic.py b/powerline_shell/themes/basic.py similarity index 88% rename from themes/basic.py rename to powerline_shell/themes/basic.py index 3c51c35..a6b745d 100644 --- a/themes/basic.py +++ b/powerline_shell/themes/basic.py @@ -1,6 +1,8 @@ -# Basic theme which only uses colors in 0-15 range +from .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 diff --git a/themes/default.py b/powerline_shell/themes/default.py similarity index 83% rename from themes/default.py rename to powerline_shell/themes/default.py index a365a64..a2178c1 100644 --- a/themes/default.py +++ b/powerline_shell/themes/default.py @@ -1,8 +1,13 @@ -class DefaultColor: +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 @@ -56,10 +61,14 @@ class DefaultColor: 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 - class Color(DefaultColor): """ This subclass is required when the user chooses to use 'default' theme. diff --git a/themes/solarized-dark.py b/powerline_shell/themes/solarized_dark.py similarity index 94% rename from themes/solarized-dark.py rename to powerline_shell/themes/solarized_dark.py index da556bf..a756254 100644 --- a/themes/solarized-dark.py +++ b/powerline_shell/themes/solarized_dark.py @@ -1,3 +1,6 @@ +from .default import DefaultColor + + class Color(DefaultColor): USERNAME_FG = 15 USERNAME_BG = 4 diff --git a/powerline_shell/themes/solarized_light.py b/powerline_shell/themes/solarized_light.py new file mode 100644 index 0000000..da14cdf --- /dev/null +++ b/powerline_shell/themes/solarized_light.py @@ -0,0 +1,38 @@ +from .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 diff --git a/themes/washed.py b/powerline_shell/themes/washed.py similarity index 95% rename from themes/washed.py rename to powerline_shell/themes/washed.py index 790c2af..37a149f 100644 --- a/themes/washed.py +++ b/powerline_shell/themes/washed.py @@ -1,3 +1,6 @@ +from .default import DefaultColor + + class Color(DefaultColor): USERNAME_FG = 8 USERNAME_BG = 251 diff --git a/powerline_shell/utils.py b/powerline_shell/utils.py new file mode 100644 index 0000000..c99b08c --- /dev/null +++ b/powerline_shell/utils.py @@ -0,0 +1,95 @@ +import sys +import threading + +py3 = sys.version_info[0] == 3 + +if py3: + def unicode(x): + return str(x) + + +class RepoStats(object): + symbols = { + 'detached': u'\u2693', + 'ahead': u'\u2B06', + 'behind': u'\u2B07', + 'staged': u'\u2714', + 'changed': u'\u270E', + 'new': u'\u2753', + 'conflicted': u'\u273C' + } + + 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): + self.powerline = powerline + + def start(self): + pass + + +class ThreadedSegment(threading.Thread): + def __init__(self, powerline): + super(ThreadedSegment, self).__init__() + self.powerline = powerline diff --git a/dev_requirements.txt b/requirements-dev.txt similarity index 100% rename from dev_requirements.txt rename to requirements-dev.txt diff --git a/segments/exit_code.py b/segments/exit_code.py deleted file mode 100644 index e3b320e..0000000 --- a/segments/exit_code.py +++ /dev/null @@ -1,6 +0,0 @@ -def add_exit_code_segment(powerline): - if powerline.args.prev_error == 0: - return - fg = Color.CMD_FAILED_FG - bg = Color.CMD_FAILED_BG - powerline.append(' %s ' % str(powerline.args.prev_error), fg, bg) diff --git a/segments/fossil.py b/segments/fossil.py deleted file mode 100644 index 6c7818e..0000000 --- a/segments/fossil.py +++ /dev/null @@ -1,50 +0,0 @@ -import os -import subprocess - -def get_fossil_status(): - has_modified_files = False - has_untracked_files = False - has_missing_files = False - output = os.popen('fossil changes 2>/dev/null').read().strip() - has_untracked_files = True if os.popen("fossil extras 2>/dev/null").read().strip() else False - has_missing_files = 'MISSING' in output - has_modified_files = 'EDITED' in output - - return has_modified_files, has_untracked_files, has_missing_files - -def _add_fossil_segment(powerline): - subprocess.Popen(['fossil'], stdout=subprocess.PIPE).communicate()[0] - branch = ''.join([i.replace('*','').strip() for i in os.popen("fossil branch 2> /dev/null").read().strip().split("\n") if i.startswith('*')]) - if len(branch) == 0: - return - - bg = Color.REPO_CLEAN_BG - fg = Color.REPO_CLEAN_FG - has_modified_files, has_untracked_files, has_missing_files = get_fossil_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(' %s ' % branch, fg, bg) - -def add_fossil_segment(powerline): - """Wraps _add_fossil_segment in exception handling.""" - - # FIXME This function was added when introducing a testing framework, - # during which the 'powerline' object was passed into the - # `add_[segment]_segment` functions instead of being a global variable. At - # that time it was unclear whether the below exceptions could actually be - # thrown. It would be preferable to find out whether they ever will. If so, - # write a comment explaining when. Otherwise remove. - - try: - _add_fossil_segment(powerline) - except OSError: - pass - except subprocess.CalledProcessError: - pass diff --git a/segments/hg.py b/segments/hg.py deleted file mode 100644 index d5f1cf4..0000000 --- a/segments/hg.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -import subprocess - -def get_hg_status(): - has_modified_files = False - has_untracked_files = False - has_missing_files = False - - p = subprocess.Popen(['hg', 'status'], stdout=subprocess.PIPE) - output = p.communicate()[0].decode("utf-8") - - 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): - 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 '') - return powerline.append(' %s ' % branch, fg, bg) diff --git a/segments/hostname.py b/segments/hostname.py deleted file mode 100644 index 609711e..0000000 --- a/segments/hostname.py +++ /dev/null @@ -1,21 +0,0 @@ -def add_hostname_segment(powerline): - if powerline.args.colorize_hostname: - from lib.color_compliment import stringToHashToColorAndOpposite - from lib.colortrans import rgb2short - from socket import gethostname - 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 = ' \\h ' - elif powerline.args.shell == 'zsh': - host_prompt = ' %m ' - else: - import socket - host_prompt = ' %s ' % socket.gethostname().split('.')[0] - - powerline.append(host_prompt, Color.HOSTNAME_FG, Color.HOSTNAME_BG) diff --git a/segments/jobs.py b/segments/jobs.py deleted file mode 100644 index a6ff180..0000000 --- a/segments/jobs.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -import re -import subprocess - -def add_jobs_segment(powerline): - pppid_proc = subprocess.Popen(['ps', '-p', str(os.getppid()), '-oppid='], - stdout=subprocess.PIPE) - pppid = pppid_proc.communicate()[0].decode("utf-8").strip() - - output_proc = subprocess.Popen(['ps', '-a', '-o', 'ppid'], - stdout=subprocess.PIPE) - output = output_proc.communicate()[0].decode("utf-8") - - num_jobs = len(re.findall(str(pppid), output)) - 1 - - if num_jobs > 0: - powerline.append(' %d ' % num_jobs, Color.JOBS_FG, Color.JOBS_BG) diff --git a/segments/node_version.py b/segments/node_version.py deleted file mode 100644 index 466a086..0000000 --- a/segments/node_version.py +++ /dev/null @@ -1,11 +0,0 @@ -import subprocess - - -def add_node_version_segment(powerline): - try: - p1 = subprocess.Popen(["node", "--version"], stdout=subprocess.PIPE) - version = p1.communicate()[0].decode("utf-8").rstrip() - version = "node " + version - powerline.append(version, 15, 18) - except OSError: - return diff --git a/segments/php_version.py b/segments/php_version.py deleted file mode 100644 index 7b9b8aa..0000000 --- a/segments/php_version.py +++ /dev/null @@ -1,14 +0,0 @@ -import subprocess - - -def add_php_version_segment(powerline): - try: - output = subprocess.check_output(['php', '-r', 'echo PHP_VERSION;'], stderr=subprocess.STDOUT) - if '-' in output: - version = ' %s ' % output.split('-')[0] - else: - version = ' %s ' % output - - powerline.append(version, 15, 4) - except OSError: - return diff --git a/segments/read_only.py b/segments/read_only.py deleted file mode 100644 index efbe11a..0000000 --- a/segments/read_only.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -def add_read_only_segment(powerline): - cwd = powerline.cwd or os.getenv('PWD') - - if not os.access(cwd, os.W_OK): - powerline.append(' %s ' % powerline.lock, Color.READONLY_FG, Color.READONLY_BG) diff --git a/segments/root.py b/segments/root.py deleted file mode 100644 index 49404ec..0000000 --- a/segments/root.py +++ /dev/null @@ -1,12 +0,0 @@ -def add_root_segment(powerline): - root_indicators = { - 'bash': ' \\$ ', - 'zsh': ' %# ', - 'bare': ' $ ', - } - bg = Color.CMD_PASSED_BG - fg = Color.CMD_PASSED_FG - if powerline.args.prev_error != 0: - fg = Color.CMD_FAILED_FG - bg = Color.CMD_FAILED_BG - powerline.append(root_indicators[powerline.args.shell], fg, bg) diff --git a/segments/ruby_version.py b/segments/ruby_version.py deleted file mode 100644 index 040f533..0000000 --- a/segments/ruby_version.py +++ /dev/null @@ -1,15 +0,0 @@ -import subprocess - - -def add_ruby_version_segment(powerline): - try: - p1 = subprocess.Popen(["ruby", "-v"], stdout=subprocess.PIPE) - p2 = subprocess.Popen(["sed", "s/ (.*//"], stdin=p1.stdout, stdout=subprocess.PIPE) - version = p2.communicate()[0].decode("utf-8").rstrip() - if os.environ.has_key("GEM_HOME"): - gem = os.environ["GEM_HOME"].split("@") - if len(gem) > 1: - version += " " + gem[1] - powerline.append(version, 15, 1) - except OSError: - return diff --git a/segments/set_term_title.py b/segments/set_term_title.py deleted file mode 100644 index e9954ae..0000000 --- a/segments/set_term_title.py +++ /dev/null @@ -1,15 +0,0 @@ -def add_set_term_title_segment(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: - import socket - set_title = '\033]0;%s@%s: %s\007' % (os.getenv('USER'), socket.gethostname().split('.')[0], powerline.cwd or os.getenv('PWD')) - - powerline.append(set_title, None, None, '') - diff --git a/segments/ssh.py b/segments/ssh.py deleted file mode 100644 index 5361bc5..0000000 --- a/segments/ssh.py +++ /dev/null @@ -1,6 +0,0 @@ -import os - -def add_ssh_segment(powerline): - - if os.getenv('SSH_CLIENT'): - powerline.append(' %s ' % powerline.network, Color.SSH_FG, Color.SSH_BG) diff --git a/segments/svn.py b/segments/svn.py deleted file mode 100644 index 89168c0..0000000 --- a/segments/svn.py +++ /dev/null @@ -1,37 +0,0 @@ -import subprocess - - -def _add_svn_segment(powerline): - is_svn = subprocess.Popen(['svn', 'status'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - is_svn_output = is_svn.communicate()[1].decode("utf-8").strip() - if len(is_svn_output) != 0: - return - - #"svn status | grep -c "^[ACDIMRX\\!\\~]" - p1 = subprocess.Popen(['svn', 'status'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - p2 = subprocess.Popen(['grep', '-c', '^[ACDIMR\\!\\~]'], - stdin=p1.stdout, stdout=subprocess.PIPE) - output = p2.communicate()[0].decode("utf-8").strip() - if len(output) > 0 and int(output) > 0: - changes = output.strip() - powerline.append(' %s ' % changes, Color.SVN_CHANGES_FG, Color.SVN_CHANGES_BG) - - -def add_svn_segment(powerline): - """Wraps _add_svn_segment in exception handling.""" - - # FIXME This function was added when introducing a testing framework, - # during which the 'powerline' object was passed into the - # `add_[segment]_segment` functions instead of being a global variable. At - # that time it was unclear whether the below exceptions could actually be - # thrown. It would be preferable to find out whether they ever will. If so, - # write a comment explaining when. Otherwise remove. - - try: - _add_svn_segment(powerline) - except OSError: - pass - except subprocess.CalledProcessError: - pass diff --git a/segments/time.py b/segments/time.py deleted file mode 100644 index 8c0313f..0000000 --- a/segments/time.py +++ /dev/null @@ -1,10 +0,0 @@ -def add_time_segment(powerline): - if powerline.args.shell == 'bash': - time = ' \\t ' - elif powerline.args.shell == 'zsh': - time = ' %* ' - else: - import time - time = ' %s ' % time.strftime('%H:%M:%S') - - powerline.append(time, Color.HOSTNAME_FG, Color.HOSTNAME_BG) diff --git a/segments/uptime.py b/segments/uptime.py deleted file mode 100644 index 4c7711c..0000000 --- a/segments/uptime.py +++ /dev/null @@ -1,16 +0,0 @@ -import subprocess -import re - -def add_uptime_segment(powerline): - try: - output = 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, Color.CWD_FG, Color.PATH_BG) - except OSError: - return diff --git a/segments/username.py b/segments/username.py deleted file mode 100644 index 3d73a12..0000000 --- a/segments/username.py +++ /dev/null @@ -1,16 +0,0 @@ - -def add_username_segment(powerline): - import os - if powerline.args.shell == 'bash': - user_prompt = ' \\u ' - elif powerline.args.shell == 'zsh': - user_prompt = ' %n ' - else: - user_prompt = ' %s ' % os.getenv('USER') - - if os.getenv('USER') == 'root': - bgcolor = Color.USERNAME_ROOT_BG - else: - bgcolor = Color.USERNAME_BG - - powerline.append(user_prompt, Color.USERNAME_FG, bgcolor) diff --git a/segments/virtual_env.py b/segments/virtual_env.py deleted file mode 100644 index 710efc6..0000000 --- a/segments/virtual_env.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - -def add_virtual_env_segment(powerline): - env = os.getenv('VIRTUAL_ENV') or os.getenv('CONDA_ENV_PATH') - if env is None: - return - - env_name = os.path.basename(env) - bg = Color.VIRTUAL_ENV_BG - fg = Color.VIRTUAL_ENV_FG - powerline.append(' %s ' % env_name, fg, bg) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1eee7db --- /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 0000000..08fc42d --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +from setuptools import setup, find_packages + +setup(name="powerline-shell", + version="0.3.0", + description="A pretty prompt for your shell", + author="Buck Ryan", + url="https://github.com/banga/powerline-shell", + classifiers=[], + 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/cwd_test.py b/test/cwd_test.py index 111863e..ed52dc3 100644 --- a/test/cwd_test.py +++ b/test/cwd_test.py @@ -3,7 +3,7 @@ import mock import os import tempfile import shutil -import powerline_shell_base as p +import powerline_shell as p class CwdTest(unittest.TestCase): @@ -15,14 +15,14 @@ class CwdTest(unittest.TestCase): shutil.rmtree(self.dirname) @mock.patch('os.getenv') - @mock.patch('powerline_shell_base.warn') + @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_base.warn') + @mock.patch('powerline_shell.warn') def test_nonexistent_warns(self, warn, getenv): subdir = os.path.join(self.dirname, 'subdir') getenv.return_value = subdir @@ -30,7 +30,7 @@ class CwdTest(unittest.TestCase): self.assertEqual(warn.call_count, 1) @mock.patch('os.getenv') - @mock.patch('powerline_shell_base.warn') + @mock.patch('powerline_shell.warn') def test_falls_back_to_getcwd(self, warn, getenv): getenv.return_value = None os.chdir(self.dirname) @@ -38,7 +38,7 @@ class CwdTest(unittest.TestCase): self.assertEqual(warn.call_count, 0) @mock.patch('os.getenv') - @mock.patch('powerline_shell_base.warn') + @mock.patch('powerline_shell.warn') def test_nonexistent_getcwd_warns(self, warn, getenv): subdir = os.path.join(self.dirname, 'subdir') getenv.return_value = None diff --git a/test/repo_stats_test.py b/test/repo_stats_test.py new file mode 100644 index 0000000..123259b --- /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/fossil_test.py b/test/segments_test/fossil_test.py new file mode 100644 index 0000000..9f9d01d --- /dev/null +++ b/test/segments_test/fossil_test.py @@ -0,0 +1,63 @@ +import unittest +import mock +import tempfile +import shutil +import sh +import powerline_shell.segments.fossil as fossil +from powerline_shell.utils import RepoStats + +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.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") + + @mock.patch("powerline_shell.segments.fossil.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 ") + + @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_test.py b/test/segments_test/git_test.py index 982289e..ab8c9dd 100644 --- a/test/segments_test/git_test.py +++ b/test/segments_test/git_test.py @@ -3,19 +3,20 @@ import mock import tempfile import shutil import sh -import segments.git as git +import powerline_shell.segments.git as git class GitTest(unittest.TestCase): def setUp(self): self.powerline = mock.MagicMock() - git.Color = mock.MagicMock() self.dirname = tempfile.mkdtemp() sh.cd(self.dirname) sh.git("init", ".") + self.segment = git.Segment(self.powerline) + def tearDown(self): shutil.rmtree(self.dirname) @@ -24,36 +25,41 @@ class GitTest(unittest.TestCase): sh.git("add", filename) sh.git("commit", "-m", "add file " + filename) - def _new_branch(self, branch): + def _checkout_new_branch(self, branch): sh.git("checkout", "-b", branch) def _get_commit_hash(self): return sh.git("rev-parse", "HEAD") - @mock.patch('segments.git.get_PATH') + @mock.patch('powerline_shell.segments.git.get_PATH') def test_git_not_installed(self, get_PATH): get_PATH.return_value = "" # so git can't be found - git.add_git_segment(self.powerline) + 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") - git.add_git_segment(self.powerline) + self.segment.start() + self.segment.add_to_powerline() self.assertEqual(self.powerline.append.call_count, 0) def test_big_bang(self): - git.add_git_segment(self.powerline) + 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") - git.add_git_segment(self.powerline) + 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._new_branch("bar") - git.add_git_segment(self.powerline) + 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): @@ -61,7 +67,8 @@ class GitTest(unittest.TestCase): commit_hash = self._get_commit_hash() self._add_and_commit("bar") sh.git("checkout", "HEAD^") - git.add_git_segment(self.powerline) + self.segment.start() + self.segment.add_to_powerline() # In detached mode, we output a unicode symbol and then the shortened # commit hash. diff --git a/test/segments_test/hg_test.py b/test/segments_test/hg_test.py new file mode 100644 index 0000000..be8d147 --- /dev/null +++ b/test/segments_test/hg_test.py @@ -0,0 +1,48 @@ +import unittest +import mock +import tempfile +import shutil +import sh +import powerline_shell.segments.hg as hg + + +class HgTest(unittest.TestCase): + + def setUp(self): + self.powerline = mock.MagicMock() + + 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.segments.hg.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 ") diff --git a/test/segments_test/hostname_test.py b/test/segments_test/hostname_test.py new file mode 100644 index 0000000..68e941c --- /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/uptime_test.py b/test/segments_test/uptime_test.py index d2dacd2..3285a00 100644 --- a/test/segments_test/uptime_test.py +++ b/test/segments_test/uptime_test.py @@ -1,6 +1,6 @@ import unittest import mock -import segments.uptime as uptime +import powerline_shell.segments.uptime as uptime test_cases = { # linux test cases @@ -19,11 +19,12 @@ class UptimeTest(unittest.TestCase): def setUp(self): self.powerline = mock.MagicMock() - uptime.Color = 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 - uptime.add_uptime_segment(self.powerline) + self.segment.start() + self.segment.add_to_powerline() self.assertEqual(self.powerline.append.call_args[0][0].split()[0], result)