diff --git a/.bin/powerline-shell.py b/.bin/powerline-shell.py deleted file mode 120000 index b6c42339..00000000 --- a/.bin/powerline-shell.py +++ /dev/null @@ -1 +0,0 @@ -powerline-shell/powerline-shell.py \ No newline at end of file diff --git a/.bin/powerline-shell.py b/.bin/powerline-shell.py new file mode 100755 index 00000000..3d1d011c --- /dev/null +++ b/.bin/powerline-shell.py @@ -0,0 +1,650 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import print_function + +import argparse +import os +import sys + +py3 = sys.version_info.major == 3 + + +def warn(msg): + print('[powerline-bash] ', msg) + + +if py3: + def unicode(x): + return x + + +class Powerline: + symbols = { + 'compatible': { + 'lock': 'RO', + 'network': 'SSH', + 'separator': u'\u25B6', + 'separator_thin': u'\u276F' + }, + 'patched': { + 'lock': u'\uE0A2', + 'network': u'\uE0A2', + 'separator': u'\uE0B0', + 'separator_thin': u'\uE0B1' + }, + 'flat': { + 'lock': '', + 'network': '', + 'separator': '', + 'separator_thin': '' + }, + } + + color_templates = { + 'bash': '\\[\\e%s\\]', + 'zsh': '%%{%s%%}', + 'bare': '%s', + } + + def __init__(self, args, cwd): + self.args = args + self.cwd = cwd + mode, shell = args.mode, args.shell + self.color_template = self.color_templates[shell] + self.reset = self.color_template % '[0m' + self.lock = Powerline.symbols[mode]['lock'] + self.network = Powerline.symbols[mode]['network'] + self.separator = Powerline.symbols[mode]['separator'] + self.separator_thin = Powerline.symbols[mode]['separator_thin'] + self.segments = [] + + def color(self, prefix, code): + if code is None: + return '' + elif code == Color.RESET: + return self.reset + else: + return self.color_template % ('[%s;5;%sm' % (prefix, code)) + + def fgcolor(self, code): + return self.color('38', code) + + def bgcolor(self, code): + return self.color('48', code) + + def append(self, content, fg, bg, separator=None, separator_fg=None): + self.segments.append((content, fg, bg, + separator if separator is not None else self.separator, + separator_fg if separator_fg is not None else bg)) + + def draw(self): + text = (''.join(self.draw_segment(i) for i in range(len(self.segments))) + + self.reset) + ' ' + if py3: + return text + else: + return text.encode('utf-8') + + def draw_segment(self, idx): + segment = self.segments[idx] + next_segment = self.segments[idx + 1] if idx < len(self.segments)-1 else None + + return ''.join(( + self.fgcolor(segment[1]), + self.bgcolor(segment[2]), + segment[0], + self.bgcolor(next_segment[2]) if next_segment else self.reset, + self.fgcolor(segment[4]), + segment[3])) + + +class RepoStats: + symbols = { + 'detached': u'\u2693', + 'ahead': u'\u2B06', + 'behind': u'\u2B07', + 'staged': u'\u2714', + 'not_staged': u'\u270E', + 'untracked': u'\u2753', + 'conflicted': u'\u273C' + } + + def __init__(self): + self.ahead = 0 + self.behind = 0 + self.untracked = 0 + self.not_staged = 0 + self.staged = 0 + self.conflicted = 0 + + @property + def dirty(self): + qualifiers = [ + self.untracked, + self.not_staged, + self.staged, + self.conflicted, + ] + return sum(qualifiers) > 0 + + def __getitem__(self, _key): + return getattr(self, _key) + + def n_or_empty(self, _key): + """Given a string name of one of the properties of this class, returns + the value of the property as a string when the value is greater than + 1. When it is not greater than one, returns an empty string. + + As an example, if you want to show an icon for untracked files, but you + only want a number to appear next to the icon when there are more than + one untracked files, you can do: + + segment = repo_stats.n_or_empty("untracked") + icon_string + """ + return unicode(self[_key]) if int(self[_key]) > 1 else u'' + + def add_to_powerline(self, powerline, color): + def add(_key, fg, bg): + if self[_key]: + s = u" {}{} ".format(self.n_or_empty(_key), self.symbols[_key]) + powerline.append(s, fg, bg) + add('ahead', color.GIT_AHEAD_FG, color.GIT_AHEAD_BG) + add('behind', color.GIT_BEHIND_FG, color.GIT_BEHIND_BG) + add('staged', color.GIT_STAGED_FG, color.GIT_STAGED_BG) + add('not_staged', color.GIT_NOTSTAGED_FG, color.GIT_NOTSTAGED_BG) + add('untracked', color.GIT_UNTRACKED_FG, color.GIT_UNTRACKED_BG) + add('conflicted', color.GIT_CONFLICTED_FG, color.GIT_CONFLICTED_BG) + + +def get_valid_cwd(): + """ We check if the current working directory is valid or not. Typically + happens when you checkout a different branch on git that doesn't have + this directory. + We return the original cwd because the shell still considers that to be + the working directory, so returning our guess will confuse people + """ + # Prefer the PWD environment variable. Python's os.getcwd function follows + # symbolic links, which is undesirable. But if PWD is not set then fall + # back to this func + try: + cwd = os.getenv('PWD') or os.getcwd() + except: + warn("Your current directory is invalid. If you open a ticket at " + + "https://github.com/milkbikis/powerline-shell/issues/new " + + "we would love to help fix the issue.") + sys.stdout.write("> ") + sys.exit(1) + + parts = cwd.split(os.sep) + up = cwd + while parts and not os.path.exists(up): + parts.pop() + up = os.sep.join(parts) + if cwd != up: + warn("Your current directory is invalid. Lowest valid directory: " + + up) + return cwd + + +if __name__ == "__main__": + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument('--cwd-mode', action='store', + help='How to display the current directory', default='fancy', + choices=['fancy', 'plain', 'dironly']) + arg_parser.add_argument('--cwd-only', action='store_true', + help='Deprecated. Use --cwd-mode=dironly') + arg_parser.add_argument('--cwd-max-depth', action='store', type=int, + default=5, help='Maximum number of directories to show in path') + arg_parser.add_argument('--cwd-max-dir-size', action='store', type=int, + help='Maximum number of letters displayed for each directory in the path') + arg_parser.add_argument('--colorize-hostname', action='store_true', + help='Colorize the hostname based on a hash of itself.') + arg_parser.add_argument('--mode', action='store', default='patched', + help='The characters used to make separators between segments', + choices=['patched', 'compatible', 'flat']) + arg_parser.add_argument('--shell', action='store', default='bash', + help='Set this to your shell type', choices=['bash', 'zsh', 'bare']) + arg_parser.add_argument('prev_error', nargs='?', type=int, default=0, + help='Error code returned by the last command') + args = arg_parser.parse_args() + + powerline = Powerline(args, get_valid_cwd()) + + +class DefaultColor: + """ + This class should have the default colors for every segment. + Please test every new segment with this theme first. + """ + # RESET is not a real color code. It is used as in indicator + # within the code that any foreground / background color should + # be cleared + RESET = -1 + + USERNAME_FG = 250 + USERNAME_BG = 240 + USERNAME_ROOT_BG = 124 + + HOSTNAME_FG = 250 + HOSTNAME_BG = 238 + + HOME_SPECIAL_DISPLAY = True + HOME_BG = 31 # blueish + HOME_FG = 15 # white + PATH_BG = 237 # dark grey + PATH_FG = 250 # light grey + CWD_FG = 254 # nearly-white grey + SEPARATOR_FG = 244 + + READONLY_BG = 124 + READONLY_FG = 254 + + SSH_BG = 166 # medium orange + SSH_FG = 254 + + REPO_CLEAN_BG = 148 # a light green color + REPO_CLEAN_FG = 0 # black + REPO_DIRTY_BG = 161 # pink/red + REPO_DIRTY_FG = 15 # white + + JOBS_FG = 39 + JOBS_BG = 238 + + CMD_PASSED_BG = 236 + CMD_PASSED_FG = 15 + CMD_FAILED_BG = 161 + CMD_FAILED_FG = 15 + + SVN_CHANGES_BG = 148 + SVN_CHANGES_FG = 22 # dark green + + GIT_AHEAD_BG = 240 + GIT_AHEAD_FG = 250 + GIT_BEHIND_BG = 240 + GIT_BEHIND_FG = 250 + GIT_STAGED_BG = 22 + GIT_STAGED_FG = 15 + GIT_NOTSTAGED_BG = 130 + GIT_NOTSTAGED_FG = 15 + GIT_UNTRACKED_BG = 52 + GIT_UNTRACKED_FG = 15 + GIT_CONFLICTED_BG = 9 + GIT_CONFLICTED_FG = 15 + + VIRTUAL_ENV_BG = 35 # a mid-tone green + VIRTUAL_ENV_FG = 00 + +class Color(DefaultColor): + """ + This subclass is required when the user chooses to use 'default' theme. + Because the segments require a 'Color' class for every theme. + """ + pass + + +class DefaultColor: + """ + This class should have the default colors for every segment. + Please test every new segment with this theme first. + """ + # RESET is not a real color code. It is used as in indicator + # within the code that any foreground / background color should + # be cleared + RESET = -1 + + USERNAME_FG = 223 #fg1 + USERNAME_BG = 237 #bg1 + USERNAME_ROOT_BG = 172 #yellow (dark) + + HOSTNAME_FG = 223 #fg1 + HOSTNAME_BG = 235 #bg0 + + HOME_SPECIAL_DISPLAY = False + # HOME_BG = 239 #bg2 + HOME_FG = 223 #fg1 + PATH_BG = 239 #bg2 + PATH_FG = 223 #fg1 + CWD_FG = 223 #fg1 + SEPARATOR_FG = 246 #fg4 + + READONLY_BG = 124 #red (dark) + READONLY_FG = 223 #fg1 + + SSH_BG = 132 #purple (dark) + SSH_FG = 223 #fg1 + + REPO_CLEAN_BG = 106 #green (dark) + REPO_CLEAN_FG = 223 #fg1 + REPO_DIRTY_BG = 124 #red (dark) + REPO_DIRTY_FG = 223 #fg1 + + JOBS_FG = 223 #fg1 + JOBS_BG = 66 #blue (dark) + + CMD_PASSED_BG = 142 #green (light) + CMD_PASSED_FG = 235 #bg0 + CMD_FAILED_BG = 167 #red (light) + CMD_FAILED_FG = 235 #bg0 + + SVN_CHANGES_BG = 245 #gray + SVN_CHANGES_FG = 214 #yellow (light) + + GIT_AHEAD_BG = 241 #bg3 + GIT_AHEAD_FG = 223 #fg1 + GIT_BEHIND_BG = 241 #bg3 + GIT_BEHIND_FG = 223 #fg1 + GIT_STAGED_BG = 142 #green (light) + GIT_STAGED_FG = 235 #bg0 + GIT_NOTSTAGED_BG = 214 #yellow (light) + GIT_NOTSTAGED_FG = 235 #bg0 + GIT_UNTRACKED_BG = 167 #red (light) + GIT_UNTRACKED_FG = 235 #bg0 + GIT_CONFLICTED_BG = 167 #red (light) + GIT_CONFLICTED_FG = 124 #red (dark) + + VIRTUAL_ENV_BG = 106 #green (dark) + VIRTUAL_ENV_FG = 223 #fg1 + +class Color(DefaultColor): + """ + This subclass is required when the user chooses to use 'default' theme. + Because the segments require a 'Color' class for every theme. + """ + pass + + +def add_set_term_title_segment(powerline): + term = os.getenv('TERM') + if not (('xterm' in term) or ('rxvt' in term)): + return + + if powerline.args.shell == 'bash': + set_title = '\\[\\e]0;\\u@\\h: \\w\\a\\]' + elif powerline.args.shell == 'zsh': + set_title = '%{\033]0;%n@%m: %~\007%}' + else: + import socket + set_title = '\033]0;%s@%s: %s\007' % (os.getenv('USER'), socket.gethostname().split('.')[0], powerline.cwd or os.getenv('PWD')) + + powerline.append(set_title, None, None, '') + + + +add_set_term_title_segment(powerline) + +def add_username_segment(powerline): + import os + if powerline.args.shell == 'bash': + user_prompt = ' \\u ' + elif powerline.args.shell == 'zsh': + user_prompt = ' %n ' + else: + user_prompt = ' %s ' % os.getenv('USER') + + if os.getenv('USER') == 'root': + bgcolor = Color.USERNAME_ROOT_BG + else: + bgcolor = Color.USERNAME_BG + + powerline.append(user_prompt, Color.USERNAME_FG, bgcolor) + + +add_username_segment(powerline) +import os + +def add_ssh_segment(powerline): + + if os.getenv('SSH_CLIENT'): + powerline.append(' %s ' % powerline.network, Color.SSH_FG, Color.SSH_BG) + + +add_ssh_segment(powerline) +import os +import re +import subprocess +import platform + +def add_jobs_segment(powerline): + num_jobs = 0 + + if platform.system().startswith('CYGWIN'): + # cygwin ps is a special snowflake... + output_proc = subprocess.Popen(['ps', '-af'], stdout=subprocess.PIPE) + output = map(lambda l: int(l.split()[2].strip()), + output_proc.communicate()[0].decode("utf-8").splitlines()[1:]) + + num_jobs = output.count(os.getppid()) - 1 + + else: + + pppid_proc = subprocess.Popen(['ps', '-p', str(os.getppid()), '-oppid='], + stdout=subprocess.PIPE) + pppid = pppid_proc.communicate()[0].decode("utf-8").strip() + + output_proc = subprocess.Popen(['ps', '-a', '-o', 'ppid'], + stdout=subprocess.PIPE) + output = output_proc.communicate()[0].decode("utf-8") + + num_jobs = len(re.findall(str(pppid), output)) - 1 + + if num_jobs > 0: + powerline.append(' %d ' % num_jobs, Color.JOBS_FG, Color.JOBS_BG) + + +add_jobs_segment(powerline) +import os + +ELLIPSIS = u'\u2026' + + +def replace_home_dir(cwd): + home = os.getenv('HOME') + if cwd.startswith(home): + return '~' + cwd[len(home):] + return cwd + + +def split_path_into_names(cwd): + names = cwd.split(os.sep) + + if names[0] == '': + names = names[1:] + + if not names[0]: + return ['/'] + + return names + + +def requires_special_home_display(name): + """Returns true if the given directory name matches the home indicator and + the chosen theme should use a special home indicator display.""" + return (name == '~' and Color.HOME_SPECIAL_DISPLAY) + + +def maybe_shorten_name(powerline, name): + """If the user has asked for each directory name to be shortened, will + return the name up to their specified length. Otherwise returns the full + name.""" + if powerline.args.cwd_max_dir_size: + return name[:powerline.args.cwd_max_dir_size] + return name + + +def get_fg_bg(name, is_last_dir): + """Returns the foreground and background color to use for the given name. + """ + if requires_special_home_display(name): + return (Color.HOME_FG, Color.HOME_BG,) + + if is_last_dir: + return (Color.CWD_FG, Color.PATH_BG,) + else: + return (Color.PATH_FG, Color.PATH_BG,) + + +def add_cwd_segment(powerline): + cwd = powerline.cwd or os.getenv('PWD') + if not py3: + cwd = cwd.decode("utf-8") + cwd = replace_home_dir(cwd) + + if powerline.args.cwd_mode == 'plain': + powerline.append(' %s ' % (cwd,), Color.CWD_FG, Color.PATH_BG) + return + + names = split_path_into_names(cwd) + + max_depth = powerline.args.cwd_max_depth + if max_depth <= 0: + warn("Ignoring --cwd-max-depth argument since it's not greater than 0") + elif len(names) > max_depth: + # https://github.com/milkbikis/powerline-shell/issues/148 + # n_before is the number is the number of directories to put before the + # ellipsis. So if you are at ~/a/b/c/d/e and max depth is 4, it will + # show `~ a ... d e`. + # + # max_depth must be greater than n_before or else you end up repeating + # parts of the path with the way the splicing is written below. + n_before = 2 if max_depth > 2 else max_depth - 1 + names = names[:n_before] + [ELLIPSIS] + names[n_before - max_depth:] + + if (powerline.args.cwd_mode == 'dironly' or powerline.args.cwd_only): + # The user has indicated they only want the current directory to be + # displayed, so chop everything else off + names = names[-1:] + + for i, name in enumerate(names): + is_last_dir = (i == len(names) - 1) + fg, bg = get_fg_bg(name, is_last_dir) + + separator = powerline.separator_thin + separator_fg = Color.SEPARATOR_FG + if requires_special_home_display(name) or is_last_dir: + separator = None + separator_fg = None + + powerline.append(' %s ' % maybe_shorten_name(powerline, name), fg, bg, + separator, separator_fg) + + +add_cwd_segment(powerline) +import re +import subprocess +import os + +def get_PATH(): + """Normally gets the PATH from the OS. This function exists to enable + easily mocking the PATH in tests. + """ + return os.getenv("PATH") + +def git_subprocess_env(): + return { + # LANG is specified to ensure git always uses a language we are expecting. + # Otherwise we may be unable to parse the output. + "LANG": "C", + + # https://github.com/milkbikis/powerline-shell/pull/126 + "HOME": os.getenv("HOME"), + + # https://github.com/milkbikis/powerline-shell/pull/153 + "PATH": get_PATH(), + } + + +def parse_git_branch_info(status): + info = re.search('^## (?P\S+?)''(\.{3}(?P\S+?)( \[(ahead (?P\d+)(, )?)?(behind (?P\d+))?\])?)?$', status[0]) + return info.groupdict() if info else None + + +def _get_git_detached_branch(): + p = subprocess.Popen(['git', 'describe', '--tags', '--always'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=git_subprocess_env()) + detached_ref = p.communicate()[0].decode("utf-8").rstrip('\n') + if p.returncode == 0: + branch = u'{} {}'.format(RepoStats.symbols['detached'], detached_ref) + else: + branch = 'Big Bang' + return branch + + +def parse_git_stats(status): + stats = RepoStats() + for statusline in status[1:]: + code = statusline[:2] + if code == '??': + stats.untracked += 1 + elif code in ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'): + stats.conflicted += 1 + else: + if code[1] != ' ': + stats.not_staged += 1 + if code[0] != ' ': + stats.staged += 1 + + return stats + + +def add_git_segment(powerline): + try: + p = subprocess.Popen(['git', 'status', '--porcelain', '-b'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=git_subprocess_env()) + except OSError: + # Popen will throw an OSError if git is not found + return + + pdata = p.communicate() + if p.returncode != 0: + return + + status = pdata[0].decode("utf-8").splitlines() + stats = parse_git_stats(status) + branch_info = parse_git_branch_info(status) + + if branch_info: + stats.ahead = branch_info["ahead"] + stats.behind = branch_info["behind"] + branch = branch_info['local'] + else: + branch = _get_git_detached_branch() + + bg = Color.REPO_CLEAN_BG + fg = Color.REPO_CLEAN_FG + if stats.dirty: + bg = Color.REPO_DIRTY_BG + fg = Color.REPO_DIRTY_FG + + powerline.append(' %s ' % branch, fg, bg) + stats.add_to_powerline(powerline, Color) + + +add_git_segment(powerline) +import os + +def add_read_only_segment(powerline): + cwd = powerline.cwd or os.getenv('PWD') + + if not os.access(cwd, os.W_OK): + powerline.append(' %s ' % powerline.lock, Color.READONLY_FG, Color.READONLY_BG) + + +add_read_only_segment(powerline) +def add_root_segment(powerline): + root_indicators = { + 'bash': ' \\$ ', + 'zsh': ' %# ', + 'bare': ' $ ', + } + bg = Color.CMD_PASSED_BG + fg = Color.CMD_PASSED_FG + if powerline.args.prev_error != 0: + fg = Color.CMD_FAILED_FG + bg = Color.CMD_FAILED_BG + powerline.append(root_indicators[powerline.args.shell], fg, bg) + + +add_root_segment(powerline) +sys.stdout.write(powerline.draw())