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