#!/usr/bin/perl -w
#
# VIT - Visual Interactive Taskwarrior
#
# vit-1.2 built Sun Apr 26 18:06:02 MDT 2026
#
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

use strict;
use Curses;
use Time::HiRes qw(usleep);

our $commands_file = '/usr/local/share/vit/commands';
our $task = '/usr/local/bin/task';
our $clear = '/usr/bin/clear';
if ( $commands_file =~ /^%/ ) { $commands_file = "./commands"; }
if ( $task =~ /^%/ ) { $task = '/usr/local/bin/task'; }
if ( $clear =~ /^%/ ) { $clear = '/usr/bin/clear'; }

our $cli_args = '';
our $audit = 0;
our @colors2pair;
our $convergence = '';
our $current_command = 'unknown';
our $cursor_position = 'unknown';
our $default_command = 'next';
our $display_start_idx = 0;
our $error_delay = 500000;
our $error_msg = '';
our $flash_convergence = 0;
our $flash_delay = 80000;
our $header_win;
our $header_attrs;
our $input_mode = 'cmd';
our $num_projects = 0;
our $num_tasks = 0;
our $feedback_msg = '';
our @parsed_tokens = ();
our @parsed_colors_fg = ();
our @parsed_colors_bg = ();
our @parsed_attrs = ();
our $prev_display_start_idx;
our $prev_ch = '';
our $prev_command = 'next';
our $prev_convergence = '';
our $prev_task_selected_idx;
our @project_types = ();
our $prompt_win;
our $refresh_needed = 0;
our $reread_needed = 0;
our $report_descr = 'unknown';
our $report_win;
our @report_header_tokens = ();
our @report_header_colors_fg = ();
our @report_header_colors_bg = ();
our @report_header_attrs = ();
our @report_tokens = ();
our @report_lines = ();
our @report_types = ();
our @report_colors_fg = ();
our @report_colors_bg = ();
our @report_attrs = ();
our @report2taskid = ();
our $search_direction = 1;
our $search_pat = '';
our $selection_attrs = '';
our @taskid2report = ();
our $tasks_completed = 0;
our $tasks_pending = 0;
our $task_selected_idx = 0;
our $titlebar = 0;
our $version = 'vit-1.2 (20260426)';
our $REPORT_LINES;
our $REPORT_COLS;

our $COLOR_HEADER = 1;
our $COLOR_REPORT_HEADER = 2;
our $COLOR_SELECTION = 3;
our $COLOR_EMPTY_LINE = 4;
our $COLOR_ERRORS = 5;
our $next_color_pair = 6;

our %shortcuts;
our $cur_pos;


###################################################################
## main...

&parse_args();
&parse_vitrc();
&init_shell_env();
&init_curses('init');
&init_task_env();
&read_report('init');
&draw_screen();
&getch_loop();
&clean_exit();

########################################################
## args.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub parse_args {
  while ( @ARGV ) {
    if ( $ARGV[0] eq '-help' ) {
      &usage();
    }
    if ( $ARGV[0] eq '-audit' || $ARGV[0] eq '-a' ) {
      $audit = 1;
      shift @ARGV;
      next;
    }
    if ( $ARGV[0] eq '-titlebar' || $ARGV[0] eq '-t' ) {
      $titlebar = 1;
      shift @ARGV;
      next;
    }
    $cli_args .= "$ARGV[0] ";
    shift @ARGV;
    next;
  }
  if ( $audit ) {
    open(AUDIT, ">", "vit_audit.log") or die "$!";
    open STDERR, '>&AUDIT';

    # flush AUDIT after printing to it
    my $ofh = select AUDIT;
    $| = 1;
    select $ofh;

    print AUDIT "$$ INIT $0 " . join(' ',@ARGV), "\r\n";
  }
}

#------------------------------------------------------------------

sub usage {
  print "usage: vit [switches] [task_args]\n";
  print "  -audit     print task commands to vit_audit.log\n";
  print "  -titlebar  sets the xterm titlebar to \"$version\"\n";
  print "  task_args  any set of task commandline args that print an \"ID\" column\n";
  exit 1;
}


########################################################
## cmdline.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub cmd_line {
  &audit("Inside of cmd_line");
  my ($prompt) = @_;
  my $str = &prompt_str($prompt);
  if ( $str eq '' ) {
    &draw_prompt_line('');
    return;
  }
  if ( $str =~ /^!(.*)/ ) {
    my $rtn = &shell_command($1);
    return;
  }
  if ( $str =~ /^\d+$/ ) {
    if ( ! defined $taskid2report[$str] ) {
       $error_msg = "Error: task number $str not found";
       &draw_error_msg();
       return;
    }
    $task_selected_idx = $taskid2report[$str] - 1;
    if ( $display_start_idx + $REPORT_LINES < $task_selected_idx ) {
      $display_start_idx = int($task_selected_idx - $REPORT_LINES + ($REPORT_LINES / 2));
    }
    if ( $display_start_idx > $task_selected_idx ) {
      $display_start_idx = int($task_selected_idx - $REPORT_LINES + ($REPORT_LINES / 2));
      if ( $display_start_idx < 0 ) {
        $display_start_idx = 0;
      } elsif ( $display_start_idx > $task_selected_idx) {
        $display_start_idx = $task_selected_idx;
      }
    }
    &draw_screen();
    return;
  }
  if ( $str =~ /^s\/(.*?)\/(.*)\/$/ || $str =~ /^%s\/(.*?)\/(.*)\/$/ ) {
    my ($old,$new) = ($1,$2);
    my $rtn = &task_modify("/$old/$new/");
    $reread_needed = 1;
    return;
  }
  if ( $str eq 'help' || $str eq 'h' ) {
    &shell_exec("view $commands_file",'no-wait');
    return;
  }
  if ( $str =~ /^help (.*)/ || $str =~ /^h (.*)/ ) {
    my $p = $1;
    my $tmp_file = "/tmp/vit-help.$$";
    open(IN,"<$commands_file");
    open(OUT,">$tmp_file");
    print OUT "\n";
    while(<IN>) {
      if ( $_ =~ /$p/ ) {
        print OUT $_;
      }
    }
    close(IN);
    print OUT "\n";
    close(OUT);
    &shell_exec("view $tmp_file",'no-wait');
    unlink($tmp_file);
    return;
  }
  if ( $str eq 'q' ) {
    &clean_exit();
  }
  if ( grep(/^$str$/,@report_types) ) {
    $prev_command = $current_command;
    $current_command = $str;
    &read_report('init');
    &draw_screen();
    return;
  }
  if ( $str =~ /^(.*?) .*/ ) {
    my $s = $1;
    if ( grep(/^$s/,@report_types) ) {
      $prev_command = $current_command;
      $current_command = $str;
      &read_report('init');
      &draw_screen();
      return;
    }
  }
  $error_msg = "$str: command not found";
  &draw_error_msg();
  return;
}

########################################################
## cmds.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

#------------------------------------------------------------------

sub prompt_quit {
  my $yes;
  $yes = &prompt_y("Quit?");
  if ( ! $yes ) {
    &draw_prompt_line('');
    return;
  }
  endwin();
  exit();
}

#------------------------------------------------------------------

sub task_add {
  my $str = &prompt_str("Add: ");
  if ( $str eq '' ) {
    &draw_prompt_line('');
    return;
  }
  my ($es,$result) = &task_exec("add \"$str\"");
  if ( $es != 0 ) {
    $error_msg = $result;
    &draw_error_msg();
    return;
  }
  $feedback_msg = $result;
  $reread_needed = 1;
}

#------------------------------------------------------------------

sub task_annotate {
  my $id = $report2taskid[$task_selected_idx];
  my $str = &prompt_str("Annotate: ");
  if ( $str eq '' ) {
    &draw_prompt_line('');
    return;
  }
  my ($es,$result) = &task_exec("$id annotate \"$str\"");
  if ( $es != 0 ) {
    $error_msg = $result;
    &draw_error_msg();
    return;
  }
  $feedback_msg = "Annotated task $id.";
  &draw_feedback_msg();
  $reread_needed = 1;
}

