From 5aca244430309339943d4bd53936aa18e31493e7 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Fri, 17 Feb 2012 15:20:55 -0600 Subject: rewrite my bash prompt in perl --- bashrc | 130 +--------------------- bin/fancy-prompt | 323 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 129 deletions(-) create mode 100755 bin/fancy-prompt diff --git a/bashrc b/bashrc index f7726e1..b11e20d 100644 --- a/bashrc +++ b/bashrc @@ -205,135 +205,7 @@ else export PROMPT_COMMAND="${PROMPT_COMMAND};history -a" fi # prompt {{{ -function _set_error { - __error=$(printf %03d $?) - if [[ "x${__error//0/}" != "x" ]]; then - __error_color=$HIRED - else - __error_color=$NORM - fi -} -function _set_vcs { - local vcs - local vcs_dirty - local vcs_branch - local vcs_dir - local vcs_state - local vcs_local_commits - __vcs='' - function _find_upwards { - local pwd - pwd=$(pwd) - while [[ -n "$pwd" ]]; do - if [[ -d "$pwd/$1" ]]; then - return 0 - fi - pwd=${pwd%/*} - done - return 1 - } - function _find_svn { - [[ -d '.svn' ]] || return 1 - vcs='svn' - } - function _find_darcs { - if _find_upwards '_darcs'; then - vcs='darcs' - return 0 - fi - return 1 - } - function _find_git { - if _find_upwards '.git'; then - vcs='git' - return 0 - fi - return 1 - } - - _find_svn || _find_darcs || _find_git - - case "$vcs" in - svn) - vcs_dirty=$([[ -n "$(svn status | grep -v '^?')" ]]; echo $?) - if [[ "$vcs_dirty" -eq 0 ]]; then - vcs_dirty='*' - else - vcs_dirty='' - fi - vcs_branch= - ;; - darcs) - vcs_dirty=$(darcs whatsnew > /dev/null 2>&1; echo $?) - if [[ "$vcs_dirty" -eq 0 ]]; then - vcs_dirty='*' - else - vcs_dirty='' - fi - vcs_branch= - ;; - git) - local git_dir - local git_base - git_dir=$(git rev-parse --git-dir) - vcs_branch=$(git symbolic-ref -q HEAD 2>/dev/null) - vcs_dirty='' - if git diff --no-ext-diff --ignore-submodules --quiet --exit-code; then - : - else - vcs_dirty="${vcs_dirty}*" - fi - if git diff-index --cached --quiet --ignore-submodules HEAD; then - : - else - vcs_dirty="${vcs_dirty}+" - fi - if [[ -z "${vcs_branch}" ]]; then - vcs_branch=$(git rev-parse --short HEAD) - else - vcs_branch=${vcs_branch#refs/heads/} - if git show-ref -q --verify "refs/remotes/origin/$vcs_branch"; then - git_base=$(git merge-base HEAD origin/$vcs_branch) - if [[ "${git_base}" == "$(git rev-parse HEAD)" ]]; then - vcs_local_commits="-$(git rev-list HEAD..origin/$vcs_branch | wc -l)" - elif [[ "${git_base}" == "$(git rev-parse origin/$vcs_branch)" ]]; then - vcs_local_commits="+$(git rev-list origin/${vcs_branch}..HEAD | wc -l)" - else - vcs_local_commits="+$(git rev-list ${git_base}..HEAD | wc -l)-$(git rev-list ${git_base}..origin/$vcs_branch | wc -l)" - fi - fi - fi - if [[ -e "${git_dir}/MERGE_HEAD" ]]; then - vcs_state='merge' - fi - if [[ -d "${git_dir}/rebase-apply" || -d "${git_dir}/rebase-merge" ]]; then - vcs_state='rebase' - fi - if [[ -e "${git_dir}/BISECT_START" ]]; then - vcs_state='bisect' - fi - if [[ "x$vcs_branch" == "xmaster" ]]; then - vcs_branch='' - fi - ;; - *) return 0 ;; - esac - if [[ -n "$vcs_branch" ]]; then - vcs_branch=":$vcs_branch" - fi - if [[ -n "$vcs_state" ]]; then - vcs_state="(${vcs_state:0:1})" - fi - if [[ "$vcs_local_commits" == "-0" ]]; then - vcs_local_commits='' - fi - if [[ -n "$vcs_local_commits" ]]; then - vcs_local_commits=":$vcs_local_commits" - fi - __vcs="(${vcs:0:1}${vcs_dirty}${vcs_branch}${vcs_local_commits}${vcs_state})" -} -export PROMPT_COMMAND="_set_error;_set_vcs;$PROMPT_COMMAND" -export PS1="\[\$__error_color\]\$__error \[${HIYELLOW}\][\t] \[${HIGREEN}\]\u@\h \[${HIBLUE}\]\W\[${CYAN}\]\$__vcs \[${HIBLUE}\]\\$\[${NORM}\] " +export PROMPT_COMMAND="__err=\$?;$PROMPT_COMMAND;PS1=\"\$(fancy-prompt "\$__err")\"" # }}} # set the correct perl {{{ if type -a perlbrew > /dev/null 2>&1; then diff --git a/bin/fancy-prompt b/bin/fancy-prompt new file mode 100755 index 0000000..d9f9809 --- /dev/null +++ b/bin/fancy-prompt @@ -0,0 +1,323 @@ +#!/usr/bin/env perl +# keep these commented out in actual use, for speed (saves ~2ms) +# use strict; +# use warnings; + +# collect information +chomp(my $hostname = `hostname`); +my $cols = `tput cols`; +my $pwd = $ENV{PWD}; +my $user = $ENV{USER}; +my $root = ($> == 0); +(my $time = localtime) =~ s/.*(\d\d:\d\d:\d\d).*/$1/; +my $err = $ARGV[0] || 0; # bash's $? is passed on the command line +my $battery = battery(); +my $charging = charging(); +my $in_tmux = exists $ENV{TMUX}; +my $in_ssh = exists $ENV{SSH_CONNECTION}; +my $vcs = vcs(); + +# initialize colors +my $tput = join("\n", map { "setf $_" } 0..15); +my @colors = qw( + black blue green cyan red magenta brown grey + darkgrey lightred lightgreen yellow lightblue lightmagenta lightcyan white +); +my %colors; +my $termcolors = `echo "$tput" | tput -S`; +if (!$?) { + my @termcolors = split /(?=\e)/, $termcolors; + $colors{$colors[$_]} = $termcolors[$_] for 0..$#colors; + $colors{reset} = `tput sgr0`; # this can be multiple escapes +} +else { + # hardcoded fallback, if terminfo isn't available + %colors = ( + reset => "\e[m", + + black => "\e[0;30m", + blue => "\e[0;34m", + green => "\e[0;32m", + cyan => "\e[0;36m", + red => "\e[0;31m", + magenta => "\e[0;35m", + brown => "\e[0;33m", + grey => "\e[0;37m", + + darkgrey => "\e[1;30m", + lightred => "\e[1;31m", + lightgreen => "\e[1;32m", + yellow => "\e[1;33m", + lightblue => "\e[1;34m", + lightmagenta => "\e[1;35m", + lightcyan => "\e[1;36m", + white => "\e[1;37m", + ); +} + +my %colornames = ( + bishamon => 'blue', + erim => 'white', + magus => 'darkgrey', + tozt => 'yellow', + xtahua => 'lightmagenta', + zaon => 'lightgreen', + + error => 'red', + + user => 'lightblue', + root => 'lightred', + + path => 'blue', + ropath => 'red', + + vcs => 'cyan', + + battwarn => 'brown', + battcrit => 'red', + battemerg => 'lightred', + + time => 'blue', + + background => 'darkgrey', + reset => 'reset', +); +$colornames{hostname} = $colornames{$hostname}; +$colornames{user} = $colornames{root} if $root; +$colornames{path} = $colornames{ropath} unless -w $pwd; +$colornames{error} = $colornames{background} unless $err; +$colornames{battery} = $battery > 40 ? $colornames{background} + : $battery > 15 ? $colornames{battwarn} + : $battery > 5 ? $colornames{battcrit} + : $colornames{battemerg}; +sub color { $colors{$colornames{$_[0]}} } + +# define some display things +my $border = '-'; +my $meter = '='; +my $empty_meter = '-'; +my $meter_end = $charging ? '<' : '>'; +my $prompt = $root ? '#' : '$'; +my $meter_length = 10; +my $maxvcslen = 20; # at least 17 + +# snippets +my $nested = ''; +# not sure how useful this really is, let's leave it off for now +# if ($in_tmux || $in_ssh) { +# $nested = '(' . ($in_tmux ? 'T' : '') . ($in_ssh ? 'S' : '') . ')'; +# } + +# build the pieces of the prompt +my $timestr = wrap($time, '[]', color('time')); + +my $errstr = color('error') . sprintf('%03d', $err); + +my $promptstr = color('user') . $prompt . color('reset'); + +my $full_length = int($battery / 100 * $meter_length); +my $battstr = wrap(($empty_meter x ($meter_length - $full_length)) + . $meter_end . ($meter x ($full_length - 1)), + '{}', + color('battery'));; + +my $trimmed_vcs = trimvcs($vcs, $maxvcslen); +my $vcslen = $trimmed_vcs ? length($trimmed_vcs) + 1 : 0; +my $maxpathlen = $cols + - 1 + - $vcslen - 2 + - 1 + - 1 + - $meter_length - 2 + - 1 + - length($user) - length($hostname) - length($nested) - 1 + - 1 + - length($time) - 2 + - 1; + +my $trimmed_pwd = trimpath($pwd, $maxpathlen - 1); +my $path_info = $trimmed_pwd + . ($trimmed_vcs ? (color('background') . '|') : '') + . color('vcs') . $trimmed_vcs; +my $pathstr = wrap($path_info, '()', color('path')) + . ' ' . color('background') . ($border x ($maxpathlen - length($trimmed_pwd))); + +my $locstr = color('user') . $user + . color('background') . '@' + . color('hostname') . $hostname + . color('background') . $nested; + +# assemble the prompt +print join(' ', '', $pathstr, $battstr, $locstr, $timestr, ''), "\n"; +print join(' ', $errstr, $promptstr, ''); + +sub battery { + my $now = slurp('/sys/class/power_supply/BAT0/charge_now'); + my $full = slurp('/sys/class/power_supply/BAT0/charge_full'); + return int(100 * $now / $full); +} + +sub charging { + my $ac = slurp('/sys/class/power_supply/AC/online'); + chomp($ac); + return $ac; +} + +sub slurp { + open my $fh, '<', $_[0]; + local $/; + <$fh>; +} + +sub wrap { + my ($str, $wrapper, $color) = @_; + $wrapper = $wrapper x 2 if length($wrapper) == 1; + return color('hostname') . substr($wrapper, 0, 1) + . $color . $str + . color('hostname') . substr($wrapper, 1, 1); +} + +sub trimpath { + my ($path, $max) = @_; + + $path =~ s/^$ENV{HOME}/~/; + + while (length($path) > $max) { + $path =~ s|([^/])[^/]+/|$1/| + or last; + } + + if (length($path) > $max) { + $path = substr($path, 0, $max - 6) . '...' . substr($path, -3); + } + + return $path; +} + +sub trimvcs { + my ($vcsinfo, $max) = @_; + return $vcsinfo if length($vcsinfo) <= $max; + my @parts = split /:/, $vcsinfo; + $max -= length($parts[0]) + 1; + $max -= length($parts[2]) + 1 if $parts[2]; + $parts[1] = substr($parts[1], 0, $max - 6) . '...' . substr($parts[1], -3); + return join(':', @parts); +} + +sub vcs { + my ($vcs, $vcsdir) = find_vcs(); + return '' unless $vcs; + return git_info($vcsdir) if $vcs eq 'git'; + return svn_info($vcsdir) if $vcs eq 'svn'; + return darcs_info($vcsdir) if $vcs eq 'darcs'; + return; +} + +sub find_upwards { + my ($vcsdir) = @_; + + my $path = $pwd; + while ($path) { + if (-d "$path/$vcsdir") { + return "$path/$vcsdir"; + } + $path =~ s{/.*?$}{}; + } + + return; +} + +sub find_svn { -d "$pwd/.svn" ? "$pwd/.svn" : undef } +sub find_git { + chomp(my $gitdir = `git rev-parse --git-dir 2>/dev/null`); + return $gitdir; +} +sub find_darcs { find_upwards('_darcs') } + +sub find_vcs { + if (my $git = find_git()) { + return ('git', $git); + } + elsif (my $svn = find_svn()) { + return ('svn', $svn); + } + elsif (my $darcs = find_darcs()) { + return ('darcs', $darcs); + } + return; +} + +sub svn_info { + my $dirty = ''; + if (!system(q[svn status | grep -qv '^?'])) { + $dirty .= '*'; + } + + return 's' . $dirty; +} + +sub darcs_info { + my $dirty = ''; + if (!system(q[darcs whatsnew > /dev/null 2>&1])) { + $dirty .= '*'; + } + + return 'd' . $dirty; +} + +sub git_info { + my ($git_dir) = @_; + + my $dirty = ''; + if (system(q[git diff --no-ext-diff --ignore-submodules --quiet --exit-code])) { + $dirty .= '*'; + } + if (system(q[git diff-index --cached --quiet --ignore-submodules HEAD])) { + $dirty .= '+'; + } + + chomp(my $branch = `git symbolic-ref -q HEAD 2>/dev/null` + || `git rev-parse --short HEAD`); + $branch =~ s{^refs/heads/}{}; + + my $local_commits = ''; + system(qq[git show-ref -q --verify "refs/remotes/origin/$branch"]); + if (!$?) { + chomp(my $base = `git merge-base HEAD origin/$branch`); + chomp(my $head = `git rev-parse HEAD`); + chomp(my $remote = `git rev-parse origin/$branch`); + if ($base eq $head) { + my @revs = `git rev-list HEAD..origin/$branch`; + $local_commits = '-' . scalar(@revs); + } + elsif ($base eq $remote) { + my @revs = `git rev-list origin/$branch..HEAD`; + $local_commits = '+' . scalar(@revs); + } + else { + my @head_revs = `git rev-list $base..HEAD`; + my @remote_revs = `git rev-list $base..origin/$branch`; + $local_commits = '+' . scalar(@head_revs) + . '-' . scalar(@remote_revs); + } + } + + $local_commits = '' if $local_commits eq '-0'; + $local_commits = ':' . $local_commits if $local_commits; + + my $state = ''; + if (-e "$git_dir/MERGE_HEAD") { + $state = 'm'; + } + elsif (-d "$git_dir/rebase-apply" || -d "$git_dir/rebase-merge") { + $state = 'r'; + } + elsif (-e "$git_dir/BISECT_START") { + $state = 'b'; + } + + $branch = '' if $branch eq 'master'; + $branch = ':' . $branch if $branch; + + return 'g' . $dirty . $branch . $local_commits . $state; +} -- cgit v1.2.3-54-g00ecf