diff --git a/bin/diff-so-fancy b/bin/diff-so-fancy new file mode 100755 index 0000000..2e165ad --- /dev/null +++ b/bin/diff-so-fancy @@ -0,0 +1,1442 @@ +#!/usr/bin/env perl + +# This chunk of stuff was generated by App::FatPacker. To find the original +# file's code, look for the end of this BEGIN block or the string 'FATPACK' +BEGIN { +my %fatpacked; + +$fatpacked{"DiffHighlight.pm"} = '#line '.(1+__LINE__).' "'.__FILE__."\"\n".<<'DIFFHIGHLIGHT'; + package DiffHighlight; + + use 5.008; + use warnings FATAL => 'all'; + use strict; + + # Use the correct value for both UNIX and Windows (/dev/null vs nul) + use File::Spec; + + my $NULL = File::Spec->devnull(); + + # Highlight by reversing foreground and background. You could do + # other things like bold or underline if you prefer. + our @OLD_HIGHLIGHT = ( + undef, + "\e[7m", + "\e[27m", + ); + our @NEW_HIGHLIGHT = ( + $OLD_HIGHLIGHT[0], + $OLD_HIGHLIGHT[1], + $OLD_HIGHLIGHT[2], + ); + + + + my $RESET = "\x1b[m"; + my $COLOR = qr/\x1b\[[0-9;]*m/; + my $BORING = qr/$COLOR|\s/; + + my @removed; + my @added; + my $in_hunk; + my $graph_indent = 0; + + our $line_cb = sub { print @_ }; + our $flush_cb = sub { local $| = 1 }; + + # Count the visible width of a string, excluding any terminal color sequences. + sub visible_width { + local $_ = shift; + my $ret = 0; + while (length) { + if (s/^$COLOR//) { + # skip colors + } elsif (s/^.//) { + $ret++; + } + } + return $ret; + } + + # Return a substring of $str, omitting $len visible characters from the + # beginning, where terminal color sequences do not count as visible. + sub visible_substr { + my ($str, $len) = @_; + while ($len > 0) { + if ($str =~ s/^$COLOR//) { + next + } + $str =~ s/^.//; + $len--; + } + return $str; + } + + sub handle_line { + my $orig = shift; + local $_ = $orig; + + # match a graph line that begins a commit + if (/^(?:$COLOR?\|$COLOR?[ ])* # zero or more leading "|" with space + $COLOR?\*$COLOR?[ ] # a "*" with its trailing space + (?:$COLOR?\|$COLOR?[ ])* # zero or more trailing "|" + [ ]* # trailing whitespace for merges + /x) { + my $graph_prefix = $&; + + # We must flush before setting graph indent, since the + # new commit may be indented differently from what we + # queued. + flush(); + $graph_indent = visible_width($graph_prefix); + + } elsif ($graph_indent) { + if (length($_) < $graph_indent) { + $graph_indent = 0; + } else { + $_ = visible_substr($_, $graph_indent); + } + } + + if (!$in_hunk) { + $line_cb->($orig); + $in_hunk = /^$COLOR*\@\@ /; + } + elsif (/^$COLOR*-/) { + push @removed, $orig; + } + elsif (/^$COLOR*\+/) { + push @added, $orig; + } + else { + flush(); + $line_cb->($orig); + $in_hunk = /^$COLOR*[\@ ]/; + } + + # Most of the time there is enough output to keep things streaming, + # but for something like "git log -Sfoo", you can get one early + # commit and then many seconds of nothing. We want to show + # that one commit as soon as possible. + # + # Since we can receive arbitrary input, there's no optimal + # place to flush. Flushing on a blank line is a heuristic that + # happens to match git-log output. + if (!length) { + $flush_cb->(); + } + } + + sub flush { + # Flush any queued hunk (this can happen when there is no trailing + # context in the final diff of the input). + show_hunk(\@removed, \@added); + @removed = (); + @added = (); + } + + sub highlight_stdin { + while () { + handle_line($_); + } + flush(); + } + + # Ideally we would feed the default as a human-readable color to + # git-config as the fallback value. But diff-highlight does + # not otherwise depend on git at all, and there are reports + # of it being used in other settings. Let's handle our own + # fallback, which means we will work even if git can't be run. + sub color_config { + my ($key, $default) = @_; + + # Removing the redirect speeds up execution by about 12ms + #my $s = `git config --get-color $key 2>$NULL`; + my $s = `git config --get-color $key`; + + return length($s) ? $s : $default; + } + + sub show_hunk { + my ($a, $b) = @_; + + # If one side is empty, then there is nothing to compare or highlight. + if (!@$a || !@$b) { + $line_cb->(@$a, @$b); + return; + } + + # If we have mismatched numbers of lines on each side, we could try to + # be clever and match up similar lines. But for now we are simple and + # stupid, and only handle multi-line hunks that remove and add the same + # number of lines. + if (@$a != @$b) { + $line_cb->(@$a, @$b); + return; + } + + my @queue; + for (my $i = 0; $i < @$a; $i++) { + my ($rm, $add) = highlight_pair($a->[$i], $b->[$i]); + $line_cb->($rm); + push @queue, $add; + } + $line_cb->(@queue); + } + + sub highlight_pair { + my @a = split_line(shift); + my @b = split_line(shift); + + # Find common prefix, taking care to skip any ansi + # color codes. + my $seen_plusminus; + my ($pa, $pb) = (0, 0); + while ($pa < @a && $pb < @b) { + if ($a[$pa] =~ /$COLOR/) { + $pa++; + } + elsif ($b[$pb] =~ /$COLOR/) { + $pb++; + } + elsif ($a[$pa] eq $b[$pb]) { + $pa++; + $pb++; + } + elsif (!$seen_plusminus && $a[$pa] eq '-' && $b[$pb] eq '+') { + $seen_plusminus = 1; + $pa++; + $pb++; + } + else { + last; + } + } + + # Find common suffix, ignoring colors. + my ($sa, $sb) = ($#a, $#b); + while ($sa >= $pa && $sb >= $pb) { + if ($a[$sa] =~ /$COLOR/) { + $sa--; + } + elsif ($b[$sb] =~ /$COLOR/) { + $sb--; + } + elsif ($a[$sa] eq $b[$sb]) { + $sa--; + $sb--; + } + else { + last; + } + } + + if (is_pair_interesting(\@a, $pa, $sa, \@b, $pb, $sb)) { + return highlight_line(\@a, $pa, $sa, \@OLD_HIGHLIGHT), + highlight_line(\@b, $pb, $sb, \@NEW_HIGHLIGHT); + } + else { + return join('', @a), + join('', @b); + } + } + + # we split either by $COLOR or by character. This has the side effect of + # leaving in graph cruft. It works because the graph cruft does not contain "-" + # or "+" + sub split_line { + local $_ = shift; + return utf8::decode($_) ? + map { utf8::encode($_); $_ } + map { /$COLOR/ ? $_ : (split //) } + split /($COLOR+)/ : + map { /$COLOR/ ? $_ : (split //) } + split /($COLOR+)/; + } + + sub highlight_line { + my ($line, $prefix, $suffix, $theme) = @_; + + my $start = join('', @{$line}[0..($prefix-1)]); + my $mid = join('', @{$line}[$prefix..$suffix]); + my $end = join('', @{$line}[($suffix+1)..$#$line]); + + # If we have a "normal" color specified, then take over the whole line. + # Otherwise, we try to just manipulate the highlighted bits. + if (defined $theme->[0]) { + s/$COLOR//g for ($start, $mid, $end); + chomp $end; + return join('', + $theme->[0], $start, $RESET, + $theme->[1], $mid, $RESET, + $theme->[0], $end, $RESET, + "\n" + ); + } else { + return join('', + $start, + $theme->[1], $mid, $theme->[2], + $end + ); + } + } + + # Pairs are interesting to highlight only if we are going to end up + # highlighting a subset (i.e., not the whole line). Otherwise, the highlighting + # is just useless noise. We can detect this by finding either a matching prefix + # or suffix (disregarding boring bits like whitespace and colorization). + sub is_pair_interesting { + my ($a, $pa, $sa, $b, $pb, $sb) = @_; + my $prefix_a = join('', @$a[0..($pa-1)]); + my $prefix_b = join('', @$b[0..($pb-1)]); + my $suffix_a = join('', @$a[($sa+1)..$#$a]); + my $suffix_b = join('', @$b[($sb+1)..$#$b]); + + return visible_substr($prefix_a, $graph_indent) !~ /^$COLOR*-$BORING*$/ || + visible_substr($prefix_b, $graph_indent) !~ /^$COLOR*\+$BORING*$/ || + $suffix_a !~ /^$BORING*$/ || + $suffix_b !~ /^$BORING*$/; + } +DIFFHIGHLIGHT + +s/^ //mg for values %fatpacked; + +my $class = 'FatPacked::'.(0+\%fatpacked); +no strict 'refs'; +*{"${class}::files"} = sub { keys %{$_[0]} }; + +if ($] < 5.008) { + *{"${class}::INC"} = sub { + if (my $fat = $_[0]{$_[1]}) { + my $pos = 0; + my $last = length $fat; + return (sub { + return 0 if $pos == $last; + my $next = (1 + index $fat, "\n", $pos) || $last; + $_ .= substr $fat, $pos, $next - $pos; + $pos = $next; + return 1; + }); + } + }; +} + +else { + *{"${class}::INC"} = sub { + if (my $fat = $_[0]{$_[1]}) { + open my $fh, '<', \$fat + or die "FatPacker error loading $_[1] (could be a perl installation issue?)"; + return $fh; + } + return; + }; +} + +unshift @INC, bless \%fatpacked, $class; + } # END OF FATPACK CODE + + +my $VERSION = "1.4.2"; + +################################################################################# + +use v5.010; # Require Perl 5.10 for 'state' variables +use warnings FATAL => 'all'; +use strict; + +use File::Spec; # For catdir +use File::Basename; # For dirname +use Cwd qw(abs_path); # For realpath() +use lib dirname(abs_path(File::Spec->catdir($0))) . "/lib"; # Add the local lib/ to @INC +use DiffHighlight; + +my $remove_file_add_header = 1; +my $remove_file_delete_header = 1; +my $clean_permission_changes = 1; +my $patch_mode = 0; +my $manually_color_lines = 0; # Usually git/hg colorizes the lines, but for raw patches we use this +my $change_hunk_indicators = git_config_boolean("diff-so-fancy.changeHunkIndicators","true"); +my $strip_leading_indicators = git_config_boolean("diff-so-fancy.stripLeadingSymbols","true"); +my $mark_empty_lines = git_config_boolean("diff-so-fancy.markEmptyLines","true"); +my $use_unicode_dash_for_ruler = git_config_boolean("diff-so-fancy.useUnicodeRuler","true"); +my $ruler_width = git_config("diff-so-fancy.rulerWidth", undef); +my $git_strip_prefix = git_config_boolean("diff.noprefix","false"); +my $has_stdin = has_stdin(); + +my $ansi_color_regex = qr/(\e\[([0-9]{1,3}(;[0-9]{1,3}){0,10})[mK])?/; +my $reset_color = color("reset"); +my $bold = color("bold"); +my $meta_color = ""; + +# Set the diff highlight colors from the config +init_diff_highlight_colors(); + +my ($file_1,$file_2); +my $args = argv(); # Hashref of all the ARGV stuff +my $last_file_seen = ""; +my $last_file_mode = ""; +my $i = 0; +my $in_hunk = 0; +my $columns_to_remove = 0; +my $is_mercurial = 0; +my $color_forced = 0; # Has the color been forced on/off + +# We try and be smart about whether we need to do line coloring, but +# this is an option to force it on/off +if ($args->{color_on}) { + $manually_color_lines = 1; + $color_forced = 1; +} elsif ($args->{color_off}) { + $manually_color_lines = 0; + $color_forced = 1; +} + +if ($args->{debug}) { + show_debug_info(); + exit(); +} + +# `git add --patch` requires our output to match the number of lines from the +# input. So, when patch mode is active, we print out empty lines to pad our +# output to match any lines we've consumed. +if ($args->{patch}) { + $patch_mode = 1; +} + +# We only process ARGV if we don't have STDIN +if (!$has_stdin) { + if ($args->{v} || $args->{version}) { + die(version()); + } elsif ($args->{'set-defaults'}) { + my $ok = set_defaults(); + exit; + } elsif ($args->{colors}) { + # We print this to STDOUT so we can redirect to bash to auto-set the colors + print get_default_colors(); + exit; + } elsif (!%$args || $args->{help} || $args->{h}) { + my $first = check_first_run(); + + if (!$first) { + die(usage()); + } + } else { + die("Missing input on STDIN\n"); + } +} + +################################################################################# +################################################################################# + +# Check to see if were using default settings +check_first_run(); + +# The logic here is that we run all the lines through DiffHighlight first. This +# highlights all the intra-word changes. Then we take those lines and send them +# to do_dsf_stuff() to convert the diff to human readable d-s-f output and add +# appropriate fanciness + +my @lines; +local $DiffHighlight::line_cb = sub { + push(@lines,@_); + + my $last_line = $lines[-1]; + + # Buffer X lines before we try and output anything + # Also make sure we're sending enough data to d-s-f to do it's magic. + # Certain things require a look-ahead line or two to function so + # we make sure we don't break on those sections prematurely + if (@lines > 24 && ($last_line !~ /^${ansi_color_regex}(---|index|old mode|similarity index|rename (from|to))/)) { + do_dsf_stuff(\@lines); + @lines = (); + } +}; + +my $line_count = 0; +while (my $line = ) { + # If the very first line of the diff doesn't start with ANSI color we're assuming + # it's a raw patch file, and we have to color the added/removed lines ourself + if (!$color_forced && $line_count == 0 && starts_with_ansi($line)) { + $manually_color_lines = 1; + } + + my $ok = DiffHighlight::handle_line($line); + $line_count++; +} + +# If we're mid hunk above process anything still pending +DiffHighlight::flush(); +do_dsf_stuff(\@lines); + +################################################################################# +################################################################################# + +sub do_dsf_stuff { + my $input = shift(); + + #print STDERR "START -------------------------------------------------\n"; + #print STDERR join("",@$input); + #print STDERR "END ---------------------------------------------------\n"; + + while (my $line = shift(@$input)) { + ###################################################### + # Pre-process the line before we do any other markup # + ###################################################### + + # If the first line of the input is a blank line, skip that + if ($i == 0 && $line =~ /^\s*$/) { + next; + } + + ###################### + # End pre-processing # + ###################### + + ####################################################################### + + #################################################################### + # Look for git index and replace it horizontal line (header later) # + #################################################################### + if ($line =~ /^${ansi_color_regex}index /) { + # Print the line color and then the actual line + $meta_color = $1 || get_config_color("meta"); + + # Get the next line without incrementing counter while loop + my $next = $input->[0] || ""; + my ($file_1,$file_2); + + # The line immediately after the "index" line should be the --- file line + # If it's not it's an empty file add/delete + if ($next !~ /^$ansi_color_regex(---|Binary files)/) { + + # We fake out the file names since it's a raw add/delete + if ($last_file_mode eq "add") { + $file_1 = "/dev/null"; + $file_2 = $last_file_seen; + } elsif ($last_file_mode eq "delete") { + $file_1 = $last_file_seen; + $file_2 = "/dev/null"; + } + } + + if ($file_1 && $file_2) { + print horizontal_rule($meta_color); + print $meta_color . file_change_string($file_1,$file_2) . "\n"; + print horizontal_rule($meta_color); + } + ######################### + # Look for the filename # + ######################### + # $4 $5 + } elsif ($line =~ /^${ansi_color_regex}diff (-r|--git|--cc) (.*?)(\e| b\/|$)/) { + + # Mercurial looks like: diff -r 82e55d328c8c hello.c + if ($4 eq "-r") { + $is_mercurial = 1; + $meta_color = get_config_color("meta"); + # Git looks like: diff --git a/diff-so-fancy b/diff-so-fancy + } else { + $last_file_seen = $5; + } + + $last_file_seen =~ s|^\w/||; # Remove a/ (and handle diff.mnemonicPrefix). + $in_hunk = 0; + if ($patch_mode) { + # we are consuming one line, and the debt must be paid + print "\n"; + } + ######################################## + # Find the first file: --- a/README.md # + ######################################## + } elsif (!$in_hunk && $line =~ /^$ansi_color_regex--- (\w\/)?(.+?)(\e|\t|$)/) { + $meta_color = get_config_color("meta"); + + if ($git_strip_prefix) { + my $file_dir = $4 || ""; + $file_1 = $file_dir . $5; + } else { + $file_1 = $5; + } + + # Find the second file on the next line: +++ b/README.md + my $next = shift(@$input); + $next =~ /^$ansi_color_regex\+\+\+ (\w\/)?(.+?)(\e|\t|$)/; + if ($1) { + print $1; # Print out whatever color we're using + } + if ($git_strip_prefix) { + my $file_dir = $4 || ""; + $file_2 = $file_dir . $5; + } else { + $file_2 = $5; + } + + if ($file_2 ne "/dev/null") { + $last_file_seen = $file_2; + } + + # Print out the top horizontal line of the header + print $reset_color; + print horizontal_rule($meta_color); + + # Mercurial coloring is slightly different so we need to hard reset colors + if ($is_mercurial) { + print $reset_color; + } + + print $meta_color; + print file_change_string($file_1,$file_2) . "\n"; + + # Print out the bottom horizontal line of the header + print horizontal_rule($meta_color); + ######################################## + # Check for "@@ -3,41 +3,63 @@" syntax # + ######################################## + } elsif (!$change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) { + $in_hunk = 1; + + print $line; + } elsif ($change_hunk_indicators && $line =~ /^${ansi_color_regex}(@@@* .+? @@@*)(.*)/) { + $in_hunk = 1; + + my $hunk_header = $4; + my $remain = bleach_text($5); + + # The number of colums to remove (1 or 2) is based on how many commas in the hunk header + $columns_to_remove = (char_count(",",$hunk_header)) - 1; + # On single line removes there is NO comma in the hunk so we force one + if ($columns_to_remove <= 0) { + $columns_to_remove = 1; + } + + if ($1) { + print $1; # Print out whatever color we're using + } + + my ($orig_offset, $orig_count, $new_offset, $new_count) = parse_hunk_header($hunk_header); + #$last_file_seen = basename($last_file_seen); + + # Figure out the start line + my $start_line = start_line_calc($new_offset,$new_count); + + # Last function has it's own color + my $last_function_color = ""; + if ($remain) { + $last_function_color = get_config_color("last_function"); + } + + # Check to see if we have the color for the fragment from git + if ($5 =~ /\e\[\d/) { + #print "Has ANSI color for fragment\n"; + } else { + # We don't have the ANSI sequence so we shell out to get it + #print "No ANSI color for fragment\n"; + my $frag_color = get_config_color("fragment"); + print $frag_color; + } + + print "@ $last_file_seen:$start_line \@${bold}${last_function_color}${remain}${reset_color}\n"; + ################################### + # Remove any new file permissions # + ################################### + } elsif ($remove_file_add_header && $line =~ /^${ansi_color_regex}.*new file mode/) { + # Don't print the line (i.e. remove it from the output); + $last_file_mode = "add"; + if ($patch_mode) { + print "\n"; + } + ###################################### + # Remove any delete file permissions # + ###################################### + } elsif ($remove_file_delete_header && $line =~ /^${ansi_color_regex}deleted file mode/) { + # Don't print the line (i.e. remove it from the output); + $last_file_mode = "delete"; + if ($patch_mode) { + print "\n"; + } + ################################ + # Look for binary file changes # + ################################ + } elsif ($line =~ /^Binary files (\w\/)?(.+?) and (\w\/)?(.+?) differ/) { + my $change = file_change_string($2,$4); + print horizontal_rule($meta_color); + print "$meta_color$change (binary)\n"; + print horizontal_rule($meta_color); + ##################################################### + # Check if we're changing the permissions of a file # + ##################################################### + } elsif ($clean_permission_changes && $line =~ /^${ansi_color_regex}old mode (\d+)/) { + my ($old_mode) = $4; + my $next = shift(@$input); + + if ($1) { + print $1; # Print out whatever color we're using + } + + my ($new_mode) = $next =~ m/new mode (\d+)/; + + if ($patch_mode) { + print "\n"; + } + print "$last_file_seen changed file mode from $old_mode to $new_mode\n"; + + ############### + # File rename # + ############### + } elsif ($line =~ /^${ansi_color_regex}similarity index (\d+)%/) { + my $simil = $4; + + # If it's a move with content change we ignore this and the next two lines + if ($simil != 100) { + shift(@$input); + shift(@$input); + next; + } + + my $next = shift(@$input); + my ($file1) = $next =~ /rename from (.+?)(\e|\t|$)/; + + $next = shift(@$input); + my ($file2) = $next =~ /rename to (.+?)(\e|\t|$)/; + + if ($file1 && $file2) { + # We may not have extracted this yet, so we pull from the config if not + $meta_color = get_config_color("meta"); + + my $change = file_change_string($file1,$file2); + + print horizontal_rule($meta_color); + print $meta_color . $change . "\n"; + print horizontal_rule($meta_color); + } + + $i += 3; # We've consumed three lines + next; + ##################################### + # Just a regular line, print it out # + ##################################### + } else { + # Mark empty line with a red/green box indicating addition/removal + if ($mark_empty_lines) { + $line = mark_empty_line($line); + } + + # Remove the correct number of leading " " or "+" or "-" + if ($strip_leading_indicators) { + $line = strip_leading_indicators($line,$columns_to_remove); + } + print $line; + } + + $i++; + } +} + +###################################################################################################### +# End regular code, begin functions +###################################################################################################### + +# Courtesy of github.com/git/git/blob/ab5d01a/git-add--interactive.perl#L798-L805 +sub parse_hunk_header { + my ($line) = @_; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = $line =~ /^\@\@+(?: -(\d+)(?:,(\d+))?)+ \+(\d+)(?:,(\d+))? \@\@+/; + $o_cnt = 1 unless defined $o_cnt; + $n_cnt = 1 unless defined $n_cnt; + return ($o_ofs, $o_cnt, $n_ofs, $n_cnt); +} + +# Mark the first char of an empty line +sub mark_empty_line { + my $line = shift(); + + my $reset_color = "\e\\[0?m"; + my $reset_escape = "\e\[m"; + my $invert_color = "\e\[7m"; + my $add_color = $DiffHighlight::NEW_HIGHLIGHT[1]; + my $del_color = $DiffHighlight::OLD_HIGHLIGHT[1]; + + # This captures lines that do not have any ANSI in them (raw vanilla diff) + if ($line eq "+\n") { + $line = $invert_color . $add_color . " " . color('reset') . "\n"; + # This captures lines that do not have any ANSI in them (raw vanilla diff) + } elsif ($line eq "-\n") { + $line = $invert_color . $del_color . " " . color('reset') . "\n"; + # This handles everything else + } else { + $line =~ s/^($ansi_color_regex)[+-]$reset_color\s*$/$invert_color$1 $reset_escape\n/; + } + + return $line; +} + +# String to boolean +sub boolean { + my $str = shift(); + $str = trim($str); + + if ($str eq "" || $str =~ /^(no|false|0)$/i) { + return 0; + } else { + return 1; + } +} + +# Get the git config +sub git_config_raw { + my $cmd = "git config --list"; + my @out = `$cmd`; + + return \@out; +} + +# Memoize fetching a textual item from the git config +sub git_config { + my $search_key = lc($_[0] || ""); + my $default_value = lc($_[1] || ""); + + state $raw = {}; + if (%$raw && $search_key) { + return $raw->{$search_key} || $default_value; + } + + if ($args->{debug}) { + print "Parsing git config\n"; + } + + my $out = git_config_raw(); + + foreach my $line (@$out) { + if ($line =~ /=/) { + my ($key,$value) = split("=",$line,2); + $value =~ s/\s+$//; + $raw->{$key} = $value; + } + } + + # If we're given a search key return that, else return the hash + if ($search_key) { + return $raw->{$search_key} || $default_value; + } else { + return $raw; + } +} + +# Fetch a boolean item from the git config +sub git_config_boolean { + my $search_key = lc($_[0] || ""); + my $default_value = lc($_[1] || 0); # Default to false + + # If we're in a unit test, use the default (don't read the users config) + if (in_unit_test()) { + return boolean($default_value); + } + + my $result = git_config($search_key,$default_value); + my $ret = boolean($result); + + return $ret; +} + +# Check if we're inside of BATS +sub in_unit_test { + if ($ENV{BATS_CWD}) { + return 1; + } else { + return 0; + } +} + +sub get_less_charset { + my @less_char_vars = ("LESSCHARSET", "LESSCHARDEF", "LC_ALL", "LC_CTYPE", "LANG"); + foreach my $key (@less_char_vars) { + my $val = $ENV{$key}; + + if (defined $val) { + return ($key, $val); + } + } + + return (); +} + +sub should_print_unicode { + if (-t STDOUT) { + # Always print unicode chars if we're not piping stuff, e.g. to less(1) + return 1; + } + + # Otherwise, assume we're piping to less(1) + my ($less_env_var, $less_charset) = get_less_charset(); + if ($less_charset && $less_charset =~ /utf-?8/i) { + return 1; + } + + return 0; +} + +# Try and be smart about what line the diff hunk starts on +sub start_line_calc { + my ($line_num,$diff_context) = @_; + my $ret; + + if ($line_num == 0 && $diff_context == 0) { + return 1; + } + + # Git defaults to three lines of context + my $default_context_lines = 3; + # Three lines on either side, and the line itself = 7 + my $expected_context = ($default_context_lines * 2 + 1); + + # The first three lines + if ($line_num == 1 && $diff_context < $expected_context) { + $ret = $diff_context - $default_context_lines; + } else { + $ret = $line_num + $default_context_lines; + } + + if ($ret < 1) { + $ret = 1; + } + + return $ret; +} + +# Remove + or - at the beginning of the lines +sub strip_leading_indicators { + my $line = shift(); # Array passed in by reference + my $columns_to_remove = shift(); # Don't remove any lines by default + + if ($columns_to_remove == 0) { + return $line; # Nothing to do + } + + $line =~ s/^(${ansi_color_regex})([ +-]){${columns_to_remove}}/$1/; + + if ($manually_color_lines) { + if (defined($5) && $5 eq "+") { + my $add_line_color = get_config_color("add_line"); + $line = $add_line_color . insert_reset_at_line_end($line); + } elsif (defined($5) && $5 eq "-") { + my $remove_line_color = get_config_color("remove_line"); + $line = $remove_line_color . insert_reset_at_line_end($line); + } + } + + return $line; +} + +# Insert the color reset code at end of line, but before any newlines +sub insert_reset_at_line_end { + my $line = shift(); + $line =~ s/^(.*)([\n\r]+)?$/${1}${reset_color}${2}/; + return $line; +} + +# Count the number of a given char in a string +# https://www.perturb.org/display/1010_Perl_Count_occurrences_of_substring.html +sub char_count { + my ($needle, $haystack) = @_; + + my $count = () = ($haystack =~ /$needle/g); + + return $count; +} + +# Remove all ANSI codes from a string +sub bleach_text { + my $str = shift(); + $str =~ s/\e\[\d*(;\d+)*m//mg; + + return $str; +} + +# Remove all trailing and leading spaces +sub trim { + my $s = shift(); + if (!$s) { return ""; } + + $s =~ s/^\s*//u; + $s =~ s/\s*$//u; + + return $s; +} + +# Print a line of em-dash or line-drawing chars the full width of the screen +sub horizontal_rule { + my $color = $_[0] || ""; + my $width = get_terminal_width(); + + # em-dash http://www.fileformat.info/info/unicode/char/2014/index.htm + #my $dash = "\x{2014}"; + # BOX DRAWINGS LIGHT HORIZONTAL http://www.fileformat.info/info/unicode/char/2500/index.htm + my $dash; + if ($use_unicode_dash_for_ruler && should_print_unicode()) { + #$dash = Encode::encode('UTF-8', "\x{2500}"); + $dash = "\xE2\x94\x80"; + } else { + $dash = "-"; + } + + # Draw the line + my $ret = $color . ($dash x $width) . "$reset_color\n"; + + return $ret; +} + +sub file_change_string { + my $file_1 = shift(); + my $file_2 = shift(); + + # If they're the same it's a modify + if ($file_1 eq $file_2) { + return "modified: $file_1"; + # If the first is /dev/null it's a new file + } elsif ($file_1 eq "/dev/null") { + my $add_color = $DiffHighlight::NEW_HIGHLIGHT[1]; + return "added: $add_color$file_2$reset_color"; + # If the second is /dev/null it's a deletion + } elsif ($file_2 eq "/dev/null") { + my $del_color = $DiffHighlight::OLD_HIGHLIGHT[1]; + return "deleted: $del_color$file_1$reset_color"; + # If the files aren't the same it's a rename + } elsif ($file_1 ne $file_2) { + my ($old, $new) = DiffHighlight::highlight_pair($file_1,$file_2,{only_diff => 1}); + # highlight_pair already includes reset_color, but adds newline characters that need to be trimmed off + $old = trim($old); + $new = trim($new); + return "renamed: $old$meta_color to $new" + # Something we haven't thought of yet + } else { + return "$file_1 -> $file_2"; + } +} + +# Check to see if STDIN is connected to an interactive terminal +sub has_stdin { + my $i = -t STDIN; + my $ret = int(!$i); + + return $ret; +} + +# We use this instead of Getopt::Long because it's faster and we're not parsing any +# crazy arguments +# Borrowed from: https://www.perturb.org/display/1153_Perl_Quick_extract_variables_from_ARGV.html +sub argv { + my $ret = {}; + + for (my $i = 0; $i < scalar(@ARGV); $i++) { + + # If the item starts with "-" it's a key + if ((my ($key) = $ARGV[$i] =~ /^--?([a-zA-Z_-]*\w)$/) && ($ARGV[$i] !~ /^-\w\w/)) { + # If the next item does not start with "--" it's the value for this item + if (defined($ARGV[$i + 1]) && ($ARGV[$i + 1] !~ /^--?\D/)) { + $ret->{$key} = $ARGV[$i + 1]; + # Bareword like --verbose with no options + } else { + $ret->{$key}++; + } + } + } + + # We're looking for a certain item + if ($_[0]) { return $ret->{$_[0]}; } + + return $ret; +} + +# Output the command line usage for d-s-f +sub usage { + my $out = color("white_bold") . version() . color("reset") . "\n"; + + $out .= "Usage: + +git diff --color | diff-so-fancy # Use d-s-f on one diff +cat diff.txt | diff-so-fancy # Use d-s-f on a diff/patch file +diff -u one.txt two.txt | diff-so-fancy # Use d-s-f on unified diff output + +diff-so-fancy --colors # View the commands to set the recommended colors +diff-so-fancy --set-defaults # Configure git-diff to use diff-so-fancy and suggested colors +diff-so-fancy --patch # Use diff-so-fancy in patch mode (interoperable with `git add --patch`) + +# Configure git to use d-s-f for *all* diff operations +git config --global core.pager \"diff-so-fancy | less --tabs=4 -RFX\" + +# Configure git to use d-s-f for `git add --patch` +git config --global interactive.diffFilter \"diff-so-fancy --patch\"\n"; + + return $out; +} + +sub get_default_colors { + my $out = "# Recommended default colors for diff-so-fancy\n"; + $out .= "# --------------------------------------------\n"; + $out .= 'git config --global color.ui true + +git config --global color.diff-highlight.oldNormal "red bold" +git config --global color.diff-highlight.oldHighlight "red bold 52" +git config --global color.diff-highlight.newNormal "green bold" +git config --global color.diff-highlight.newHighlight "green bold 22" + +git config --global color.diff.meta "yellow" +git config --global color.diff.frag "magenta bold" +git config --global color.diff.commit "yellow bold" +git config --global color.diff.old "red bold" +git config --global color.diff.new "green bold" +git config --global color.diff.whitespace "red reverse" +'; + + return $out; +} + +# Output the current version string +sub version { + my $ret = "Diff-so-fancy: https://github.com/so-fancy/diff-so-fancy\n"; + $ret .= "Version : $VERSION\n"; + + return $ret; +} + +sub is_windows { + if ($^O eq 'MSWin32' or $^O eq 'dos' or $^O eq 'os2' or $^O eq 'cygwin' or $^O eq 'msys') { + return 1; + } else { + return 0; + } +} + +# Return value is whether this is the first time they've run d-s-f +sub check_first_run { + my $ret = 0; + + # If first-run is not set, or it's set to "true" + my $first_run = git_config_boolean('diff-so-fancy.first-run'); + # See if they're previously set SOME diff-highlight colors + my $has_dh_colors = git_config_boolean('color.diff-highlight.oldnormal') || git_config_boolean('color.diff-highlight.newnormal'); + + #$first_run = 1; $has_dh_colors = 0; + + if (!$first_run || $has_dh_colors) { + return 0; + } else { + print "This appears to be the first time you've run diff-so-fancy, please note\n"; + print "that the default git colors are not ideal. Diff-so-fancy recommends the\n"; + print "following colors.\n\n"; + + print get_default_colors(); + + # Set the first run flag to false + my $cmd = 'git config --global diff-so-fancy.first-run false'; + system($cmd); + + exit; + } + + return 1; +} + +sub set_defaults { + my $color_config = get_default_colors(); + my $git_config = 'git config --global core.pager "diff-so-fancy | less --tabs=4 -RFX"'; + my $first_cmd = 'git config --global diff-so-fancy.first-run false'; + + my @cmds = split(/\n/,$color_config); + push(@cmds,$git_config); + push(@cmds,$first_cmd); + + # Remove all comments from the commands + foreach my $x (@cmds) { + $x =~ s/#.*//g; + } + + # Remove any empty commands + @cmds = grep($_,@cmds); + + foreach my $cmd (@cmds) { + system($cmd); + my $exit = ($? >> 8); + + if ($exit != 0) { + die("Error running: '$cmd' (error #18941)\n"); + } + } + + return 1; +} + +# Borrowed from: https://www.perturb.org/display/1167_Perl_ANSI_colors.html +# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue' +sub color { + my $str = shift(); + + # No string sent in, so we just reset + if (!length($str) || $str eq 'reset') { return "\e[0m"; } + + # Some predefined colors + my %color_map = qw(red 160 blue 21 green 34 yellow 226 orange 214 purple 93 white 15 black 0); + $str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg; + + # Get foreground/background and any commands + my ($fc,$cmd) = $str =~ /(\d+)?_?(\w+)?/g; + my ($bc) = $str =~ /on_?(\d+)/g; + + # Some predefined commands + my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7); + my $cmd_num = $cmd_map{$cmd // 0}; + + my $ret = ''; + if ($cmd_num) { $ret .= "\e[${cmd_num}m"; } + if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; } + if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; } + + return $ret; +} + +# Get colors used for various output sections (memoized) +{ + my $static_config; + + sub get_config_color { + my $str = shift(); + + my $ret = ""; + if ($static_config->{$str}) { + return $static_config->{$str}; + } + + #print color(15) . "Shelling out for color: '$str'\n" . color('reset'); + + if ($str eq "meta") { + # Default ANSI yellow + $ret = git_ansi_color(git_config('color.diff.meta')) || color(11); + } elsif ($str eq "reset") { + $ret = color("reset"); + } elsif ($str eq "add_line") { + # Default ANSI green + $ret = git_ansi_color(git_config('color.diff.new')) || color("2_bold"); + } elsif ($str eq "remove_line") { + # Default ANSI red + $ret = git_ansi_color(git_config('color.diff.old')) || color("1_bold"); + } elsif ($str eq "fragment") { + $ret = git_ansi_color(git_config('color.diff.frag')) || color("13_bold"); + } elsif ($str eq "last_function") { + $ret = git_ansi_color(git_config('color.diff.func')) || color("146_bold"); + } + + # Cache (memoize) the entry for later + $static_config->{$str} = $ret; + + return $ret; + } +} + +# https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_colors_in_git +sub git_ansi_color { + my $str = shift(); + my @parts = split(' ', $str); + + if (!@parts) { + return ''; + } + my $colors = { + 'black' => 0, + 'red' => 1, + 'green' => 2, + 'yellow' => 3, + 'blue' => 4, + 'magenta' => 5, + 'cyan' => 6, + 'white' => 7, + }; + + my @ansi_part = (); + + if (grep { /bold/ } @parts) { + push(@ansi_part, "1"); + @parts = grep { !/bold/ } @parts; # Remove from array + } + + if (grep { /reverse/ } @parts) { + push(@ansi_part, "7"); + @parts = grep { !/reverse/ } @parts; # Remove from array + } + + my $fg = $parts[0] // ""; + my $bg = $parts[1] // ""; + + ############################################# + + # It's an numeric value, so it's an 8 bit color + if (is_numeric($fg)) { + if ($fg < 8) { + push(@ansi_part, $fg + 30); + } elsif ($fg < 16) { + push(@ansi_part, $fg + 82); + } else { + push(@ansi_part, "38;5;$fg"); + } + # It's a simple 16 color OG ansi + } elsif ($fg) { + my $bright = $fg =~ s/bright//; + my $color_num = $colors->{$fg} + 30; + + if ($bright) { $color_num += 60; } # Set bold + + push(@ansi_part, $color_num); + } + + ############################################# + + # It's an numeric value, so it's an 8 bit color + if (is_numeric($bg)) { + if ($bg < 8) { + push(@ansi_part, $bg + 40); + } elsif ($bg < 16) { + push(@ansi_part, $bg + 92); + } else { + push(@ansi_part, "48;5;$bg"); + } + # It's a simple 16 color OG ansi + } elsif ($bg) { + my $bright = $bg =~ s/bright//; + my $color_num = $colors->{$bg} + 40; + + if ($bright) { $color_num += 60; } # Set bold + + push(@ansi_part, $color_num); + } + + ############################################# + + my $ansi_str = join(";", @ansi_part); + my $ret = "\e[" . $ansi_str . "m"; + + return $ret; +} + +sub is_numeric { + my $s = shift(); + + if ($s =~ /^\d+$/) { + return 1; + } + + return 0; +} + +sub starts_with_ansi { + my $str = shift(); + + if ($str =~ /^$ansi_color_regex/) { + return 1; + } else { + return 0; + } +} + +sub get_terminal_width { + # Make width static so we only calculate it once + state $width; + + if ($width) { + return $width; + } + + # If there is a ruler width in the config we use that + if ($ruler_width) { + $width = $ruler_width; + # Otherwise we check the terminal width using tput + } else { + my $tput = `tput cols`; + + if ($tput) { + $width = int($tput); + + if (is_windows()) { + $width--; + } + } else { + print color('orange') . "Warning: `tput cols` did not return numeric input" . color('reset') . "\n"; + $width = 80; + } + } + + return $width; +} + +sub show_debug_info { + my @less = get_less_charset(); + my $git_ver = trim(`git --version`); + $git_ver =~ s/[^\d.]//g; + + print "Diff-so-fancy : v$VERSION\n"; + print "Git : v$git_ver\n"; + print "Perl : $^V\n"; + print "\n"; + + print "Terminal width : " . get_terminal_width() . "\n"; + print "Terminal \$LANG : " . ($ENV{LANG} || "") . "\n"; + print "\n"; + print "Supports Unicode: " . yes_no(should_print_unicode()) . "\n"; + print "Unicode Ruler : " . yes_no($use_unicode_dash_for_ruler) . "\n"; + print "\n"; + print "Less Charset Var: " . ($less[0] // "") . "\n"; + print "Less Charset : " . ($less[1] // "") . "\n"; + print "\n"; + print "Is Windows : " . yes_no(is_windows()) . "\n"; + print "Operating System: $^O\n"; +} + +sub yes_no { + my $val = shift(); + + if ($val) { + return "Yes"; + } else { + return "No"; + } +} + +# If there are colors set in the gitconfig use those, otherwise leave the defaults +sub init_diff_highlight_colors { + $DiffHighlight::NEW_HIGHLIGHT[0] = git_ansi_color(git_config('color.diff-highlight.newnormal')) || $DiffHighlight::NEW_HIGHLIGHT[0]; + $DiffHighlight::NEW_HIGHLIGHT[1] = git_ansi_color(git_config('color.diff-highlight.newhighlight')) || $DiffHighlight::NEW_HIGHLIGHT[1]; + + $DiffHighlight::OLD_HIGHLIGHT[0] = git_ansi_color(git_config('color.diff-highlight.oldnormal')) || $DiffHighlight::OLD_HIGHLIGHT[0]; + $DiffHighlight::OLD_HIGHLIGHT[1] = git_ansi_color(git_config('color.diff-highlight.oldhighlight')) || $DiffHighlight::OLD_HIGHLIGHT[1]; +} + +sub debug_log { + my $log_line = shift(); + my $file = "/tmp/diff-so-fancy.debug.log"; + + state $fh; + if (!$fh) { + printf("%sDebug log enabled:%s $file\n", color('orange'), color()); + open ($fh, ">", $file) or die("Cannot write to $file"); + } + + print $fh trim($log_line) . "\n"; + + return 1; +} + +# Enable k() and kd() if there is a DSF_DEBUG environment variable +BEGIN { + if ($ENV{"DSF_DEBUG"}) { + require Data::Dump::Color; + *k = sub { Data::Dump::Color::dd(@_) }; + *kd = sub { + k(@_); + + printf("Died at %2\$s line #%3\$s\n",caller()); + exit(15); + } + } else { + *k = sub {}; + *kd = sub {}; + } +} + +# vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4 diff --git a/bin/prettyping b/bin/prettyping new file mode 100755 index 0000000..8a22efd --- /dev/null +++ b/bin/prettyping @@ -0,0 +1,867 @@ +#!/usr/bin/env bash +# +# Written by Denilson Figueiredo de Sá +# MIT license +# +# Requirements: +# * bash (tested on 4.20, should work on older versions too) +# * awk (works with GNU awk, nawk, busybox awk, mawk) +# * ping (from iputils) +# +# More information: +# http://denilsonsa.github.io/prettyping/ +# https://github.com/denilsonsa/prettyping +# http://www.reddit.com/r/linux/comments/1op98a/prettypingsh_a_better_ui_for_watching_ping/ +# Third-party demonstration video: https://www.youtube.com/watch?v=ziEMY1BcikM + +# TODO: Adjust how many items in the legend are printed based on the terminal width. +# +# TODO: Detect the following kinds of message and avoid printing it repeatedly. +# From 192.168.1.11: icmp_seq=4 Destination Host Unreachable +# Request timeout for icmp_seq 378 +# +# TODO: Handle when a single message is spread over multiple lines. Specially, +# like in this case: https://bitbucket.org/denilsonsa/small_scripts/issue/5 +# +# TODO: Print the destination (also) at the bottom bar. Useful after leaving +# the script running for quite some time. +# +# TODO: Print the destination as escape codes to xterm title. +# +# TODO: Print the current time in the beginning of each line. +# +# TODO: Implement audible ping. +# +# TODO: Autodetect the width of printf numbers, so they will always line up correctly. +# +# TODO: Test the behavior of this script upon receiving out-of-order packets, like these: +# http://www.blug.linux.no/rfc1149/pinglogg.txt +# +# TODO? How will prettyping behave if it receives a duplicate response? + +print_help() { + cat << EOF +Usage: $MYNAME [prettyping parameters] + +This script is a wrapper around the system's "ping" tool. It will substitute +each ping response line by a colored character, giving a very compact overview +of the ping responses. + +prettyping parameters: + --[no]color Enable/disable color output. (default: enabled) + --[no]multicolor Enable/disable multi-color unicode output. Has no effect if + either color or unicode is disabled. (default: enabled) + --[no]unicode Enable/disable unicode characters. (default: enabled) + --[no]legend Enable/disable the latency legend. (default: enabled) + --[no]terminal Force the output designed to a terminal. (default: auto) + --last Use the last "n" pings at the statistics line. (default: 60) + --columns Override auto-detection of terminal dimensions. + --lines Override auto-detection of terminal dimensions. + --rttmin Minimum RTT represented in the unicode graph. (default: auto) + --rttmax Maximum RTT represented in the unicode graph. (default: auto) + --awkbin Override the awk interpreter. (default: awk) + --pingbin Override the ping tool. (default: ping) + -6 Shortcut for: --pingbin ping6 + +ping parameters handled by prettyping: + -a Audible ping is not implemented yet. + -f Flood mode is not allowed in prettyping. + -q Quiet output is not allowed in prettyping. + -R Record route mode is not allowed in prettyping. + -v Verbose output seems to be the default mode in ping. + +All other parameters are passed directly to ping. +EOF +} + +# Thanks to people at #bash who pointed me at +# http://bash-hackers.org/wiki/doku.php/scripting/posparams +parse_arguments() { + USE_COLOR=1 + USE_MULTICOLOR=1 + USE_UNICODE=1 + USE_LEGEND=1 + + if [ -t 1 ]; then + IS_TERMINAL=1 + else + IS_TERMINAL=0 + fi + + LAST_N=60 + OVERRIDE_COLUMNS=0 + OVERRIDE_LINES=0 + RTT_MIN=auto + RTT_MAX=auto + + PING_BIN="ping" + #PING_BIN="./mockping.awk" + PING_PARAMS=( ) + + AWK_BIN="awk" + AWK_PARAMS=( ) + + while [[ $# != 0 ]] ; do + case "$1" in + -h | -help | --help ) + print_help + exit + ;; + + # Forbidden ping parameters within prettyping: + -f ) + echo "${MYNAME}: You can't use the -f (flood) option." + exit 1 + ;; + -R ) + # -R prints extra information at each ping response. + echo "${MYNAME}: You can't use the -R (record route) option." + exit 1 + ;; + -q ) + echo "${MYNAME}: You can't use the -q (quiet) option." + exit 1 + ;; + -v ) + # -v enables verbose output. However, it seems the output with + # or without this option is the same. Anyway, prettyping will + # strip this parameter. + ;; + # Note: + # Small values for -s parameter prevents ping from being able to + # calculate RTT. + + # New parameters: + -a ) + # TODO: Implement audible ping for responses or for missing packets + ;; + + -color | --color ) USE_COLOR=1 ;; + -nocolor | --nocolor ) USE_COLOR=0 ;; + -multicolor | --multicolor ) USE_MULTICOLOR=1 ;; + -nomulticolor | --nomulticolor ) USE_MULTICOLOR=0 ;; + -unicode | --unicode ) USE_UNICODE=1 ;; + -nounicode | --nounicode ) USE_UNICODE=0 ;; + -legend | --legend ) USE_LEGEND=1 ;; + -nolegend | --nolegend ) USE_LEGEND=0 ;; + -terminal | --terminal ) IS_TERMINAL=1 ;; + -noterminal | --noterminal ) IS_TERMINAL=0 ;; + + -awkbin | --awkbin ) AWK_BIN="$2" ; shift ;; + -pingbin | --pingbin ) PING_BIN="$2" ; shift ;; + -6 ) PING_BIN="ping6" ;; + + #TODO: Check if these parameters are numbers. + -last | --last ) LAST_N="$2" ; shift ;; + -columns | --columns ) OVERRIDE_COLUMNS="$2" ; shift ;; + -lines | --lines ) OVERRIDE_LINES="$2" ; shift ;; + -rttmin | --rttmin ) RTT_MIN="$2" ; shift ;; + -rttmax | --rttmax ) RTT_MAX="$2" ; shift ;; + + * ) + PING_PARAMS+=("$1") + ;; + esac + shift + done + + if [[ "${RTT_MIN}" -gt 0 && "${RTT_MAX}" -gt 0 && "${RTT_MIN}" -ge "${RTT_MAX}" ]] ; then + echo "${MYNAME}: Invalid --rttmin and -rttmax values." + exit 1 + fi + + if [[ "${#PING_PARAMS[@]}" = 0 ]] ; then + echo "${MYNAME}: Missing parameters, use --help for instructions." + exit 1 + fi + + # Workaround for mawk: + # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=593504 + local version="$(echo | "${AWK_BIN}" -W version 2>&1)" + if [[ "${version}" == mawk* ]] ; then + AWK_PARAMS+=(-W interactive) + fi +} + +MYNAME=`basename "$0"` +parse_arguments "$@" + + +export LC_ALL=C + +# Warning! Ugly code ahead! +# The code is so ugly that the comments explaining it are +# bigger than the code itself! +# +# Suppose this: +# +# cmd_a | cmd_b & +# +# I need the PID of cmd_a. How can I get it? +# In bash, $! will give me the PID of cmd_b. +# +# So, I came up with this ugly solution: open a subshell, like this: +# +# ( +# cmd_a & +# echo "This is the PID I want $!" +# wait +# ) | cmd_b + + +# Ignore Ctrl+C here. +# If I don't do this, this shell script is killed before +# ping and gawk can finish their work. +trap '' 2 + +# Now the ugly code. +( + "${PING_BIN}" "${PING_PARAMS[@]}" & + PING_PID="$!" + + # Commented out, because it looks like this line is not needed + #trap "kill -2 $PING_PID ; exit 1" 2 # Catch Ctrl+C here + + wait +) 2>&1 | ( + if [ "${IS_TERMINAL}" = 1 ]; then + # Print a message to notify the awk script about terminal size change. + trap "echo SIGWINCH" 28 + fi + + # The trap must be in another subshell because otherwise it will interrupt + # the "wait" commmand. + while read line; do + echo -E "$line" + done +) 2>&1 | "${AWK_BIN}" "${AWK_PARAMS[@]}" ' +# Weird that awk does not come with abs(), so I need to implement it. +function abs(x) { + return ( (x < 0) ? -x : x ) +} + +# Ditto for ceiling function. +function ceil(x) { + return (x == int(x)) ? x : int(x) + 1 +} + +# Local variables MUST be declared in argument list, else they are +# seen as global. Ugly, but that is how awk works. +function get_terminal_size(SIZE, SIZEA, HAS_DETECTED, CMD) { + HAS_DETECTED = 0 + + CMD = "stty -f /dev/tty size 2> /dev/null" + + if ( (CMD | getline SIZE) == 1 ) { + split(SIZE, SIZEA, " ") + LINES = int(SIZEA[1]) + COLUMNS = int(SIZEA[2]) + HAS_DETECTED = 1 + } + close(CMD) + + if ( HAS_DETECTED == 0 ) { + CMD = "tput lines" + if ( (CMD | getline SIZE) == 1 ) { + LINES = int(SIZE) + HAS_DETECTED = 1 + } + close(CMD) + + CMD = "tput cols" + if ( (CMD | getline SIZE) == 1 ) { + COLUMNS = int(SIZE) + HAS_DETECTED = 1 + } + close(CMD) + } + + if ( int('"${OVERRIDE_COLUMNS}"') ) { COLUMNS = int('"${OVERRIDE_COLUMNS}"') } + if ( int('"${OVERRIDE_LINES}"') ) { LINES = int('"${OVERRIDE_LINES}"') } +} + +############################################################ +# Functions related to cursor handling + +# Function called whenever a non-dotted line is printed. +# +# It will move the cursor to the line next to the statistics and +# restore the default color. +function other_line_is_printed() { + if ( IS_PRINTING_DOTS ) { + if ( '"${IS_TERMINAL}"' ) { + printf( ESC_DEFAULT ESC_NEXTLINE ESC_NEXTLINE "\n" ) + } else { + printf( ESC_DEFAULT "\n" ) + print_statistics_bar() + } + } + IS_PRINTING_DOTS = 0 + CURR_COL = 0 +} + +# Function called whenever a non-dotted line is repeated. +function other_line_is_repeated() { + if ( other_line_times < 2 ) { + return + } + if ( '"${IS_TERMINAL}"' ) { + printf( ESC_DEFAULT ESC_ERASELINE "\r" ) + } + printf( "Last message repeated %d times.", other_line_times ) + if ( ! '"${IS_TERMINAL}"' ) { + printf( "\n" ) + } +} + +# Function called whenever the repeating line has changed. +function other_line_finished_repeating() { + if ( other_line_times >= 2 ) { + if ( '"${IS_TERMINAL}"' ) { + printf( "\n" ) + } else { + other_line_is_repeated() + } + } + other_line = "" + other_line_times = 0 +} + +# Prints the newlines required for the live statistics. +# +# I need to print some newlines and then return the cursor back to its position +# to make sure the terminal will scroll. +# +# If the output is not a terminal, break lines on every LAST_N dots. +function print_newlines_if_needed() { + if ( '"${IS_TERMINAL}"' ) { + # COLUMNS-1 because I want to avoid bugs with the cursor at the last column + if ( CURR_COL >= COLUMNS-1 ) { + CURR_COL = 0 + } + if ( CURR_COL == 0 ) { + if ( IS_PRINTING_DOTS ) { + printf( "\n" ) + } + #printf( "\n" "\n" ESC_PREVLINE ESC_PREVLINE ESC_ERASELINE ) + printf( ESC_DEFAULT "\n" "\n" ESC_CURSORUP ESC_CURSORUP ESC_ERASELINE ) + } + } else { + if ( CURR_COL >= LAST_N ) { + CURR_COL = 0 + printf( ESC_DEFAULT "\n" ) + print_statistics_bar() + } + } + IS_PRINTING_DOTS = 1 +} + +############################################################ +# Functions related to the data structure of "Last N" statistics. + +# Clears the data structure. +function clear(d) { + d["index"] = 0 # The next position to store a value + d["size"] = 0 # The array size, goes up to LAST_N +} + +# This function stores the value to the passed data structure. +# The data structure holds at most LAST_N values. When it is full, +# a new value overwrite the oldest one. +function store(d, value) { + d[d["index"]] = value + d["index"]++ + if ( d["index"] >= d["size"] ) { + if ( d["size"] < LAST_N ) { + d["size"]++ + } else { + d["index"] = 0 + } + } +} + +############################################################ +# Functions related to processing the received response + +function process_rtt(rtt) { + # Overall statistics + last_rtt = rtt + total_rtt += rtt + if ( last_seq == 0 ) { + min_rtt = max_rtt = rtt + } else { + if ( rtt < min_rtt ) min_rtt = rtt + if ( rtt > max_rtt ) max_rtt = rtt + } + + # "Last N" statistics + store(lastn_rtt,rtt) +} + +function lost_a_packet() { + print_newlines_if_needed() + print_missing_response() + + last_seq++ + lost++ + store(lastn_lost, 1) +} + +############################################################ +# Functions related to printing the fancy ping response + +# block_index, n, w are just local variables. +function print_response_legend(i, n, w) { + if ( ! '"${USE_LEGEND}"' ) { + return + } + if ( BLOCK_LEN > 1 ) { + # w counts the cursor position in the current line. Because of the + # escape codes, I need to jump through some hoops in order to count the + # position correctly. + w = 0 + n = "0 " + w += length(n) + 1 + + printf( n BLOCK[0] ESC_DEFAULT ) + + for ( i=1 ; i= COLUMNS ) { + printf( "\n" ) + w = length(n) + 1 + } else { + printf( " " ) + w += 1 + } + + printf( n BLOCK[i] ESC_DEFAULT ) + } + printf( " ∞\n" ) + } + + # Useful code for debugging. + #for ( i=0 ; i<=BLOCK_RTT_MAX ; i++ ) { + # print_received_response(i) + # printf( ESC_DEFAULT "%4d\n", i ) + #} +} + +# block_index is just a local variable. +function print_received_response(rtt, block_index) { + if ( rtt < BLOCK_RTT_MIN ) { + block_index = 0 + } else if ( rtt >= BLOCK_RTT_MAX ) { + block_index = BLOCK_LEN - 1 + } else { + block_index = 1 + int((rtt - BLOCK_RTT_MIN) * (BLOCK_LEN - 2) / BLOCK_RTT_RANGE) + } + printf( BLOCK[block_index] ) + CURR_COL++ +} + +function print_missing_response(rtt) { + printf( ESC_RED "!" ) + CURR_COL++ +} + +############################################################ +# Functions related to printing statistics + +# All arguments are just local variables. +function print_overall(percentage_lost, avg_rtt) { + # Handling division by zero. + # Note that mawk does not consider division by zero an error, while all + # other awk implementations abort in such case. + # https://stackoverflow.com/questions/14581966/why-does-awk-produce-different-results-for-division-by-zero + + avg_rtt = ( received > 0 ) ? (total_rtt/received) : 0 + percentage_lost = ( lost+received > 0 ) ? (lost*100/(lost+received)) : 0 + + if ( '"${IS_TERMINAL}"' ) { + printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms; last: " ESC_BOLD "%4.0f" ESC_DEFAULT "ms", + lost, + lost+received, + percentage_lost, + min_rtt, + avg_rtt, + max_rtt, + last_rtt ) + } else { + printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms", + lost, + lost+received, + percentage_lost, + min_rtt, + avg_rtt, + max_rtt ) + } +} + +# All arguments are just local variables. +function print_last_n(i, percentage_lost, sum, min, avg, max, diffs) { + # Calculate and print the lost packets statistics + sum = 0 + for ( i=0 ; i 0) ? (sum*100/lastn_lost["size"]) : 0 + printf( "%2d/%3d (%2d%%) lost; ", + sum, + lastn_lost["size"], + percentage_lost ) + + # Calculate the min/avg/max rtt times + sum = diffs = 0 + min = max = lastn_rtt[0] + for ( i=0 ; i max ) max = lastn_rtt[i] + } + avg = (lastn_rtt["size"]) ? (sum/lastn_rtt["size"]) : 0 + + # Calculate mdev (mean absolute deviation) + for ( i=0 ; i 0 ) { + diffs /= lastn_rtt["size"] + } + + # Print the rtt statistics + printf( "%4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0f/%4.0fms (last %d)", + min, + avg, + max, + diffs, + lastn_rtt["size"] ) +} + +function print_statistics_bar() { + if ( '"${IS_TERMINAL}"' ) { + printf( ESC_SAVEPOS ESC_DEFAULT ) + + printf( ESC_NEXTLINE ESC_ERASELINE ) + print_overall() + printf( ESC_NEXTLINE ESC_ERASELINE ) + print_last_n() + + printf( ESC_UNSAVEPOS ) + } else { + print_overall() + printf( "\n" ) + print_last_n() + printf( "\n" ) + } +} + +function print_statistics_bar_if_terminal() { + if ( '"${IS_TERMINAL}"' ) { + print_statistics_bar() + } +} + +############################################################ +# Initializations +BEGIN { + # Easy way to get each value from ping output + FS = "=" + + ############################################################ + # General internal variables + + # This is needed to keep track of lost packets + last_seq = 0 + + # The previously printed non-ping-response line + other_line = "" + other_line_times = 0 + + # Variables to keep the screen clean + IS_PRINTING_DOTS = 0 + CURR_COL = 0 + + ############################################################ + # Variables related to "overall" statistics + received = 0 + lost = 0 + total_rtt = 0 + min_rtt = 0 + max_rtt = 0 + last_rtt = 0 + + ############################################################ + # Variables related to "last N" statistics + LAST_N = int('"${LAST_N}"') + + # Data structures for the "last N" statistics + clear(lastn_lost) + clear(lastn_rtt) + + ############################################################ + # Terminal height and width + + # These are sane defaults, in case we cannot query the actual terminal size + LINES = 24 + COLUMNS = 80 + + # Auto-detecting the terminal size + get_terminal_size() + if ( '"${IS_TERMINAL}"' && COLUMNS <= 50 ) { + print "Warning: terminal width is too small." + } + + ############################################################ + # ANSI escape codes + + # Color escape codes. + # Fortunately, awk defaults any unassigned variable to an empty string. + if ( '"${USE_COLOR}"' ) { + ESC_DEFAULT = "\033[0m" + ESC_BOLD = "\033[1m" + #ESC_BLACK = "\033[0;30m" + #ESC_GRAY = "\033[1;30m" + ESC_RED = "\033[0;31m" + ESC_GREEN = "\033[0;32m" + ESC_YELLOW = "\033[0;33m" + ESC_BLUE = "\033[0;34m" + ESC_MAGENTA = "\033[0;35m" + ESC_CYAN = "\033[0;36m" + ESC_WHITE = "\033[0;37m" + ESC_YELLOW_ON_GREEN = "\033[42;33m" + ESC_RED_ON_YELLOW = "\033[43;31m" + } + # Other escape codes, see: + # http://en.wikipedia.org/wiki/ANSI_escape_code + # http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + ESC_NEXTLINE = "\n" + ESC_CURSORUP = "\033[A" + ESC_CURSORDOWN = "\033[B" + ESC_SCROLLUP = "\033[S" + ESC_SCROLLDOWN = "\033[T" + ESC_ERASELINEEND = "\033[0K" + ESC_ERASELINE = "\033[2K" + ESC_SAVEPOS = "\0337" + ESC_UNSAVEPOS = "\0338" + + # I am avoiding these escapes as they are not listed in: + # http://vt100.net/docs/vt100-ug/chapter3.html + #ESC_PREVLINE = "\033[F" + #ESC_SAVEPOS = "\033[s" + #ESC_UNSAVEPOS = "\033[u" + + # I am avoiding this to improve compatibility with (older versions of) tmux + #ESC_NEXTLINE = "\033[E" + + ############################################################ + # Unicode characters (based on https://github.com/holman/spark ) + if ( '"${USE_UNICODE}"' ) { + BLOCK[ 0] = ESC_GREEN "▁" + BLOCK[ 1] = ESC_GREEN "▂" + BLOCK[ 2] = ESC_GREEN "▃" + BLOCK[ 3] = ESC_GREEN "▄" + BLOCK[ 4] = ESC_GREEN "▅" + BLOCK[ 5] = ESC_GREEN "▆" + BLOCK[ 6] = ESC_GREEN "▇" + BLOCK[ 7] = ESC_GREEN "█" + BLOCK[ 8] = ESC_YELLOW_ON_GREEN "▁" + BLOCK[ 9] = ESC_YELLOW_ON_GREEN "▂" + BLOCK[10] = ESC_YELLOW_ON_GREEN "▃" + BLOCK[11] = ESC_YELLOW_ON_GREEN "▄" + BLOCK[12] = ESC_YELLOW_ON_GREEN "▅" + BLOCK[13] = ESC_YELLOW_ON_GREEN "▆" + BLOCK[14] = ESC_YELLOW_ON_GREEN "▇" + BLOCK[15] = ESC_YELLOW_ON_GREEN "█" + BLOCK[16] = ESC_RED_ON_YELLOW "▁" + BLOCK[17] = ESC_RED_ON_YELLOW "▂" + BLOCK[18] = ESC_RED_ON_YELLOW "▃" + BLOCK[19] = ESC_RED_ON_YELLOW "▄" + BLOCK[20] = ESC_RED_ON_YELLOW "▅" + BLOCK[21] = ESC_RED_ON_YELLOW "▆" + BLOCK[22] = ESC_RED_ON_YELLOW "▇" + BLOCK[23] = ESC_RED_ON_YELLOW "█" + if ( '"${USE_MULTICOLOR}"' && '"${USE_COLOR}"' ) { + # Multi-color version: + BLOCK_LEN = 24 + BLOCK_RTT_MIN = 10 + BLOCK_RTT_MAX = 230 + } else { + # Simple version: + BLOCK_LEN = 8 + BLOCK_RTT_MIN = 25 + BLOCK_RTT_MAX = 175 + } + } else { + BLOCK[ 0] = ESC_GREEN "_" + BLOCK[ 1] = ESC_GREEN "." + BLOCK[ 2] = ESC_GREEN "o" + BLOCK[ 3] = ESC_GREEN "O" + BLOCK[ 4] = ESC_YELLOW "_" + BLOCK[ 5] = ESC_YELLOW "." + BLOCK[ 6] = ESC_YELLOW "o" + BLOCK[ 7] = ESC_YELLOW "O" + BLOCK[ 8] = ESC_RED "_" + BLOCK[ 9] = ESC_RED "." + BLOCK[10] = ESC_RED "o" + BLOCK[11] = ESC_RED "O" + if ( '"${USE_MULTICOLOR}"' && '"${USE_COLOR}"' ) { + # Multi-color version: + BLOCK_LEN = 12 + BLOCK_RTT_MIN = 20 + BLOCK_RTT_MAX = 220 + } else { + # Simple version: + BLOCK_LEN = 4 + BLOCK_RTT_MIN = 75 + BLOCK_RTT_MAX = 225 + } + } + + if ( int('"${RTT_MIN}"') > 0 && int('"${RTT_MAX}"') > 0 ) { + BLOCK_RTT_MIN = int('"${RTT_MIN}"') + BLOCK_RTT_MAX = int('"${RTT_MAX}"') + } else if ( int('"${RTT_MIN}"') > 0 ) { + BLOCK_RTT_MIN = int('"${RTT_MIN}"') + BLOCK_RTT_MAX = BLOCK_RTT_MIN * (BLOCK_LEN - 1) + } else if ( int('"${RTT_MAX}"') > 0 ) { + BLOCK_RTT_MAX = int('"${RTT_MAX}"') + BLOCK_RTT_MIN = int(BLOCK_RTT_MAX / (BLOCK_LEN - 1)) + } + + BLOCK_RTT_RANGE = BLOCK_RTT_MAX - BLOCK_RTT_MIN + print_response_legend() +} + +############################################################ +# Main loop +{ + if ( $0 ~ /^[0-9]+ bytes from .*: icmp_[rs]eq=[0-9]+ ttl=[0-9]+ time=[0-9.]+ *ms/ ) { + # Sample line from ping: + # 64 bytes from 8.8.8.8: icmp_seq=1 ttl=49 time=184 ms + if ( other_line_times >= 2 ) { + other_line_finished_repeating() + } + + # $1 = useless prefix string + # $2 = icmp_seq + # $3 = ttl + # $4 = time + + # This must be called before incrementing the last_seq variable! + rtt = int($4) + process_rtt(rtt) + + seq = int($2) + + while ( last_seq < seq - 1 ) { + lost_a_packet() + } + + # Received a packet + print_newlines_if_needed() + print_received_response(rtt) + + # In case of receiving multiple responses with the same seq number, it + # is better to use "last_seq = seq" than to increment last_seq. + last_seq = seq + + received++ + store(lastn_lost, 0) + + print_statistics_bar_if_terminal() + } else if ( $0 ~ /^.*onnected to.*, seq=[0-9]+ time=[0-9.]+ *ms/ ) { + # Sample line from httping: + # connected to 200.149.119.168:80 (273 bytes), seq=0 time=129.86 ms + if ( other_line_times >= 2 ) { + other_line_finished_repeating() + } + + seq = $0 + sub(/.* seq=/, "", seq) + seq = int(seq) + + rtt = $0 + sub(/.* time=/, "", rtt) + rtt = int(rtt) + + process_rtt(rtt) + + while ( last_seq < seq - 1 ) { + lost_a_packet() + } + + # Received a packet + print_newlines_if_needed() + print_received_response(rtt) + + # In case of receiving multiple responses with the same seq number, it + # is better to use "last_seq = seq" than to increment last_seq. + last_seq = seq + + received++ + store(lastn_lost, 0) + + print_statistics_bar_if_terminal() + } else if ( $0 == "" ) { + # Do nothing on blank lines. + } else if ( $0 == "error shutting down ssl" ) { + # Common error message when using httping, ignore it. + } else if ( $0 ~ /^Request timeout for icmp_seq [0-9]+/ ) { + # Reply timeout is printed on Mac OS X. + + if ( other_line_times >= 2 ) { + other_line_finished_repeating() + } + + lost_a_packet() + + # Making sure the last_seq number is correct. + gsub(/.* icmp_seq /, "") + seq = int($0) + last_seq = seq + + print_newlines_if_needed() + print_statistics_bar_if_terminal() + } else if ( $0 ~ /^SIGWINCH$/ ) { + get_terminal_size() + + if ( IS_PRINTING_DOTS ) { + if ( CURR_COL >= COLUMNS-1 ) { + # Not enough space anyway. + } else { + # Making up room in case the number of lines has changed. + printf( ESC_NEXTLINE ESC_NEXTLINE ESC_CURSORUP ESC_CURSORUP ) + # Moving to the correct column and erasing the rest of the line. + printf( "\033[" (CURR_COL+1) "G" ESC_DEFAULT ESC_ERASELINEEND ) + } + + print_newlines_if_needed() + print_statistics_bar_if_terminal() + } + } else { + other_line_is_printed() + original_line = $0 + gsub(/icmp_seq[= ][0-9]+/, "") + if ( $0 == other_line ) { + other_line_times++ + if ( '"${IS_TERMINAL}"' ) { + other_line_is_repeated() + } + } else { + other_line_finished_repeating() + other_line = $0 + other_line_times = 1 + printf( "%s\n", original_line ) + } + } + + # Not needed when the output is a terminal, but does not hurt either. + fflush() +}' diff --git a/i3/config-nuc b/i3/config-nuc index feff9ca..89cf944 100644 --- a/i3/config-nuc +++ b/i3/config-nuc @@ -41,6 +41,10 @@ font pango:monospace 8 # X core fonts rendering does not support right-to-left and this being a bitmap # font, it doesn’t scale on retina/hidpi displays. +exec_always xset -dpms ; xset s 0 0 ; xset s off +#exec autocutsel -selection CLIPBOARD +#exec autocutsel -selection PRIMARY + # Use Mouse+$mod to drag floating windows to their wanted position floating_modifier $mod diff --git a/i3/i3blocks.conf b/i3/i3blocks.conf index b043d57..dc8a63b 100644 --- a/i3/i3blocks.conf +++ b/i3/i3blocks.conf @@ -28,8 +28,7 @@ command=/usr/share/i3blocks/$BLOCK_NAME separator_block_width=15 markup=none -[volume-pulseaudio] -label=🎚️ +[volume-pipewire] command=~/.config/i3/scripts/volume-pulseaudio interval=1 signal=1 diff --git a/i3/scripts/volume-pipewire b/i3/scripts/volume-pipewire new file mode 100755 index 0000000..bc7da69 --- /dev/null +++ b/i3/scripts/volume-pipewire @@ -0,0 +1,172 @@ +#!/bin/bash +# Displays the default device, volume, and mute status for i3blocks + +set -a + +AUDIO_HIGH_SYMBOL=${AUDIO_HIGH_SYMBOL:-' '} + +AUDIO_MED_THRESH=${AUDIO_MED_THRESH:-50} +AUDIO_MED_SYMBOL=${AUDIO_MED_SYMBOL:-' '} + +AUDIO_LOW_THRESH=${AUDIO_LOW_THRESH:-0} +AUDIO_LOW_SYMBOL=${AUDIO_LOW_SYMBOL:-' '} + +AUDIO_MUTED_SYMBOL=${AUDIO_MUTED_SYMBOL:-' '} + +AUDIO_DELTA=${AUDIO_DELTA:-5} + +DEFAULT_COLOR=${DEFAULT_COLOR:-"#ffffff"} +MUTED_COLOR=${MUTED_COLOR:-"#a0a0a0"} + +LONG_FORMAT=${LONG_FORMAT:-'${SYMB} ${VOL}% [${INDEX}:${NAME}]'} +SHORT_FORMAT=${SHORT_FORMAT:-'${SYMB} ${VOL}% [${INDEX}]'} +USE_ALSA_NAME=${USE_ALSA_NAME:-0} +USE_DESCRIPTION=${USE_DESCRIPTION:-0} + +SUBSCRIBE=${SUBSCRIBE:-0} + +MIXER=${MIXER:-""} +SCONTROL=${SCONTROL:-""} + +while getopts F:Sf:adH:M:L:X:T:t:C:c:i:m:s:h opt; do + case "$opt" in + S) SUBSCRIBE=1 ;; + F) LONG_FORMAT="$OPTARG" ;; + f) SHORT_FORMAT="$OPTARG" ;; + a) USE_ALSA_NAME=1 ;; + d) USE_DESCRIPTION=1 ;; + H) AUDIO_HIGH_SYMBOL="$OPTARG" ;; + M) AUDIO_MED_SYMBOL="$OPTARG" ;; + L) AUDIO_LOW_SYMBOL="$OPTARG" ;; + X) AUDIO_MUTED_SYMBOL="$OPTARG" ;; + T) AUDIO_MED_THRESH="$OPTARG" ;; + t) AUDIO_LOW_THRESH="$OPTARG" ;; + C) DEFAULT_COLOR="$OPTARG" ;; + c) MUTED_COLOR="$OPTARG" ;; + i) AUDIO_INTERVAL="$OPTARG" ;; + m) MIXER="$OPTARG" ;; + s) SCONTROL="$OPTARG" ;; + h) printf \ +"Usage: volume-pulseaudio [-S] [-F format] [-f format] [-p] [-a|-d] [-H symb] [-M symb] + [-L symb] [-X symb] [-T thresh] [-t thresh] [-C color] [-c color] [-i inter] + [-m mixer] [-s scontrol] [-h] +Options: +-F, -f\tOutput format (-F long format, -f short format) to use, with exposed variables: +\${SYMB}, \${VOL}, \${INDEX}, \${NAME} +-S\tSubscribe to volume events (requires persistent block, always uses long format) +-a\tUse ALSA name if possible +-d\tUse device description instead of name if possible +-H\tSymbol to use when audio level is high. Default: '$AUDIO_HIGH_SYMBOL' +-M\tSymbol to use when audio level is medium. Default: '$AUDIO_MED_SYMBOL' +-L\tSymbol to use when audio level is low. Default: '$AUDIO_LOW_SYMBOL' +-X\tSymbol to use when audio is muted. Default: '$AUDIO_MUTED_SYMBOL' +-T\tThreshold for medium audio level. Default: $AUDIO_MED_THRESH +-t\tThreshold for low audio level. Default: $AUDIO_LOW_THRESH +-C\tColor for non-muted audio. Default: $DEFAULT_COLOR +-c\tColor for muted audio. Default: $MUTED_COLOR +-i\tInterval size of volume increase/decrease. Default: $AUDIO_DELTA +-m\tUse the given mixer. +-s\tUse the given scontrol. +-h\tShow this help text +" && exit 0;; + esac +done + +if [[ -z "$MIXER" ]] ; then + MIXER="default" + if amixer -D pulse info >/dev/null 2>&1 ; then + MIXER="pulse" + fi +fi + +if [[ -z "$SCONTROL" ]] ; then + SCONTROL=$(amixer -D "$MIXER" scontrols | sed -n "s/Simple mixer control '\([^']*\)',0/\1/p" | head -n1) +fi + +CAPABILITY=$(amixer -D $MIXER get $SCONTROL | sed -n "s/ Capabilities:.*cvolume.*/Capture/p") + + +function move_sinks_to_new_default { + DEFAULT_SINK=$1 + pactl list sink-inputs | grep 'Sink Input #' | grep -o '[0-9]\+' | while read SINK + do + pactl move-sink-input $SINK $DEFAULT_SINK + done +} + +function set_default_playback_device_next { + inc=${1:-1} + num_devices=$(pactl list sinks | grep -c Name:) + sink_arr=($(pactl list sinks | grep Name: | sed -r 's/\s+Name: (.+)/\1/')) + default_sink=$(pactl get-default-sink) + default_sink_index=$(for i in "${!sink_arr[@]}"; do if [[ "$default_sink" = "${sink_arr[$i]}" ]]; then echo "$i"; fi done) + default_sink_index=$(( ($default_sink_index + $num_devices + $inc) % $num_devices )) + default_sink=${sink_arr[$default_sink_index]} + pactl set-default-sink $default_sink + move_sinks_to_new_default $default_sink +} + +case "$BLOCK_BUTTON" in + 1) set_default_playback_device_next ;; + 2) amixer -q -D $MIXER sset $SCONTROL $CAPABILITY toggle ;; + 3) set_default_playback_device_next -1 ;; + 4) amixer -q -D $MIXER sset $SCONTROL $CAPABILITY $AUDIO_DELTA%+ ;; + 5) amixer -q -D $MIXER sset $SCONTROL $CAPABILITY $AUDIO_DELTA%- ;; +esac + +function print_format { + echo "$1" | envsubst '${SYMB}${VOL}${INDEX}${NAME}' +} + +function print_block { + ACTIVE=$(pactl list sinks | grep "State\: RUNNING" -B4 -A55 | grep "Name:\|Volume: \(front-left\|mono\)\|Mute:\|api.alsa.pcm.card = \|node.nick = ") + for Name in NAME MUTED VOL INDEX NICK; do + read $Name + done < <(echo "$ACTIVE") + INDEX=$(echo "$INDEX" | grep -o '".*"' | sed 's/"//g') + VOL=$(echo "$VOL" | grep -o "[0-9]*%" | head -1 ) + VOL="${VOL%?}" + NAME=$(echo "$NICK" | grep -o '".*"' | sed 's/"//g') + + if [[ $USE_ALSA_NAME == 1 ]] ; then + ALSA_NAME=$(pactl list sinks |\ +awk '/^\s*\*/{f=1}/^\s*index:/{f=0}f' |\ +grep "alsa.name\|alsa.mixer_name" |\ +head -n1 |\ +sed 's/.*= "\(.*\)".*/\1/') + NAME=${ALSA_NAME:-$NAME} + elif [[ $USE_DESCRIPTION == 1 ]] ; then + DESCRIPTION=$(pactl list sinks | grep "State\: RUNNING" -B4 -A55 | grep 'Description: ' | sed 's/^.*: //') + NAME=${DESCRIPTION:-$NAME} + fi + + if [[ $MUTED =~ "no" ]] ; then + SYMB=$AUDIO_HIGH_SYMBOL + [[ $VOL -le $AUDIO_MED_THRESH ]] && SYMB=$AUDIO_MED_SYMBOL + [[ $VOL -le $AUDIO_LOW_THRESH ]] && SYMB=$AUDIO_LOW_SYMBOL + COLOR=$DEFAULT_COLOR + else + SYMB=$AUDIO_MUTED_SYMBOL + COLOR=$MUTED_COLOR + fi + + if [[ $ACTIVE = "" ]] ; then + echo "Sound inactive" + COLOR='#222225' + fi + + if [[ $SUBSCRIBE == 1 ]] ; then + print_format "$LONG_FORMAT" + else + print_format "$LONG_FORMAT" + print_format "$SHORT_FORMAT" + echo "$COLOR" + fi +} + +print_block +if [[ $SUBSCRIBE == 1 ]] ; then + while read -r EVENT; do + print_block + done < <(pactl subscribe | stdbuf -oL grep change) +fi diff --git a/i3/scripts/volume-pulseaudio b/i3/scripts/volume-pulseaudio index 9dab468..1b6be3e 100755 --- a/i3/scripts/volume-pulseaudio +++ b/i3/scripts/volume-pulseaudio @@ -59,9 +59,9 @@ done function set_default_playback_device_next { inc=${1:-1} - num_devices=$(pacmd list-sinks | grep -c index:) - sink_arr=($(pacmd list-sinks | grep index: | grep -o '[0-9]\+')) - default_sink_index=$(( $(pacmd list-sinks | grep index: | grep -no '*' | grep -o '^[0-9]\+') - 1 )) + num_devices=$(pactl list sinks | grep -c index:) + sink_arr=($(pactl list sinks | grep index: | grep -o '[0-9]\+')) + default_sink_index=$(( $(patl list sinks | grep index: | grep -no '*' | grep -o '^[0-9]\+') - 1 )) default_sink_index=$(( ($default_sink_index + $num_devices + $inc) % $num_devices )) default_sink=${sink_arr[$default_sink_index]} pacmd set-default-sink $default_sink @@ -77,7 +77,7 @@ esac for name in INDEX NAME VOL MUTED; do read $name -done < <(pacmd list-sinks | grep "index:\|name:\|volume: front\|muted:" | grep -A3 '*') +done < <(pactl list sinks | grep "index:\|name:\|volume: front\|muted:" | grep -A3 '*') INDEX=$(echo "$INDEX" | grep -o '[0-9]\+') VOL=$(echo "$VOL" | grep -o "[0-9]*%" | head -1 ) VOL="${VOL%?}" @@ -89,7 +89,7 @@ NAME=$(echo "$NAME" | sed \ if [[ $USE_ALSA_NAME == 1 ]] ; then ALSA_NAME=$( - pacmd list-sinks |\ + pactl list sinks |\ awk '/^\s*\*/{f=1}/^\s*index:/{f=0}f' |\ grep "alsa.name\|alsa.mixer_name" |\ head -n1 |\ diff --git a/nvim/conf.d/coc.vim b/nvim/conf.d/coc.vim index 413e32d..295fb66 100644 --- a/nvim/conf.d/coc.vim +++ b/nvim/conf.d/coc.vim @@ -53,6 +53,8 @@ inoremap \ coc#refresh() inoremap pumvisible() ? "\" : "\" +inoremap pumvisible() ? "\" : "\" +inoremap pumvisible() ? "\" : "\" function! s:check_back_space() abort let col = col('.') - 1 return !col || getline('.')[col - 1] =~# '\s' diff --git a/nvim/init.vim b/nvim/init.vim index 29555af..540dca7 100644 --- a/nvim/init.vim +++ b/nvim/init.vim @@ -26,7 +26,7 @@ Plug 'jparise/vim-graphql' Plug 'posva/vim-vue' " Python formatter -Plug 'psf/black', {'commit': 'ce14fa8b497bae2b50ec48b3bd7022573a59cdb1'} +Plug 'psf/black' " tag finder Plug 'majutsushi/tagbar'