#------------------------------------------------------------------

sub task_den_or_del {
  my ($ch, $str, $yes);
  my $id = $report2taskid[$task_selected_idx];
  for my $t (0 .. $#{ $report_tokens[$task_selected_idx] } ) {
    $str .= "$report_tokens[$task_selected_idx][$t]";
  }
  my $target = ( $str !~ s/^\s*\d+\/\d+\/\d+\s+// )
             ? "task"
             : "annotation";
  $str =~ s/\s+$//;
  $yes = &prompt_y("Delete current $target? ");
  if ( ! $yes ) {
    &draw_prompt_line('');
    return;
  }
  my ($es,$result) = ($target eq "annotation")
                   ? &task_exec("$id denotate \"$str\"")
                   : &task_exec("$id delete rc.confirmation:no");
  if ( $es != 0 ) {
    $error_msg = $result;
    &draw_error_msg();
    return;
  }
  $feedback_msg = "Deleted $target.";
  &draw_feedback_msg();
  &flash_current_task();
  $reread_needed = 1;
}

#------------------------------------------------------------------

sub task_done {
  my ($ch, $str, $yes);
  my $id = $report2taskid[$task_selected_idx];
  $yes = &prompt_y("Mark task $id done? ");
  if ( ! $yes ) {
    &draw_prompt_line('');
    return;
  }
  my ($es,$result) = &task_exec("$id done");
  if ( $es != 0 ) {
    $error_msg = $result;
    &draw_error_msg();
    return;
  }
  $feedback_msg = "Marked task done.";
  &draw_feedback_msg();
  &flash_current_task();
  $reread_needed = 1;
}

#------------------------------------------------------------------

sub task_filter {
  my ($c, $f);
  if ( $current_command =~ /(.*?)\s+(.*)/ ) {
    ($c,$f) = ($1,$2);
  } else {
    $c = $current_command;
    $f = '';
  }
  my $str = &prompt_str("Filter: $f");
  if ( $str eq '' ) {
    &draw_prompt_line('');
    $current_command = $c;
    if ( $f ne '' ) { $reread_needed = 1; }
    return;
  }
  $prev_command = $current_command;
  $current_command = "$c $str";
  $reread_needed = 1;
}

#------------------------------------------------------------------

sub task_modify {
  my $args = $_[0];
  my $id = $report2taskid[$task_selected_idx];
  &shell_exec("$task $id modify \"$args\"",'wait');
  $reread_needed = 1;
}

#------------------------------------------------------------------

sub task_modify_prompt {
  my $id = $report2taskid[$task_selected_idx];
  my $str = &prompt_str("Modify: ");
  if ( $str eq '' ) {
    &draw_prompt_line('');
    return;
  }
  &task_modify("$str");
  $reread_needed = 1;
}

#------------------------------------------------------------------

sub task_set_priority {
  my $id = $report2taskid[$task_selected_idx];
  my $prio = &task_info('Priority');
  if ( $prio eq '' ) {
    $prio = 'N';
  }
  my $p = &prompt_chr("Change priority (l/m/h/n): ");
  $p = uc($p);
  if ( $p ne $prio && $p =~ /[LMHN]/ ) {
    if ( $p eq 'N' ) {
      $p = '';
    }
    my ($es,$result) = &task_exec("$id modify prio:$p");
    if ( $es != 0 ) {
      $error_msg = $result;
      &draw_error_msg();
      return;
    }
    $feedback_msg = "Modified task $id.";
    &flash_current_task();
    $reread_needed = 1;
  }
  else {
    &draw_prompt_line('');
    return;
  }
}

#------------------------------------------------------------------

sub task_set_project {
  my $id = $report2taskid[$task_selected_idx];
  my $p = &prompt_str("Project: ");
  if ( $p eq '' ) {
    &draw_prompt_line('');
    return;
  }
  my $proj = &task_info('Project');
  if ( $p eq $proj ) {
    beep();
    return;
  }
  my ($es,$result) = &task_exec("$id modify proj:$p");
  if ( $es != 0 ) {
    $error_msg = $result;
    &draw_error_msg();
    return;
  }
  $feedback_msg = "Modified task $id.";
  &flash_current_task();
  $reread_needed = 1;
}

#------------------------------------------------------------------

sub shell_command {
  my $args = $_[0];
  my ($opts, $cmd);
  if ( $args =~ /([^ ]*) (.+)/ ) {
    $opts = $1;
    $cmd = $2;
  }
  else {
    $error_msg = "Empty shell command for ':!'. See help (:h).";
    &draw_error_msg();
    return;
  }

  my $wait = "no-wait";
  foreach my $l ( split //, $opts ) {
    if ( $l eq 'r' ) {
      $reread_needed = 1;
    } elsif ( $l eq 'w' ) {
      $wait = "wait";
    } else {
      $error_msg = "$l is not a valid command option to ':!'. See help (:h).";
      &draw_error_msg();
      return;
    }
  }

  $cmd =~ s/%TASKID/$report2taskid[$task_selected_idx]/g;
  $cmd =~ s/%TASKARGS/$current_command/g;

  &shell_exec($cmd,"$wait");
}

########################################################
## color.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub parse_report_line {
  my ($l,$str) = @_;
  &parse_line($l,$str);
  if ( $current_command eq 'summary' && $parsed_tokens[0] =~ /^(\d+) project/ ) {
    $num_projects = $1;
    return;
  }
  push @{ $report_tokens[$l] }, (@parsed_tokens);
  push @{ $report_colors_fg[$l] },  (@parsed_colors_fg);
  push @{ $report_colors_bg[$l] }, (@parsed_colors_bg);
  push @{ $report_attrs[$l] }, (@parsed_attrs);
}

#------------------------------------------------------------------

sub parse_line {
  my ($l,$str) = @_;
  my $fg = 999999;
  my $bg = 999999;
  my $attr = '';
  @parsed_tokens = ();
  @parsed_colors_fg = ();
  @parsed_colors_bg = ();
  @parsed_attrs = ();
  my @toks = split(/\x1B/,$str);
  my $t = 0;
  #debug("PARSE IN $str");
  for my $tok (@toks) {
    if ( $tok eq '' ) { next; }
    $attr = '';
    CASE: {
      # ANSI 16 color attr pairs...
      if ( $tok =~ s/\[(\d+);(\d+)m// ) {
        my ($a,$b) = ($1,$2);
        if ( $a > 30 ) {
          $fg = $a - 30;
          $bg = $b - 40;
        } else {
          if ( $a eq '1' ) { $attr .= 'bold '; }
          if ( $b eq '4' ) { $attr .= 'underline' ; }
          if ( $a eq '7' ) { $attr .= 'inverse '; }
          if ( $b < 38 ) {
            $fg = $b - 30;
          } else {
            $bg = $b - 40;
          }
        }
        last CASE;
      }
      # ANSI 16 color single colors or single attrs or attrs off...
      if ( $tok =~ s/\[(\d+)m// ) {
        my $a = $1;
        if ( $a eq '0' ) {
          $fg = $bg = 999999;
          $attr .= 'none ';
        } elsif ( $a eq '1' ) {
          $attr .= 'bold ';
        } elsif ( $a eq '4' ) {
          $attr .= 'underline ';
        } elsif ( $a < 38 ) {
          $fg = $a - 30;
          $bg = 999999;
        } elsif ( $a > 99 ) {
          $attr .= 'standout '; # "bright" in taskwarrior
          $bg = $a - 100;
          $fg = 999999;
        } else {
          $bg = $a - 40;
          $fg = 999999;
        }
        last CASE;
      }
      # ANSI 16 color bold...
      if ( $tok =~ s/\[1;(\d+);(\d+)m// ) {
        my ($a,$b) = ($1,$2);
        $attr .= 'bold ';
        $fg = $a - 30;
        $bg = $b - 40;
        last CASE;
      }
      # ANSI 16 color underline...
      if ( $tok =~ s/\[4;(\d+);(\d+)m// ) {
        my ($a,$b) = ($1,$2);
        $attr .= 'underline ';
        $fg = $a - 30;
        $bg = $b - 40;
        last CASE;
      }
      # ANSI 16 color inverse...
      if ( $tok =~ s/\[7;(\d+);(\d+)m// ) {
        my ($a,$b) = ($1,$2);
        $attr .= 'inverse ';
        $fg = $a - 30;
        $bg = $b - 40;
        last CASE;
      }
      # 256 color xterm foreground...
      if ( $tok =~ s/\[38;5;(\d+)m// ) {
        $fg = $1;
        last CASE;
      }
      # 256 color xterm background...
      if ( $tok =~ s/\[48;5;(\d+)m// ) {
        $bg = $1;
        last CASE;
      }
    }
    # FIXME summary mode...
    # if ( $tok =~ /0%\s+100%/ ) { debug("summary graph tok=\"$tok\" column=$t"); }
    if ( $tok ne '' ) {
      $parsed_tokens[$t] = $tok;
      $parsed_colors_fg[$t] = $fg;
      $parsed_colors_bg[$t] = $bg;
      if ( $attr eq '' ) { $attr = 'none'; }
      $parsed_attrs[$t] = $attr;
      #if ( $t == 0 ) { debug("PARSE OUT tok=\"$tok\" pos=$l.$t cp=$fg,$bg attr=$attr"); }
      $t++;
    }
  }
}

#------------------------------------------------------------------

sub extract_color {
  my ($s,$t) = @_;
  $parsed_colors_fg[1] = -1;
  $parsed_colors_bg[1] = -1;
  $parsed_attrs[1] = '';
  &audit("EXEC $task rc._forcecolor=on color $s 2>&1");
  open(IN2,"$task rc._forcecolor=on color $s 2>&1 |");
  while(<IN2>){
    if ( $_ =~ /Your sample:/ ) {
      $_ = <IN2>; $_ = <IN2>;
      &parse_line(0,$_);
      if ( $parsed_colors_fg[1] eq '999999' ) { $parsed_colors_fg[1] = -1; }
      if ( $parsed_colors_bg[1] eq '999999' ) { $parsed_colors_bg[1] = -1; }
    }
  }
  close(IN2);
}

#------------------------------------------------------------------


########################################################
## curses.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub init_curses {
  my $m = $_[0];
  initscr();
  noecho();
  curs_set(0);
  start_color();
  use_default_colors();
  init_pair($COLOR_ERRORS,COLOR_WHITE,COLOR_RED);
  if ( $m eq 'init' ) {
    init_pair($COLOR_SELECTION,COLOR_WHITE,COLOR_BLUE);
  }
  init_pair($COLOR_EMPTY_LINE,COLOR_BLUE,-1); # blue foreground
  my $HEADER_SIZE = 3;
  $REPORT_LINES = $LINES - $HEADER_SIZE - 1;
  $REPORT_COLS = $COLS - 2;
  $header_win = newwin($HEADER_SIZE, $COLS, 0, 0);
  $report_win = newwin($REPORT_LINES+$HEADER_SIZE, $REPORT_COLS+2, 3, 1);
  $prompt_win = newwin(1, $COLS, $LINES-1, 0);
  keypad($report_win, 1);
  keypad($prompt_win, 1);
}

#------------------------------------------------------------------

sub get_color_pair {
  my($fg,$bg) = @_;
  my $cp = 0;
  if ( defined $colors2pair[$fg][$bg] ) {
    $cp = $colors2pair[$fg][$bg];
  } else {
    $cp = $next_color_pair;
    $colors2pair[$fg][$bg] = $next_color_pair;
    $next_color_pair++;
    if ( $fg == 999999 ) { $fg = -1; }
    if ( $bg == 999999 ) { $bg = -1; }
    init_pair($cp,$fg,$bg);
  }
  return $cp;
}


########################################################
## draw.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub draw_header_line {
  my ($row,$lhs,$rhs) = @_;
  my $str = ' ' x $COLS;
  $header_win->addstr($row, 0, $str);
  $header_win->addstr($row, 0, $lhs);
  $header_win->addstr($row, $COLS - length($rhs), $rhs);
  $header_win->refresh();
}

#------------------------------------------------------------------

sub draw_prompt_line {
  my ($lhs) = @_;
  $prompt_win->addstr(0, 0, $lhs);
  $prompt_win->clrtoeol();
  $prompt_win->addstr(0, $COLS - length($cursor_position) - 1, $cursor_position);
  $prompt_win->refresh();
}

#------------------------------------------------------------------

sub draw_prompt {
  my ($lhs) = @_;
  $prompt_win->addstr(0, 0, $lhs);
  $prompt_win->clrtoeol();
  $prompt_win->refresh();
}

#------------------------------------------------------------------

sub draw_prompt_cur {
  my ($lhs) = @_;
  $prompt_win->addstr(0, 0, $lhs);
  $prompt_win->clrtoeol();
  $prompt_win->move(0, $cur_pos);
  $prompt_win->refresh();
}

#------------------------------------------------------------------

sub draw_error_msg {
  beep();
  &audit("ERROR $error_msg");
  $prompt_win->addstr(0, 0, ' ');
  $prompt_win->clrtoeol();
  $prompt_win->attron(COLOR_PAIR($COLOR_ERRORS));
  $prompt_win->attron(A_BOLD);
  $prompt_win->addstr(0, 0, $error_msg);
  $prompt_win->attroff(A_BOLD);
  $prompt_win->attroff(COLOR_PAIR($COLOR_ERRORS));
  $prompt_win->attron(COLOR_PAIR($COLOR_HEADER));
  $prompt_win->addstr(0, $COLS - length($cursor_position) - 1, $cursor_position);
  $prompt_win->refresh();
}

#------------------------------------------------------------------

sub draw_feedback_msg {
  my $len = length($feedback_msg);
  my $start = ($COLS/2) - ($len/2) - 3;
  $prompt_win->addstr(0, 0, ' ');
  $prompt_win->clrtoeol();
  $prompt_win->addstr(0, $start, $feedback_msg);
  $prompt_win->addstr(0, $COLS - length($cursor_position) - 1, $cursor_position);
  $prompt_win->refresh();
}

#------------------------------------------------------------------

sub draw_report_line {
  my ($i,$line,$mode) = @_;
  my ($x, $t, $cp, $str);
  $x = 0;
  if ( $mode eq 'with-selection' && $i == $task_selected_idx ) {
    $report_win->attron(COLOR_PAIR($COLOR_SELECTION));
    &set_attron($report_win,$selection_attrs);
  }
  for $t (0 .. $#{ $report_tokens[$i] } ) {
    if ( $mode eq 'without-selection' || $i != $task_selected_idx ) {
      my $fg = $report_colors_fg[$i][$t];
      my $bg = $report_colors_bg[$i][$t];
      $cp = &get_color_pair($fg,$bg);
      $report_win->attron(COLOR_PAIR($cp));
    }
    #if ( $t == 0 ) { debug("DRAW tok=$line.$t cp=$cp \"$report_tokens[$i][$t]\""); }
    &set_attron($report_win,$report_attrs[$i][$t]);
    $report_win->addstr($line,$x,$report_tokens[$i][$t]);
    &set_attroff($report_win,$report_attrs[$i][$t]);
    if ( $mode eq 'without-selection' || $i != $task_selected_idx ) {
      $report_win->attroff(COLOR_PAIR($cp));
    }
    $x += length($report_tokens[$i][$t]);
  }
  $str = ' ' x ($REPORT_COLS - $x);
  if ( $mode eq 'without-selection' || $i != $task_selected_idx ) {
    $report_win->attron(COLOR_PAIR($cp));
  }
  &set_attron($report_win,$report_attrs[$i][$#{ $report_tokens[$i] }]);
  $report_win->addstr($line,$x,$str);
  &set_attroff($report_win,$report_attrs[$i][$#{ $report_tokens[$i] }]);
  if ( $mode eq 'with-selection' && $i == $task_selected_idx ) {
    $report_win->attroff(COLOR_PAIR($COLOR_SELECTION));
    &set_attroff($report_win,$selection_attrs);
  } else {
    $report_win->attroff(COLOR_PAIR($cp));
  }
}

#------------------------------------------------------------------

sub flash_current_task {
  my ($x, $t, $cp, $str);
  my $i = $task_selected_idx;
  my $line = $task_selected_idx - $display_start_idx;

  &draw_report_line($i,$line,'without-selection');
  $report_win->refresh();
  usleep($flash_delay);

  $report_win->addstr($line,0,' ');
  $report_win->clrtoeol();
  $report_win->refresh();
  usleep($flash_delay);

  &draw_report_line($i,$line,'without-selection');
  $report_win->refresh();
  usleep($flash_delay);

  $report_win->addstr($line,0,' ');
  $report_win->clrtoeol();
  $report_win->refresh();
  usleep($flash_delay);

  &draw_report_line($i,$line,'without-selection');
  $report_win->refresh();
  usleep($flash_delay);
}

#------------------------------------------------------------------

sub flash_convergence {
  $header_win->attron(COLOR_PAIR($COLOR_HEADER));
  &set_attron($header_win,$header_attrs);
  &draw_header_line(1,'',"$tasks_completed tasks completed");
  usleep($flash_delay);
  &draw_header_line(1,$convergence,"$tasks_completed tasks completed");
  usleep($flash_delay);
  &draw_header_line(1,'',"$tasks_completed tasks completed");
  usleep($flash_delay);
  &draw_header_line(1,$convergence,"$tasks_completed tasks completed");
  usleep($flash_delay);
  &set_attroff($header_win,$header_attrs);
  $header_win->attroff(COLOR_PAIR($COLOR_HEADER));
}

#------------------------------------------------------------------

sub set_attron {
  my ($win,$attr) = @_;
  if ( ! defined $attr ) { return; }
  if ( $attr =~ /underline/ ) {
    $win->attron(A_UNDERLINE);
  }
  if ( $attr =~ /bold/ ) {
    $win->attron(A_BOLD);
  }
}

#------------------------------------------------------------------

sub set_attroff {
  my ($win,$attr) = @_;
  if ( ! defined $attr ) { return; }
  if ( $attr =~ /underline/ ) {
    $win->attroff(A_UNDERLINE);
  }
  if ( $attr =~ /bold/ ) {
    $win->attroff(A_BOLD);
  }
  if ( $attr =~ /inverse/ ) {
    $win->attroff(A_REVERSE);
  }
  if ( $attr =~ /standout/ ) {
    $win->attroff(A_STANDOUT);
  }
}

########################################################
## env.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub init_shell_env {
  if ( $ENV{'TERM'} =~ /^xterm/ || $ENV{'TERM'} =~ /^screen/ ) {
    &audit("ENV TERM=xterm-256color");
    $ENV{'TERM'} = 'xterm-256color';
  }
  if ( $titlebar ) {
    &audit("ENV set titlebar");
    open(TTY, ">>/dev/tty");
    print TTY "\e]0;$version\cg\n";
    close(TTY);
  }
}

#------------------------------------------------------------------

sub init_task_env {
  my @reports;
  my $id_column = 0;
  my ($header_color,$task_header_color,$vit_header_color);
  &audit("EXEC $task show 2>&1");
  open(IN,"$task show 2>&1 |");
  while(<IN>) {
    chop;
    if ( $_ =~ /color\.header\s+(.*)/ ) {
      $task_header_color = $1;
      next;
    }
    if ( $_ =~ /color\.vit\.header\s+(.*)/ ) {
      $vit_header_color = $1;
      next;
    }
    if ( $_ =~ /color\.vit\.selection\s+(.*)/ ) {
      my $str = $1;
      $str =~ s/\x1b.*?m//g;
      $str =~ s/^\s+//;
      $str =~ s/\s+$//;
      &extract_color($str,'vit selection');
      $selection_attrs = $parsed_attrs[1];
      init_pair($COLOR_SELECTION,$parsed_colors_fg[1],$parsed_colors_bg[1]);
      next;
    }
    if ( $_ =~ /default.command\s+(.*)/ ) {
      $default_command = $1;
      $default_command =~ s/\x1b.*?m//g;
      $default_command =~ s/^\s+//g;
      $default_command =~ s/\s+$//g;
      next;
    }
    if ( $_ =~ /report\.(.*?)\.columns/ ) {
      push(@reports, $1);
      next;
    }
    if ( $_ =~ /The color .* is not recognized/ ) {
      endwin();
      print "$_\r\n";
      exit(1);
    }
  }
  close(IN);
  if ( defined $vit_header_color ) {
    $header_color = $vit_header_color;
  } elsif ( defined $task_header_color ) {
    $header_color = $task_header_color;
  } else {
    init_pair($COLOR_HEADER,-1,-1); # not reached
  }
  if ( defined $header_color ) {
    &extract_color($header_color,'header');
    $header_color =~ s/\x1b.*?m//g;
    $header_color =~ s/^\s+//;
    $header_color =~ s/\s+$//;
    $header_attrs = $parsed_attrs[1];
    init_pair($COLOR_HEADER,$parsed_colors_fg[1],$parsed_colors_bg[1]);
  }
  if ( $cli_args ne '' ) {
    chop $cli_args;
    $default_command = $cli_args;
  }
  &audit("EXEC $task rc._forcecolor=on rc.verbose=on $default_command 2>&1");
  open(IN,"$task rc._forcecolor=on rc.verbose=on $default_command 2>&1 |");
  while(<IN>) {
    chop;
    if ( $_ =~ /ID/ || ($default_command eq 'summary' && $_ =~ /Project/) ) {
      &parse_line(0,$_);
      @report_header_colors_fg = @parsed_colors_fg;
      @report_header_colors_bg = @parsed_colors_bg;
      @report_header_attrs = @parsed_attrs;
      if ( $parsed_colors_fg[0] eq '999999' ) { $parsed_colors_fg[0] = -1; }
      if ( $parsed_colors_bg[0] eq '999999' ) { $parsed_colors_bg[0] = -1; }
      init_pair($COLOR_REPORT_HEADER,$parsed_colors_fg[0],$parsed_colors_bg[0]);
      $id_column = 1;
    }
  }
  close(IN);
  if ( ! $id_column && $default_command ne 'summary' ) {
    endwin();
    print "Fatal error: default.command (\"$default_command\") must print an \"ID\" column\n";
    exit(1);
  }
  &audit("ENV default command is \"$default_command\"");
  $current_command = $default_command;
  push(@reports,$default_command);
  push(@reports,'summary');
  @report_types = sort(@reports);

}

########################################################
## exec.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub task_exec {
  my ($cmd) = @_;
  my $es = 0;
  my $result = '';
  &audit("TASK EXEC $task $cmd 2>&1");
  open(IN,"echo -e \"yes\\n\" | $task $cmd 2>&1 |");
  while(<IN>) {
    chop;
    $_ =~ s/\x1b.*?m//g; # decolorize
    if ( $_ =~ /^\w+ override:/ ) { next; }
    $result .= "$_ ";
  }
  close(IN);
  if ( $! ) {
    $es = 1;
    &audit("FAILED \"$task $cmd\" error closing short pipe");
  }
  if ( $? != 0 ) {
    $es = $?;
    &audit("FAILED \"$task $cmd\" returned exit status $?");
  }
  return ($es,$result);
}

#------------------------------------------------------------------

sub shell_exec {
  my ($cmd,$mode) = @_;
  endwin();
  if ( $clear ne 'NOT_FOUND' ) { system("$clear"); }
  if ( $audit ) {
    print "$_[0]\r\n";
  }
  if ( ! fork() ) {
    &audit("EXEC $cmd");
    exec($cmd);
    exit();
  }
  wait();
  if ( $mode eq 'wait' ) {
    print "Press return to continue.\r\n";
    <STDIN>;
  }
  &init_curses('refresh');
  &draw_screen();
}


########################################################
## getch.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub getch_loop {
  while (1) {
    my $ch = $report_win->getch();
    &audit("Received key: $ch");
    $refresh_needed = 0;
    $reread_needed = 0;
    $error_msg = '';
    $feedback_msg = '';

    CASE: {

      if (exists $shortcuts{$ch}) {
        my $action = $shortcuts{$ch};
        &audit("Processing the following shortcut: $action");
        &ungetstr($action);
        last CASE;
      }

      if ( $ch eq '0' || ( $ch eq 'g' && $prev_ch eq 'g' ) ) {
        $task_selected_idx = 0;
        $display_start_idx = 0;
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch =~ /^\d$/ ) {
        &cmd_line(":$ch");
        last CASE;
      }

      if ( $ch eq 'a' ) {
        &task_add();
        last CASE;
      }

      if ( $ch eq 'A' ) {
        &task_annotate();
        last CASE;
      }

      if ( $ch eq 'D' ) {
        &task_den_or_del();
        last CASE;
      }

      if ( $ch eq 'd' ) {
        if ( grep(/^Complete\s*$/,@report_header_tokens) ) { # FIXME: really, good enough?
          $error_msg = "Error: task has already been completed.";
          $refresh_needed = 1;
          last CASE;
        }
        &task_done();
        last CASE;
      }

      if ( $ch eq "e" ) {
        &shell_exec("task $report2taskid[$task_selected_idx] edit",'wait');
        $reread_needed = 1;
        last CASE;
      }

      if ( $ch eq 'f' ) {
        &task_filter();
        last CASE;
      }

      if ( $ch eq 'G' ) {
        $task_selected_idx = $#report_tokens;
        if ( $display_start_idx + $REPORT_LINES <= $#report_tokens ) {
          $display_start_idx = $task_selected_idx - $REPORT_LINES + 1;
        }
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq 'H' ) {
        $task_selected_idx = $display_start_idx;
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq 'j' || $ch eq KEY_DOWN || $ch eq ' ' ) {
        if ( $task_selected_idx >= $#report_tokens ) {
          beep;
          last CASE;
        }
        $task_selected_idx++;
        if ( $task_selected_idx - $REPORT_LINES >= $display_start_idx ) {
          $display_start_idx++;
        }
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq 'k' || $ch eq KEY_UP ) {
        if ( $task_selected_idx == 0 ) {
          beep;
          last CASE;
        }
        $task_selected_idx--;
        if ( $task_selected_idx < $display_start_idx ) {
          $display_start_idx--;
        }
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq 'L' ) {
        $task_selected_idx = $display_start_idx + $REPORT_LINES - 1;
        if ( $task_selected_idx >= $#report_tokens-1 ) { $task_selected_idx = $#report_tokens; }
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq 'M' ) {
        $task_selected_idx = $display_start_idx + int($REPORT_LINES / 2);
        if ( $display_start_idx + $REPORT_LINES > $#report_tokens ) {
          $task_selected_idx = $display_start_idx + int(($#report_tokens - $display_start_idx) / 2);
        }
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq 'm' ) {
        &task_modify_prompt();
        last CASE;
      }

      if ( $ch eq 'N' && $input_mode eq 'search' ) {
        &do_search('N');
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq 'n' && $input_mode eq 'search' ) {
        &do_search('n');
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq 'P' ) {
        &task_set_priority();
        last CASE;
      }

      if ( $ch eq 'p' ) {
        &task_set_project();
        last CASE;
      }

      if ( $ch eq 'q' ) {
        &prompt_quit();
        last CASE;
      }

      if ( $ch eq 'Q' || ($ch eq 'Z' && $prev_ch eq 'Z') ) {
        return;
      }

      if ( $ch eq 's' ) {
        my $majmin = &task_version('major.minor');
        if ( $majmin >= 2.3 ) {
          &shell_exec("task sync",'wait');
        }
        else {
          $error_msg = "'sync' was introduced in Taskwarrior 2.3.0";
          $refresh_needed = 1;
        }
        last CASE;
      }

      if ( $ch eq 't' ) {
        &ungetstr(':!rw task ')
      }

      if ( $ch eq 'u' ) {
        &shell_exec('task undo','wait');
        $reread_needed = 1;
        last CASE;
      }

      if ( $ch eq '/' ) {
        $search_direction = 1;
        &start_search();
        last CASE;
      }

      if ( $ch eq '?' ) {
        $search_direction = 0;
        &start_search();
        last CASE;
      }

      if ( $ch eq ':' ) {
        &cmd_line(':');
        last CASE;
      }

      if ( $ch eq '=' || $ch eq "\n" ) {
        if ( $current_command eq 'summary' ) {
          my $p = $report_tokens[$task_selected_idx][0];
          $p =~ s/(.*?)\s+.*/$1/;
          $p =~ s/\(none\)//;
          $current_command = "ls proj:$p";
          $reread_needed = 1;
        } else {
          &shell_exec("task $report2taskid[$task_selected_idx] info",'wait');
        }
        last CASE;
      }

      if ( $ch eq "\cb" || $ch eq KEY_PPAGE ) {
        $display_start_idx -= $REPORT_LINES;
        $task_selected_idx -= $REPORT_LINES;
        if ( $display_start_idx < 0 ) { $display_start_idx = 0; }
        if ( $task_selected_idx < 0 ) { $task_selected_idx = 0; }
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq "\cf" || $ch eq KEY_NPAGE ) {
        $display_start_idx += $REPORT_LINES;
        $task_selected_idx += $REPORT_LINES;
        if ( $task_selected_idx > $#report_tokens ) {
          $display_start_idx = $#report_tokens;
          $task_selected_idx = $#report_tokens;
        }
        $refresh_needed = 1;
        last CASE;
      }

      if ( $ch eq "\cl" ) {
        endwin();
        &init_curses('refresh');
        &read_report('refresh');
        if ( $task_selected_idx > $display_start_idx + $REPORT_LINES - 1 ) {
          $display_start_idx = $task_selected_idx - $REPORT_LINES + 1;
        }
        &draw_screen();
        last CASE;
      }

      if ( $ch eq "\e" ) {
        $error_msg = '';
        $feedback_msg = '';
        $refresh_needed = 1;
        $input_mode = 'cmd';
        last CASE;
      }
      if ( $ch eq 'Z' ) { last CASE; }
      if ( $ch eq "410" ) {
        # FIXME resize
        &init_curses('refresh');
        &draw_screen();
        last CASE;
      }
      if ( $ch eq '-1' ) { last CASE; }
      beep();
    }
    if ( $ch ne '/' && $ch ne '?' && $ch ne 'n' && $ch ne 'N' ) {
      $input_mode = 'cmd';
    }
    $prev_ch = $ch;
    if ( $reread_needed ) { &read_report('refresh'); }
    if ( $refresh_needed || $reread_needed ) { &draw_screen(); }

  }
}

########################################################
## misc.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub audit {
  if ( $audit ) {
    print AUDIT "$$ ";
    print AUDIT @_;
    print AUDIT "\r\n";
  }
}

#------------------------------------------------------------------

sub clean_exit {
  unless( $audit ) {
    &shell_exec("clear", 'no-wait');
  }
  if ( $audit ) {
      close(AUDIT) or die "$!";
  }

  endwin();
  exit();
}

#------------------------------------------------------------------

sub error_exit {
  unless( $audit ) {
    &shell_exec("clear", 'no-wait');
  }

  endwin();
  print STDERR "VIT fatal error: @_\r\n";

  if ( $audit ) {
      close(AUDIT) or die "$!";
  }

  exit(1);
}

#------------------------------------------------------------------

sub debug {
  print AUDIT @_;
  print AUDIT "\r\n";
}

#------------------------------------------------------------------

sub is_printable {
  my $char = $_[0];
  if ( $char =~ /^[0-9]+$/ && $char >= KEY_MIN ) {
    return 0;
  }
  if ( $char =~ /[[:cntrl:]]/ ) {
    return 0;
  }
  return 1;
}

#------------------------------------------------------------------

sub task_version {
  my $request = $_[0];
  my $version;
  open(IN,"task --version 2>&1 |");
  while(<IN>) {
    chop;
    $version = $_;
  }
  close(IN);
  if ( $request eq "major.minor" ) {
    my @v_ = split(/\./,$version);
    return "$v_[0].$v_[1]";
  }
  return $version;
}

#------------------------------------------------------------------

sub task_info {
  my $n = $_[0];
  my $id = $report2taskid[$task_selected_idx];
  &audit("EXEC $task $id info 2>&1");
  open(IN,"task $id info 2>&1 |");
  while(<IN>) {
    chop;
    $_ =~ s/\x1b.*?m//g; # decolorize
    if ( $_ =~ /^$n\s+(.*)/ ) {
      my $v = $1;
      $v =~ s/\s+$//;
      close(IN);
      return $v;
    }
  }
  close(IN);
  return '';
}

#------------------------------------------------------------------

sub ungetstr {
  my $str = $_[0];
  my $err;
  foreach my $ch (reverse split('', $str)) {
    $err = ungetch($ch);
    if ( $err != 0 ) {
      error_exit("Shortcut is too long.");
    }
  }
  return '';
}

########################################################
## prompt.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub prompt_y {
  my $ch = &prompt_chr(@_);
  my $ans = 0;
  if ( $ch eq "y" || $ch eq "Y" ) { $ans = 1; }
  return $ans;
}

#------------------------------------------------------------------

sub prompt_chr {
  my ($prompt) = @_;
  my $ch;
  echo();
  curs_set(1);
  &draw_prompt($prompt);
  $ch = $prompt_win->getch();
  noecho();
  curs_set(0);
  return $ch;
}

#------------------------------------------------------------------

sub prompt_str {
  my ($prompt) = @_;
  $cur_pos = length($prompt);
  my $str = '';
  my $tab_cnt = 0;
  my $tab_match_str = '';
  my $mode;
  my @match_types;
  if ( $prompt =~ /^(:)(.*)/ || $prompt =~ /^(.*?: )(.*)/ || $prompt =~ /^(.*?:)(.*)/ ) {
    $prompt = $1;
    $str = $2;
  }
  my $prompt_len = length($prompt);
  if ( $prompt eq ':' ) {
    $mode = 'cmd';
    @match_types = @report_types;
  } else {
    $mode = lc($prompt);
    $mode =~ s/:.*$//;
    if ( $mode eq 'project' ) {
      @match_types = @project_types;
    }
  }
  curs_set(1);
  &draw_prompt("$prompt$str");
  while (1) {
    my $ch = $prompt_win->getch();
    #debug("TOP str=\"$str\" ch=\"$ch\"");
# tab completion is broken and undocumented
#    if ( $ch eq "\b" || $ch eq "\c?" ) {
#      if ( $str ne '' ) {
#        chop $str;
#        if ( length($str) < length($tab_match_str) ) {
#          chop $tab_match_str;
#        }
#      } else {
#        $tab_match_str = '';
#        $tab_cnt = 0;
#      }
#      &draw_prompt("$prompt$str");
#      next;
#    }
    if ( $ch eq "\cu" ) {
      $str = substr($str, $cur_pos - $prompt_len);
      $tab_match_str = '';
      $tab_cnt = 0;
      $cur_pos = $prompt_len;
      &draw_prompt_cur("$prompt$str");
      next;
    }
    if ( $ch eq "\e" ) {
      noecho();
      curs_set(0);
      return '';
    }
    if ( $ch eq "\n" ) {
      last;
    }
    if ( $ch eq "\t" ) {
      if ( $mode ne 'cmd' && $mode ne 'project' ) {
        beep();
        next;
      }
      $tab_cnt++;
      if ( $tab_cnt == 1 ) { $tab_match_str = $str; }
      if ( $tab_match_str eq '' ) {
        my $idx = $tab_cnt % ($#match_types + 1) - 1;
        $str = $match_types[$idx];
      } else {
        my @matches = (grep(/^$tab_match_str/,@match_types));
        if ( $#matches == -1 ) {
          $tab_cnt = 0;
          beep();
        } else  {
          my $idx = $tab_cnt % ($#matches + 1) - 1;
          $str = $matches[$idx];
        }
      }
      &draw_prompt("$prompt$str");
      next;
    }
# This code was causing problems and was undocumented.
#    if ( $ch eq "\cw" ) {
#      if ( $str eq '' ) {
#        chop $str;
#        beep();
#        next;
#      }
#      if ( $str =~ s/^(.*\s+)\S+\s+$/$1/ ) {
#        &draw_prompt("$prompt$str");
#        next;
#      }
#      if ( $str =~ s/^.*\s+$// ) {
#        &draw_prompt("$prompt$str");
#        next;
#      }
#      if ( $str =~ s/^(.*\s+).*/$1/ ) {
#        &draw_prompt("$prompt$str");
#        next;
#      }
#      $str = "";
#      &draw_prompt("$prompt$str");
#      next;
#    }
    if ( $ch eq KEY_BACKSPACE || $ch eq "\b" || $ch eq "\c?" ) {
      if ( $cur_pos > $prompt_len ) {
        $cur_pos--;
        substr($str, $cur_pos - $prompt_len, 1, "");
        &draw_prompt_cur("$prompt$str");
        next;
      }
    }
    if ( $ch eq KEY_LEFT ) {
      if ( $cur_pos > $prompt_len ) {
        $cur_pos -= 1;
      }
      &draw_prompt_cur("$prompt$str");
      next;
    }
    if ( $ch eq KEY_RIGHT ) {
      if ( $cur_pos < length("$prompt$str") ) {
        $cur_pos += 1;
      }
      &draw_prompt_cur("$prompt$str");
      next;
    }
    if ( ! &is_printable($ch) ) {
      next;
    }
    if ( &is_printable($ch) ) {
      substr($str, $cur_pos - $prompt_len, 0, $ch);
      $cur_pos++;
    }
    &draw_prompt_cur("$prompt$str");
  }
  noecho();
  curs_set(0);
  if ( $mode ne 'project' && $str eq '' ) { beep(); }
  if ( ! $str =~ /^:!/ ) {
    $str =~ s/"/\\"/g;
    $str =~ s/^\s+//;
    $str =~ s/\s+$//;
  }
  return $str;
}

########################################################
## read.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub read_report {
  my ($mode) = @_;
  &inner_read_report($mode);
  if ( $prev_ch eq 'd' && $error_msg =~ /Error: task .*: no matches/ ) {
    # take care of marking last done...
    &inner_read_report('init');
  }
  if ( $current_command eq 'summary' ) {
    &get_num_tasks();
  }
}

#------------------------------------------------------------------

sub inner_read_report {
  my ($mode) = @_;

  my $report_header_idx = 0;
  my $args;
  my @prev_num_tasks = $num_tasks;
  my @prev_report2taskid = @report2taskid;
  my @prev_report_tokens = @report_tokens;
  my @prev_report_lines = @report_lines;
  my @prev_report_colors_fg = @report_colors_fg;
  my @prev_report_colors_bg = @report_colors_bg;
  my @prev_report_attrs = @report_attrs;
  my @prev_report_header_tokens = @report_header_tokens;
  my @prev_report_header_attrs = @report_header_attrs;
  $prev_convergence = $convergence;

  $prev_display_start_idx = $display_start_idx;
  $prev_task_selected_idx = $task_selected_idx;
  @report2taskid = ();
  @report_tokens = ();
  @report_lines = ();
  @report_colors_fg = ();
  @report_colors_bg = ();
  @report_attrs = ();
  @report_header_tokens = ();
  @report_header_attrs = ();
  @project_types = ();
  if ( $mode eq 'init' ) {
    $task_selected_idx = 0;
    $display_start_idx = 0;
  }

  &audit("EXEC $task stat 2>&1");
  open(IN,"$task stat 2>&1 |");
  while(<IN>) {
    chop;
    if ( $_ =~ /^\s*$/ ) { next; }
    $_ =~ s/\x1b.*?m//g;
    if ( $_ =~ /Pending\s+(\d+)/ ) {
      $tasks_pending = $1;
      next;
    }
    if ( $_ =~ /Completed\s+(\d+)/ ) {
      $tasks_completed = $1;
      next;
    }
  }
  close(IN);

  $args = "rc.defaultwidth=$REPORT_COLS rc.defaultheight=$REPORT_LINES burndown";
  &audit("EXEC $task $args 2>&1");
  open(IN,"$task $args 2>&1 |");
  while(<IN>) {
    if ( $_ =~ /Estimated completion: No convergence/ ) {
      $convergence = "no convergence";
      last;
    }
    if ( $_ =~ /Estimated completion: .* \((.*)\)/ ) {
      $convergence = "convergence in $1";
      last;
    }
  }
  close(IN);
  if ( $convergence ne $prev_convergence && $prev_convergence ne '' ) {
    $flash_convergence = 1;
  } else {
    $flash_convergence = 0;
  }

  &audit("EXEC $task projects 2>&1");
  open(IN,"$task projects 2>&1 |");
  while(<IN>) {
    chop;
    if ( $_ =~ /^\s*$/ ) { next; }
    $_ =~ s/\x1b.*?m//g;
    if ( $_ =~ /^\w+ override/ ) { next; }
    if ( $_ =~ /^Project/ ) { next; }
    if ( $_ =~ /^\d+ project/ ) { next; }
    my $p = (split(/\s+/,$_))[0];
    if ( $p eq '(none)' ) { next; }
    push(@project_types, $p);
  }
  close(IN);

  $args = "rc.defaultwidth=$REPORT_COLS rc.defaultheight=0 rc._forcecolor=on $current_command";
  &audit("EXEC $task $args 2> /dev/null");
  open(IN,"$task $args 2> /dev/null |");
  my $i = 0;
  my $prev_id;
  while(<IN>) {
    chop;
    if ( $_ =~ /^\s*$/ ) { next; }
    if ( $_ =~ /^(\d+) tasks?$/ ||
         $_ =~ /^\x1b.*?m(\d+) tasks?\x1b\[0m$/ ||
         $_ =~ /^\d+ tasks?, (\d+) shown$/ ||
         $_ =~ /^\x1b.*?m\d+ tasks?, (\d+) shown\x1b\[0m$/ ) {
      $num_tasks = $1;
      next;
    }
    &parse_report_line($i,$_);
    $_ =~ s/\x1b.*?m//g;
    $report_lines[$i] =  $_;
    if ( $_ =~ /^ID / ) {
      $report_header_idx = $i;
      $i++;
      next;
    }
    if ( $_ =~ /^\s*(\d+) / ) {
      $report2taskid[$i] = $1;
      $taskid2report[$1] = $i;
    } else {
      $report2taskid[$i] = $prev_id;
      $taskid2report[$i] = $prev_id;
    }
    $prev_id = $report2taskid[$i];
    $i++;
  }
  close(IN);

  if ( $#report_tokens > -1 ) {
    @report_header_tokens = @{ $report_tokens[$report_header_idx] };
    @report_header_attrs = @{ $report_attrs[$report_header_idx] };
    splice(@report_tokens,$report_header_idx,1);
    splice(@report_lines,$report_header_idx,1);
    splice(@report_colors_fg,$report_header_idx,1);
    splice(@report_colors_bg,$report_header_idx,1);
    splice(@report_attrs,$report_header_idx,1);
    splice(@report2taskid,$report_header_idx,1);
    if ( $task_selected_idx > $#report_tokens ) {
      $task_selected_idx = $#report_tokens;
    }
  } else {
    $error_msg = "Error: task $current_command: no matches";
    $current_command = $prev_command;
    $display_start_idx = $prev_display_start_idx;
    $task_selected_idx = $prev_task_selected_idx;
    @report_header_tokens = @prev_report_header_tokens;
    @report_header_attrs = @prev_report_header_attrs;
    @report_tokens = @prev_report_tokens;
    @report_lines = @prev_report_lines;
    @report_colors_fg = @prev_report_colors_fg;
    @report_colors_bg = @prev_report_colors_bg;
    @report_attrs = @prev_report_attrs;
    @report2taskid = @prev_report2taskid;
    $convergence = $prev_convergence;
    return;
  }

}

#------------------------------------------------------------------

sub get_num_tasks {
  $num_tasks = 0;
  &audit("EXEC $task projects 2> /dev/null");
  open(IN,"$task projects 2> /dev/null |");
  while(<IN>) {
    if ( $_ =~ /(\d+) task/ ) {
      $num_tasks = $1;
      last;
    }
  }
  close(IN);
}

#------------------------------------------------------------------


########################################################
## search.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub start_search {
  my $ch = $_[0];
  if ( $search_direction == 1 ) {
    $search_pat = '/';
  } else {
    $search_pat = '?';
  }
  &draw_prompt($search_pat);
  echo();
  curs_set(1);
  $cur_pos = 1;
  GETCH: while (1) {
    my $ch = $prompt_win->getch();
    if ( $ch eq "\ch" || $ch eq KEY_BACKSPACE ) {
      if ( $cur_pos > 1 ) {
        $cur_pos--;
        substr($search_pat, $cur_pos, 1, "");
      }
      &draw_prompt_cur($search_pat);
      next GETCH;
    }
    if ( $ch eq "\cu" ) {
      my $search_ch;
      if ( $search_direction == 1 ) {
        $search_ch = '/';
      } else {
        $search_ch = '?';
      }
      $search_pat = $search_ch.substr($search_pat, $cur_pos);
      $cur_pos = 1;
      &draw_prompt_cur($search_pat);
      next GETCH;
    }
    if ( $ch eq "\e" ) {
      &draw_prompt('');
      noecho();
      curs_set(0);
      return;
    }
    if ( $ch eq "\n" ) {
      last GETCH;
    }
    if ( $ch eq KEY_LEFT ) {
      if ( $cur_pos > 1 ) {
        $cur_pos--;
      }
      &draw_prompt_cur($search_pat);
      next GETCH;
    }
    if ( $ch eq KEY_RIGHT ) {
      if ( $cur_pos < length($search_pat) ) {
        $cur_pos++;
      }
      &draw_prompt_cur($search_pat);
      next GETCH;
    }

    if ( &is_printable($ch) ) {
      substr($search_pat, $cur_pos, 0, $ch);
      $cur_pos = $cur_pos + 1;
    }
    &draw_prompt_cur($search_pat);
  }
  noecho();
  curs_set(0);
  $search_pat = substr($search_pat, 1);
  if ( $search_pat eq '' ) {
    $search_pat = '';
    &draw_prompt('');
    beep();
    return;
  }
  $refresh_needed = 1;
  if ( ! &do_search('n') ) {
    return;
  }
  $input_mode = 'search';
  return;
}

#------------------------------------------------------------------

sub do_search {
  my $ch = $_[0];
  my $rtn = &do_inner_search($ch);
  if ( $rtn == 1 ) {
    if ( $task_selected_idx - $display_start_idx >= $REPORT_LINES ) {
      $display_start_idx = $task_selected_idx - $REPORT_LINES + 1;
    } elsif ( $task_selected_idx < $display_start_idx ) {
      $display_start_idx = $task_selected_idx;
    }
    return 1;
  } else {
    $error_msg = "Pattern not found: $search_pat";
    $search_pat = '';
    beep();
    return 0;
  }
  return 0;
}

#------------------------------------------------------------------

sub do_inner_search {
  my $ch = $_[0];
  if ( $search_direction == 1 && $ch eq 'n' || $search_direction == 0 && $ch eq 'N' ) {
    for ( my $i = $task_selected_idx + 1; $i <= $#report_lines; $i++ ) {
      if ( $report_lines[$i] =~ /$search_pat/i ) {
         $task_selected_idx = $i;
         return 1;
      }
    }
    &draw_prompt('Search hit BOTTOM, continuing at TOP');
    usleep($error_delay);
    for ( my $i = 0; $i < $task_selected_idx; $i++ ) {
      if ( $report_lines[$i] =~ /$search_pat/i ) {
        $task_selected_idx = $i;
        return 1;
      }
    }
    if ( $report_lines[$task_selected_idx] =~ /$search_pat/i ) { return 1; }
    return 0;
  }
  if ( $search_direction == 1 && $ch eq 'N' || $search_direction == 0 && $ch eq 'n' ) {
    for ( my $i = $task_selected_idx - 1; $i >= 0; $i-- ) {
      if ( $report_lines[$i] =~ /$search_pat/i ) {
         $task_selected_idx = $i;
        return 1;
      }
    }
    &draw_prompt('Search hit TOP, continuing at BOTTOM');
    usleep($error_delay);
    for ( my $i = $#report_lines; $i > $task_selected_idx; $i-- ) {
      if ( $report_lines[$i] =~ /$search_pat/i ) {
        $task_selected_idx = $i;
        return 1;
      }
    }
    if ( $report_lines[$task_selected_idx] =~ /$search_pat/i ) { return 1; }
    return 0;
  }
  return -1;
}

########################################################
## screen.pl...
# Copyright 2012 - 2013, Steve Rader
# Copyright 2013 - 2014, Scott Kostyshak

sub draw_screen {
  my ($x,$t,$fg,$bg,$cp,$str);
  my $line = 0;

  $header_win->attron(COLOR_PAIR($COLOR_HEADER));
  &set_attron($header_win,$header_attrs);
  CASE: {
    if ( $current_command eq 'summary' && $num_projects == 1 ) {
      $str = '1 project';
      last CASE;
    }
    if ( $current_command eq 'summary' ) {
      $str = "$num_projects projects";
      last CASE;
    }
    if ( $num_tasks == 1 ) {
      $str = '1 task shown';
      last CASE;
    }
    $str = "$num_tasks tasks shown";
  }
  &draw_header_line(0,"task $current_command",$str);
  CASE: {
    if ( $current_command eq 'summary' && $num_tasks == 1 ) {
      $str = '1 task';
      last CASE;
    }
    if ( $current_command eq 'summary' ) {
      $str = "$num_tasks tasks";
      last CASE;
    }
    if ( $tasks_completed == 1 ) {
      $str = '1 task completed';
      last CASE;
    }
    $str = "$tasks_completed tasks completed";
  }
  &draw_header_line(1,$convergence,$str);
  &set_attroff($header_win,$header_attrs);
  $header_win->attroff(COLOR_PAIR($COLOR_HEADER));

  $header_win->attron(COLOR_PAIR($COLOR_REPORT_HEADER));
  $x = 1;
  for $t (0 .. $#report_header_tokens) {
    &set_attron($header_win,$report_header_attrs[$t]);
    $header_win->addstr(2,$x,$report_header_tokens[$t]);
    &set_attroff($header_win,$report_header_attrs[$t]);
    $x += length($report_header_tokens[$t]);
  }
  $str = ' ' x ($REPORT_COLS - $x + 1);
  &set_attron($header_win,$report_header_attrs[$#report_header_attrs]);
  $header_win->addstr(2,$x,$str);
  &set_attroff($header_win,$report_header_attrs[$#report_header_attrs]);
  $header_win->attroff(COLOR_PAIR($COLOR_REPORT_HEADER));
  $header_win->refresh();

  #debug("DRAW lines=$REPORT_LINES start=$display_start_idx cur=$task_selected_idx");
  for my $i ($display_start_idx .. ($display_start_idx+$REPORT_LINES-1)) {
    $cp = 0;
    if ( $i > $#report_tokens ) {
      $str = '~' . ' ' x ($COLS-2);
      $report_win->attron(COLOR_PAIR($COLOR_EMPTY_LINE));
      $report_win->attron(A_BOLD);
      $report_win->addstr($line,0,$str);
      $report_win->attroff(A_BOLD);
      $report_win->attroff(COLOR_PAIR($COLOR_EMPTY_LINE));
      $line++;
      next;
    }
    &draw_report_line($i,$line,'with-selection');
    $line++;
  }
  $report_win->refresh();
  if ( $display_start_idx == 0 ) {
    $cursor_position = 'Top';
  } elsif ( $display_start_idx + $REPORT_LINES >= $#report_tokens + 1 ) {
    $cursor_position = 'Bot';
  } else {
    $cursor_position = int($task_selected_idx/$#report_tokens*100) . '%';
  }
  CASE: {
    if ( $error_msg ne '' ) {
      &draw_error_msg();
      last CASE;
    }
    if ( $feedback_msg ne '' ) {
      &draw_feedback_msg();
      last CASE;
    }
    if ( $input_mode eq 'search' && $search_direction == 1 ) {
      &draw_prompt_line("/$search_pat");
      last CASE;
    }
    if ( $input_mode eq 'search' && $search_direction == 0 ) {
      &draw_prompt_line("?$search_pat");
      last CASE;
    }
    &draw_prompt_line('');
  }
  if ( $flash_convergence ) {
    &flash_convergence();
    $flash_convergence = 0;
    $prev_convergence = $convergence;
  }

}


########################################################
## vitrc.pl...
# Copyright 2013 - 2014, Scott Kostyshak

sub parse_vitrc {
  my $vitrc = glob("~/.vitrc");
  if ( open(IN,"<$vitrc") ) {
    while (<IN>) {
      chop;
      my $parse_error = "ERROR: incorrect key bind line in .vitrc:\n $_\n";
      if ( $_ =~ s/^map// ) {
        my($scut, $cmd) = split(/=/, $_, 2);

        my $skey;
        if ($scut =~ s/ ([^ ]+)$//) {
          $skey = $1;
        }
        else {
          print STDERR "$parse_error";
          exit(1);
        }

        $skey = &replace_keycodes("$skey");
        $cmd = &replace_keycodes("$cmd");

        $skey = eval "\"$skey\"";

        $shortcuts{$skey} = $cmd;
      }
    }
    close(IN);
  }
}

#------------------------------------------------------------------

sub replace_keycodes {
  my $str_ = $_[0];

  $str_ =~ s/<F1>/KEY_F(1)/e;
  $str_ =~ s/<F2>/KEY_F(2)/e;
  $str_ =~ s/<F3>/KEY_F(3)/e;
  $str_ =~ s/<F4>/KEY_F(4)/e;
  $str_ =~ s/<F5>/KEY_F(5)/e;
  $str_ =~ s/<F6>/KEY_F(6)/e;
  $str_ =~ s/<F7>/KEY_F(7)/e;
  $str_ =~ s/<F8>/KEY_F(8)/e;
  $str_ =~ s/<F9>/KEY_F(9)/e;
  $str_ =~ s/<F10>/KEY_F(10)/e;
  $str_ =~ s/<F11>/KEY_F(11)/e;
  $str_ =~ s/<F12>/KEY_F(12)/e;

  $str_ =~ s/<Home>/KEY_HOME/e;
  $str_ =~ s/<End>/KEY_END/e;

  $str_ =~ s/<Insert>/KEY_IC/e;
  $str_ =~ s/<Del>/KEY_DC/e;

  $str_ =~ s/<PageUp>/KEY_PPAGE/e;
  $str_ =~ s/<PageDown>/KEY_NPAGE/e;

  $str_ =~ s/<Up>/KEY_UP/e;
  $str_ =~ s/<Down>/KEY_DOWN/e;
  $str_ =~ s/<Right>/KEY_RIGHT/e;
  $str_ =~ s/<Left>/KEY_LEFT/e;

  $str_ =~ s/<Backspace>/KEY_BACKSPACE/e;

  # We don't evaluate these ones (no 'e').
  $str_ =~ s/<Space>/ /;
  $str_ =~ s/<Tab>/\t/;
  $str_ =~ s/<Return>/\n/;
  $str_ =~ s/<Esc>/\e/;

  return $str_;
}


