#!/bin/bash

#==============================================================================
# cli-package-installer
#
# A cli wrapper around apt-get and apt-cache to provide some of the
# functionality of Synaptic on the command line.
#
# (C) 2017 Paul Banham <antiX@operamail.com>
# License: GPLv3 or later
#==============================================================================

MAIN_TITLE="antiX CLI Package Installer"

        ME=${0##*/}
    MY_DIR=$(dirname "$(readlink -f $0)")
MY_LIB_DIR=$(readlink -f "$MY_DIR/../cli-shell-utils")
   LIB_DIR="/usr/local/lib/cli-shell-utils"

  LIB_PATH="$MY_LIB_DIR:$LIB_DIR"
      PATH="$MY_LIB_DIR/bin:$LIB_DIR/bin:$PATH"
 SHELL_LIB="cli-shell-utils.bash"

    LIST_DIR=/var/lib/apt/lists
  SOURCE_DIR=/etc/apt/sources.list.d

  SUGGESTED_SRC=/usr/local/share/$ME/suggested.packages

  THE_LOG_FILE=/var/log/$ME.log

  # For now so it stays on the usb stick
  THE_LOG_FILE=./$ME.log

AUTO_UPDATE_INTERVAL="5"  # Days

    WORK_DIR=/tmp/$ME

           LOCK_FILE=/run/lock/$ME.lock
    FOUND_MATCH_FILE=$WORK_DIR/found-match
         SEARCH_FILE=$WORK_DIR/SEARCH-RESULTS
             DB_FILE=$WORK_DIR/all.list
      INSTALLED_FILE=$WORK_DIR/installed.list
      SUGGESTED_FILE=$WORK_DIR/suggested.list

MARK_CNT=0
: ${EDITOR:=name}


 BAR_80="#=============================================================================="
SBAR_80="#------------------------------------------------------------------------------"

start_testing_mode() {

    TEST_DIR=Private/apt
    SOURCE_DIR=$TEST_DIR/sources.d
    LIST_DIR=$TEST_DIR/lists

    THE_LOG_FILE=$ME.log
    LOG_FILE=$ME.log
    #PRETEND_MODE=true

  SUGGESTED_SRC=share/suggested.packages

    ARCH="i386"

    #- perform_search() {
    #-     local str=$1

    #-     grep -Eh "$str" $TEST_DIR/full.list
    #- }

    list_installed() {
        cat $TEST_DIR/installed.list
    }

    list_all() {
        cat $TEST_DIR/full.list
    }

    list_depends() {
        local regex=$(echo "$*" | tr ' ' '|')
        egrep "^($regex):" $TEST_DIR/all.depend | cut -d" " --complement -f1 | tr ' ' '\n' | sort -u
    }

    package_exists() {
        local pack=$1
        [ -n "${pack##p*}" ]
        return $?
    }
}

#------------------------------------------------------------------------------
# The main program.  Should add command line options for color, simple/expert
# and so on.   Could that be a submenu???
#------------------------------------------------------------------------------
main() {
    testing || need_root

    # For now (at least) add menus to the log file
    VERBOSE_SELECT=true

    # Used in my_select() to return instead of exit on 'q'
    BACK_TO_MAIN="go back to main menu"

    local UPGRADE_CNT

    case $(uname -m) in
          i686) ARCH="i386"  ;;
        x86_64) ARCH="amd64" ;;
    esac

    testing && start_testing_mode

    trap on_exit EXIT

    testing || do_flock
    #shout $"Starting %s" "$ME"
    start_log "$*"

    find_man_page

    mkdir -p $WORK_DIR || fatal "Could not create directory %s" "$WORK_DIR"
    testing || mount_work_dir

    #msg "Checking to see if an %s is needed ..." "$(pq apt-get update)"

    # Don't run check_for_updates or generate_database_files automatically
    local JUST_UPDATE=true

    # FIXME!!
    run_outer check_list_files || exit

    check_for_upgrades
    generate_database_files

    # Ok, you can run check_for_updates or generate_database_file automatically now
    JUST_UPDATE=

    #shout_title "$MAIN_MENU"
    #do_search

    while true; do
        do_main_menu
    done
}

#------------------------------------------------------------------------------
# NOTE: do_[....]_menu() is for presenting the menu and dealing with the choice
# made by the user.  This one is for the main menu (duh).
#------------------------------------------------------------------------------
do_main_menu() {

    msg
    shout_title "$MAIN_TITLE"

    suggest_warning
    upgrade_warning

    local title=$"Main Menu"
    local ans

    run_outer my_select 'ans' "$title" "$(main_menu)"

    case $ans in
              search) do_search       ;;
              update) do_update       ;;
             upgrade) do_upgrade_menu ;;
                edit) do_source_menu  ;;
           suggested) do_suggested    ;;
         view-marked) do_view_marked  ;;
                quit) exit            ;;

        *)  warn "The '%s' feature isn't implemented yet" "$(pqw $ans)"
            press_enter ;;
    esac
}

#------------------------------------------------------------------------------
# Note: xxxx_menu() is to to generate the text of the menu to be sent to the
# my_select() routine.  This one is for the main menu.
#------------------------------------------------------------------------------
main_menu() {
    local upgrade_lab
    : ${UPGRADE_CNT:=$(count_upgrades)}

    menu_printf search    "Search for packages to mark or install"
    add_suggest_entry
    menu_printf kernels   "Search for kernels or antiX kernels"
    menu_printf update    "Update package index"
    menu_printf edit      "Edit the repo source files"
    add_upgrade_entry
    add_mark_entry
    menu_printf quit      "Quit"
}

#------------------------------------------------------------------------------
# Gently warn if the suggestions are all already installed
#------------------------------------------------------------------------------
suggest_warning() {
    local file=$SUGGESTED_FILE
    test -r $file || return
    local tot=$(egrep --count "^[MIa-z0-9]" $file)
    local cnt=$(egrep -v "^M?I" $file | egrep --count "^[MIa-z0-9]")
    [ $cnt -gt 0 ] && return
    [ $tot -eq 0 ] && return
    msg "All %s suggested packages have been installed" "$tot"
}

#------------------------------------------------------------------------------
# Gently warn if there are no pending upgrades
#------------------------------------------------------------------------------
upgrade_warning() {
    [ $UPGRADE_CNT -gt 0 ] && return
    msg "(no pending upgrades)"
}

#------------------------------------------------------------------------------
# Add the suggestions entry if there are any left to install
#------------------------------------------------------------------------------
add_suggest_entry() {
    local file=$SUGGESTED_FILE
    test -r $file || return
    local cnt=$(egrep "^[MIa-z0-9]" $file | egrep --count -v "^M?I")
    [ $cnt -gt 0 ] || return
    menu_printf suggested "View a list of %s suggested uninstalled packages" "$(nq $cnt)"
}

#------------------------------------------------------------------------------
# Add an "upgrade" entry if it is appropriate
#------------------------------------------------------------------------------
add_upgrade_entry() {
    local cnt=${1:-$UPGRADE_CNT}
    [ $cnt -gt 0 ] || return
    menu_printf_plural  upgrade $cnt "View or perform %s upgrade now" "View or perform %s upgrades now"
}

#------------------------------------------------------------------------------
# Menu elements for doing ao system upgrade
#------------------------------------------------------------------------------
upgrade_menu() {
    local cnt=${1:-$UPGRADE_CNT}

    menu_printf_plural upgrade $cnt "Upgrade %s package now" "Upgrade %s packages now"
    menu_printf        view    "View list of packages to be upgraded"
    menu_printf        ignore  "Ignore these upgrades for now"
}

#------------------------------------------------------------------------------
# Add a entry linking to mark menu if appropriate
#------------------------------------------------------------------------------
add_mark_entry() {
    local old_cnt=$MARK_CNT
    MARK_CNT=$(grep --count "^M" $DB_FILE)
    local cnt=$MARK_CNT
    [ $cnt -ne $old_cnt ] && warn "Mark count adjusted from %s to %s" $old_cnt $cnt

    [ $cnt -gt 0 ] || return

    menu_printf_plural view-marked "$cnt" "View or install %s marked package"  "View or install %s marked packages"
}

#------------------------------------------------------------------------------
# This is the main event.  I'm trying to make it as easy to use as possible.
# Can still be improved.  That is why it is not so neat and tidy.  The loops
# allow for easy flow control with 'return', 'continue', and 'break' I can
# easily go to 3 different places (usually: main, new search, repeat same
# search).
#------------------------------------------------------------------------------
do_source_menu() {

    shout_subtitle "Edit-Source Menu"

    while true; do
        local ans  title="Please select a source file to edit"
        my_select ans "$title" "$(source_menu)"
        case $ans in
            quit) break ;;
        esac
        local file=$ans
        if ! test -e "$file"; then
            warn "Could not find file %s to edit" "$(pqw $file)"
            continue
        fi
        nano "$file"
    done
    echo
    check_list_files || exit
}

#------------------------------------------------------------------------------
# Create a menu for editing source files
#------------------------------------------------------------------------------
source_menu() {

    local file
    for file in $SOURCE_DIR/*.list; do
        test -e "$file" || continue
        menu_printf "$file" "$(basename "$file")"
    done
    menu_printf quit "Back to Main"
}

#------------------------------------------------------------------------------
# Display a package name and description in a nice way and indicate if the
# package is installed and/or marked
#------------------------------------------------------------------------------
show_package() {
    local match=$(echo "$1" | tr -s ' ')  width=$(screen_width)  attrib
    local max_width=$((width - 5))
    [ -z "$match" ] && return
    if [ -z "${match##M*}" ]; then
        attrib="$attrib${attrib:+ }(${mark_co}marked$nc_co)"
        match=${match#M}
    fi
    if [ -z "${match##I*}" ]; then
        attrib="$attrib${attrib:+ }(${inst_co}installed$nc_co)"
        match=${match#I}
    fi
    match=${match# }

    arrow_msg "$hi_co%s" "${match:0:$max_width}"
    [ "$attrib" ] && echo "$attrib"
}

#------------------------------------------------------------------------------
# This is the core routine of this program.  I try to make good use of the
# limited flow control in Shell, "break", "continue", "return" which is why
# this routine has too many nested loops and not enough modularized calls
# to smaller functions.
#------------------------------------------------------------------------------
do_search() {

    local ans
    # This loop is for doing new searches.  Maybe there is a better way in
    # order to avoid so much nesting

    # LOOP: new search
    while true; do
        shout
        shout_subtitle "Search for packages"

        local all_str
        get_glob_string all_str

        case $all_str in
            [qQ]) return ;;
        esac

        [ -z "$all_str" ] && return


        # This loop is to repeat viewing of the same search results.  In here
        # a 'break' gets you to a new search and a 'continue' will reshow the
        # same results

        # LOOP: repeat same search
        while true; do

            local desc_str=${all_str#^}
            desc_str=${desc_str%$}
            local name_str="^([MI]+ )?[^ ]*$desc_str"
            local lead_str="^([MI]+ )?$desc_str"
            local exact_str="$lead_str "

            local exact_cnt=   lead_cnt=    name_cnt=    desc_cnt=
            local exact_match= lead_match=  name_match=  desc_match=

            start_timer

            local TOTAL_MATCHES=0  FOUND_MATCH=
            count_matches  exact "exact name match"           "exact name matches"
            count_matches  lead  "leading name match"         "leading name matches"
            count_matches  name  "any name match"             "any name matches"
            count_matches  desc  "name or description match"  "name or description matches"

            echo
            msg_elapsed_t "searches"
            echo

            if [ $TOTAL_MATCHES -eq 0 ]; then
                warn "No matches were found"
                YES_no "Do you want to try a different search?" && break
                return
            fi

            # LOOP: package info
            while true; do

                # Need to use a file because the commands below are in a subshell
                rm -f $FOUND_MATCH_FILE
                local result_menu=$(
                    add_to_result_menu exact "exact"
                    add_to_result_menu lead  "leading"
                    add_to_result_menu name  "total name"
                    add_to_result_menu desc  "description"
                    menu_printf  'search'    "Do a different search"
                )


                local found_match= act2=
                read found_match 2>/dev/null <$FOUND_MATCH_FILE

                local menu_result the_string
                shout "Search Result Menu"
                local result_title="Please select an action"
                my_select 'menu_result' "$result_title" "$result_menu"
                [ -z "$menu_result" ] && continue

                local action=${menu_result#*-}
                local search_type=${menu_result%%-*}

                case $action in
                      view) ;;
                      info)      package_info "$found_match" ; continue ;;
                      mark)      mark_package "$found_match"            ;;
                    unmark)    unmark_package "$found_match"            ;;
                   install)   install_package "$found_match" ; return   ;; # Should we really return?
                 uninstall) uninstall_package "$found_match" ; return   ;;

                    search) act2=break  ;;
                      quit) return      ;;
                         *) internal_error "search result menu" "$action" ;;
                esac
                break
            done

            case $act2 in
                break) break ;;
            esac

            if [ "$action" != 'view' ]; then
                my_select ans "What next?" "$(search_again_menu)"
                case $ans in
                     view-marked) do_view_marked ; return ;;
                          search) break                   ;;
                          review) continue                ;;
                            quit) return                  ;;
                    *) internal_error "search again menu" "$ans" ;;
                esac
            fi

            eval the_string=\$${search_type}_str

            local args=
            [ "$search_type" = 'desc' ] && args="--color=always"
            perform_search "$the_string" $args > $SEARCH_FILE

            local cnt=$(cat $SEARCH_FILE | wc -l)

            local height=$(screen_height)
            if [ $cnt -lt $((height - 5)) ]; then
                cat $SEARCH_FILE | number_results | less -EXRS
            else
                #local blanks=$((height - 5))
                local blanks=5
                (local i; for i in $(seq 1 $blanks); do echo; done)  > $SEARCH_FILE.nl
                cat $SEARCH_FILE | number_results                   >> $SEARCH_FILE.nl

                msg "%s packages were found." "$(nq $cnt)"
                msg
                questn "In the next step you will been shown a list of packages that you can scroll"
                questn "Use %s, %s, %s, and %s to scroll the list" \
                    "<$(bqq up-arrow)>" "<$(bqq down-arrow)>" "<$(bqq page-up)>" "<$(bqq page-down)>"

                questn "Position the package you want near the bottom and then press %s to continue" \
                    "$(bqq q)"
                msg

                questn "Press %s to see the list, %s to go the main menu,"  "<$(bqq Enter)>" "'$(bqq q)'"
                questn "%s to repeat same search, %s to do another search"  "'$(bqq r)'"     "'$(bqq s)'"
                quest "> "

                read -n1 ans
                case $ans in
                    [qQ]*) return    ;;
                    [sS]*) break     ;;
                    [rR]*) continue  ;;
                esac

                # Never start with blank lines under the last line
                local offset=$((blanks + 1))
                [ $((offset + height)) -gt $((blanks + cnt + 2)) ] && offset=$((cnt + blanks - height + 2))

                less -RXSm +$offset --shift=.20 $SEARCH_FILE.nl

            fi

            # This loop is to repeat the 'number' input if needed.
            # The action variable helps us to figure out what to do in the surrounding loop
            local action enter=$(bqq "Enter")

            # LOOP: enter a number
            while true; do

                questn "Press %s to skip picking a package" "<$enter>"
                questn "Or enter the number of the package you want to mark or install"
                questn "%s go to main menu, %s new search, %s see the results again" \
                    "$(bqq q)<$enter>" "$(bqq s)<$enter>" "$(bqq r)<$enter>"
                quest "> "

                local number=
                read number

                case $number in
                       "") break ;;
                    [qQ]*) return   ;;
                    [rR]*) action=continue ; break ;;
                    [sS]*) action=break ; break ;;
                    [1-9]|[1-9][0-9]|[1-9][0-9][0-9]) ;;
                                [1-9][0-9][0-9][0-9]) ;;
                           [1-9][0-9][0-9][0-9][0-9]) ;;
                      [1-9][0-9][0-9][0-9][0-9][0-9]) ;;
                    *) warn "Invalid input.  Please try again" ; continue ;;
                esac

                if [ "$number" -gt $cnt ]; then
                    warn "That number was out of range, please try again"
                    continue
                fi

                break
            done

            case $action in
                continue) continue ;;
                   break) break    ;;
            esac

            local full_package=
            if [ "$number" ]; then
                full_package=$(nl $SEARCH_FILE | sed -n -r "s/^\s*$number\s+//p")
                if [ -z "$full_package" ]; then
                    warn "Could not find package number %s" "$number"
                    break
                fi
                msg "Selected package %s" "$(nq $number)"
                show_package "$full_package"
            fi

            # LOOP: package info
            while true; do
                action=
                local menu=$(
                    mark_or_install_menu "$full_package"
                    mark_all_menu        $SEARCH_FILE
                    menu_printf repeat   $"Repeat the same search"
                    menu_printf search   $"Search again"
                    menu_printf quit     $"Back to main menu"
                )

                my_select 'ans' "Please select an action" "$menu"

                case $ans in
                      info)      package_info "$full_package" ; continue ;;
                      mark)      mark_package "$full_package"          ;;
                    unmark)    unmark_package "$full_package"          ;;
                   install)   install_package "$full_package" ; return ;;
                 uninstall) uninstall_package "$full_package" ; return ;;
                  mark-all)     mark_all_file $SEARCH_FILE             ;;
                unmark-all)   unmark_all_file $SEARCH_FILE             ;;
                    repeat) action=continue                            ;;
                    search) action=break                               ;;
                      quit) return                                     ;;
                         *) internal_error "2nd search result menu" "$ans" ;;
                esac
                break

            done

            case $action in
                continue) continue ;;
                   break) break    ;;
            esac

            my_select 'ans' "Now what?" "$(search_again_menu)"

            case $ans in
            view-marked) do_view_marked ; return        ;;
                 search) break                          ;;
                 review) continue                       ;;
                   quit) return                         ;;
                      *) internal_error "install menu" "$ans" ;;
            esac
        done
    done
}

#------------------------------------------------------------------------------
# Used twice within do_search().  This is the menu you see after you've marked
# or unmarked a package.
#------------------------------------------------------------------------------
search_again_menu() {
    menu_printf search  "Do a new search"
    menu_printf review  "See search results again"
    add_mark_entry
    menu_printf quit    "Return to main menu"
}

#------------------------------------------------------------------------------
# Add parens and colorize each line to make it more clear what is marked and
# what installed and what is both.
#------------------------------------------------------------------------------
color_results() {
    # The first 2nd & 3rd expressions are for "undoing" grep highlighting

    sed -r  -e "/^M/ s/(\x1B\[m\x1B\[K)/\1$mark_co/g" \
            -e "/^I/ s/(\x1B\[m\x1B\[K)/\1$inst_co/g" \
            -e "s/^(MI)(.*)/$inst_co$mark_co(MI)\2$nc_co/" \
            -e "s/^(M)(.*)/$mark_co(\1)\2$nc_co/"  \
            -e "s/^(I)(.*)/$inst_co(\1)\2$nc_co/"
}


#------------------------------------------------------------------------------
# Add numbers and colorize a list of packages
#------------------------------------------------------------------------------
number_suggestions() {
    color_suggestions | my_nl | color_numbers | color_headers
}

#------------------------------------------------------------------------------
# Colorize the suggestions list.  This is similar to but not identical to
# color_results()
#------------------------------------------------------------------------------
color_suggestions() {
    # The first 2nd & 3rd expressions are for "undoing" grep highlighting

    sed -r  -e "s/(\*+)/$bold_co\1\x1B[m\x1B[K/" \
            -e "/^M/ s/(\x1B\[m\x1B\[K)/\1$mark_co/g" \
            -e "/^I/ s/(\x1B\[m\x1B\[K)/\1$inst_co/g" \
            -e "s/^([MI] [^ ]+)    /\1/" \
            -e "s/^(MI [^ ]+)     /\1/" \
            -e "s/^(MI)(.*)/$inst_co$mark_co(MI)\2$nc_co/" \
            -e "s/^(M)(.*)/$mark_co(\1)\2$nc_co/"  \
            -e "s/^(I)(.*)/$inst_co(\1)\2$nc_co/"
}

color_numbers() {
    sed -r "s/^(\s*[0-9]+)\s/$m_co\1$quest_co)$nc_co /"
}

#------------------------------------------------------------------------------
# We want to do this *after* my_nl() so my_nl() can easily ignore lines that
# start with "#".
#------------------------------------------------------------------------------
color_headers() {
    sed -r -e "s/^(#-+)/$bold_co\1$nc_co/" \
           -e "s/^(# )(.*)/$bold_co\1$quest_co\2$nc_co/"
}

#------------------------------------------------------------------------------
# Use the "nl" program to prefix each line with a number and then use sed to
# colorize the the numbers
#------------------------------------------------------------------------------
number_results() {
        color_results | nl | sed -r "s/^(\s*[0-9]+)\s/$m_co\1$quest_co)$nc_co /"
}

#------------------------------------------------------------------------------
# Present a menu for how to deal with a selected package that is already
# installed.
#------------------------------------------------------------------------------
uninstall_package() {
    local ans  full=$1  pack=$(package_name "$1")

    shout_subtitle "Remove / Purge Menu"

    local ans
    show_package "$full"
    my_select ans "Please choose an action for this package" "$(uninstall_menu)"
    case $ans in
            remove) apt_get_cmd remove  $pack ;;
             purge) apt_get_cmd purge   $pack ;;
           install) apt_get_cmd install $pack ;;
         reinstall) apt_get_cmd --reinstall install $pack ;;
              quit) return  ;;
              *) internal_error "uninstall menu" "$ans" ;;
    esac
    update_package_status
}

#------------------------------------------------------------------------------
# Choices for dealing with an installed package
#------------------------------------------------------------------------------
uninstall_menu() {
    menu_printf remove    "Un-install package [remove package but not its config files]"
    menu_printf purge     "Purge package [remove package AND its config files]"
    menu_printf reinstall "Reinstall package [even if it is up to date]"
    menu_printf install   "Install package [only if it is not up to date]"
}

#------------------------------------------------------------------------------
# A "simple" wrapper around "apt-get".
#------------------------------------------------------------------------------
apt_get_cmd() {
    local label=${1#--}  cmd="apt-get $*"
    local fold_width=$(($(screen_width) - 5))
    printf "=> %s\n" "$cmd" | fold -w $fold_width -s | tee -a $LOG_FILE

    testing && return 0

    start_timer
    $cmd 2>&1                       #| tee -a $LOG_FILE
    local ret=${PIPESTATUS[0]}
    msg_elapsed_t "$label"

    [ $ret -eq 0 ] && return 0
    warn "The %s command failed" "$(pqw apt-get)"
    return 1
}

#------------------------------------------------------------------------------
# This gets the search string and converts glob "*" and "?" into regexes.  This
# conversion may not be a good idea.
#------------------------------------------------------------------------------
get_glob_string() {
    local var_nam=$1  enter=$(bqq "Enter")
    local line1="Please enter the term you want to search for."
    local line2=$(printf "Standard globbing wildcards %s and %s work." "'$(bqq '*')'" "'$(bqq '?')'")
    local line3=$(printf "Use an empty string or %s to return to main menu." "'$(bqq q)'<$enter>")

    title=$(printf "%s\n%s\n\n%s\n" "$line1" "$line2" "$line3")

    local input prompt=$(quest "> ")
    quest "$title"
    echo -en "\n$prompt"
    read -r input

    # Convert '*' and '?' to their regular expression equivalents
    # First protect "." in the original string
    input=${input//./\\.}
    input=${input//\?/.}
    input=${input//\*/.*}

    eval $var_nam=\$input
}

#------------------------------------------------------------------------------
# Count and display the number of matches for various search criteria.
#------------------------------------------------------------------------------
count_matches() {
    local prefix=$1  lab1=$2  lab2=$3  found="Found"
    eval local str=\$${prefix}_str

    local cnt=$(perform_search "$str" | wc -l)

    eval ${prefix}_cnt=\$cnt

    local fmt="  %s $num_co%5s$m_co %s"
    TOTAL_MATCHES=$((TOTAL_MATCHES + $cnt))
    if [ $cnt -eq 1 ]; then
        local match=$(perform_search "$str")
        # FIXME: count lines and warn/error on -ne 1
        # Also make sure length is not zero
        eval ${prefix}_match=\$match

        msg "$fmt"  "$found"  "$cnt"  "$lab1"
        if [ -z "$FOUND_MATCH" ]; then
            FOUND_MATCH=true
            show_package "$match"
        fi
    else
        msg "$fmt" "$found" "$cnt" "$lab2"
    fi
}

#------------------------------------------------------------------------------
# Count the number of ***'ed packages
#------------------------------------------------------------------------------
count_star_matches() {
    local prefix=$1  lab1=$2  lab2=$3  stars=$4  found="Found"
    eval local str=\$${prefix}_str

    local cnt=$(star_search "$str" --count)

    eval ${prefix}_cnt=\$cnt

    local fmt="  %s $num_co%5s$m_co %s"
    TOTAL_MATCHES=$((TOTAL_MATCHES + $cnt))
    local lab=$lab2
    [ $cnt -eq 1 ] && lab=$lab1

    return

    if [ $# -lt 4 ]; then
        msg "  %s $num_co%3s$m_co %s" "$found" "$cnt" "$lab"
    else
        msg "  %s $num_co%3s$m_co %s %s" "$found" "$cnt" "$(starq "$stars")" "$lab"
    fi
}

starq() { printf "%5s" "$1" | sed -r "s/(\*+)/$bold_co\1$m_co/" ; }


#------------------------------------------------------------------------------
# Uses information from in count_matches() to create menu entries for each
# type of search result.  If there are no matches, do nothing, the first time
# there is only one match then offer to install/uninstall or mark/unmark that
# package.  If there is more than one match then offer to let the user view
# those matches.
#------------------------------------------------------------------------------
add_to_result_menu() {
    local prefix=$1  lab=$2
    local cnt match
    eval cnt=\$${prefix}_cnt
    case $cnt in
        0)  return ;;
        1)  test -e $FOUND_MATCH_FILE && return

            eval match=\$${prefix}_match
            local package=$(package_name "$match")
            echo "$match" > $FOUND_MATCH_FILE

            mark_or_install_menu "$match" "$prefix-"

            ;;

        *)  menu_printf "$prefix-view" "View %s %s results" "$(nq $cnt)"  "$lab"  ;;
    esac
}

#------------------------------------------------------------------------------
# Add a entry for showing only the ***+ packages
#------------------------------------------------------------------------------
add_to_star_menu() {
    local prefix=$1  view=$2  lab=$3  stars=$4
    local cnt match
    eval cnt=\$${prefix}_cnt
    if [ $# -lt 4 ]; then
        menu_printf "$prefix-view" "%s $num_co%3s$m_co %s" "$view" "$cnt"  "$lab"
    else
        menu_printf "$prefix-view" "%s $num_co%3s$m_co %s %s" "$view" "$cnt" "$(starq "$stars")"  "$lab"
    fi
}

#------------------------------------------------------------------------------
# This generates two menu entries for marking/unmarking a package and for
# isntalling/uninstalling it.
#------------------------------------------------------------------------------
mark_or_install_menu() {
    local match=$1  prefix=$2
    [ -z "$match" ] && return

    local package=$(package_name "$match")
    local pq_pack=$(pq $package)

    menu_printf "${prefix}info" "Show more information about package %s" "$pq_pack"

    if is_installed "$match"; then
        menu_printf "${prefix}uninstall" "Uninstall package %s" "$pq_pack"
        return
    fi

    if is_marked "$match"; then
        menu_printf "${prefix}unmark"   "Unmark package %s"  "$pq_pack"
    else
        menu_printf "${prefix}mark"     "Mark package %s"    "$pq_pack"
    fi

    menu_printf "${prefix}install"   "Install package %s"   "$pq_pack"
}

#------------------------------------------------------------------------------
# Works on a STRING.  Sees if that string starts with "M"
#------------------------------------------------------------------------------
is_marked() {
    local match=$1
    [ -z "${match##M*}" ]
    return $?
}

#------------------------------------------------------------------------------
# Works on a STRING.  Sees if that string starts with" MI" or "I"
#------------------------------------------------------------------------------
is_installed() {
    local match=$1
    [ -z "${match##I*}" -o -z "${match##MI*}" ]
    return $?
}

#------------------------------------------------------------------------------
# Does the actual search.  If additional arguments are given after the first
# then they are passed to grep.  This is currently used for coloring the
# matches found in a description search.
#------------------------------------------------------------------------------
perform_search() {
    local str=$1 iflag=i ; shift

    # Any uppercase letter causes case sensitive search
    [ -z "${str##*[A-Z]*}" ] && iflag=
    GREP_COLORS="mt=$grep_co" grep "$@" -Eh$iflag "$str" $DB_FILE
    #GREP_COLORS="mt=7" grep "$@" -Eh$iflag "$str" $DB_FILE
    #GREP_COLORS="mt=1;37" grep "$@" -Eh$iflag "$str" $DB_FILE
}

#------------------------------------------------------------------------------
# Search
#------------------------------------------------------------------------------
star_search() {
    local str=$1  file=$SUGGESTED_FILE ; shift
    egrep "$@" "$str" "$file"
}

#------------------------------------------------------------------------------
#List all packages that a given set of packages depend on.
#------------------------------------------------------------------------------
list_depends() {
    apt-cache depends $* | sed -n -r "s/^\s*(Pre)?Depends:\s*//p" | grep -v "^<" | sort -u
}

#------------------------------------------------------------------------------
# Strip off the leading (M)arked and (I)installed flags and the trailing
# description to get the package name and nothing else.
#------------------------------------------------------------------------------
package_name() {
    echo "$1" | sed -r -e "s/^[MI]+ //" -e "s/ .*//"
}

#==============================================================================
# Check to see if all (most) list files exist as expected
#==============================================================================

#------------------------------------------------------------------------------
# See if we need to run an "apt-get update".  The conversion from src file
# entries to .list file names was created heuristically so there may be bugs
# for cases I haven't explored.
#------------------------------------------------------------------------------
check_list_files() {
    src_dir=${1:-$SOURCE_DIR}  list_dir=${2:-$LIST_DIR}

    # LOOP: until list files check out
    while true; do

        local LIST_ERR_CNT=0  LIST_FOUND_CNT=0  LIST_OLDEST_T=999999999999

        local type url repo other root ext

        msg "Checking source files ..."
        while read type url repo other; do
            [ -n "$type" ] || continue
            case $repo in
                *updates) continue ;;
                *) ;;
            esac
            root=${url#*://}
            root=${root//\//_}

            #echo "$type $root $repo $other"
            root=${root%_}_dists_$repo

            local full=$list_dir/$root

            if [ "$type" = "deb" ]; then
                ext="_binary-${ARCH}_Packages"
                find_list_file ${full}_InRelease \
                    || find_list_file ${full}_Release \
                    || list_error "${root}_Release or ${root}_InRelease"

                for part in $other; do
                    test_list_file ${full}_$part$ext
                done

            elif [ "$type" = "deb-src" ]; then
                ext="_source_Sources"
                for part in $other; do
                    test_list_file ${full}_$part$ext
                done
            fi

        done << Read_Sources
$(grep -h "^\s*\(deb\|deb-src\)\>" $src_dir/*.list)
Read_Sources

        local now_t=$(date +%s)
        local max_interval=$((AUTO_UPDATE_INTERVAL * 60 * 60 * 24))
        local interval=$(($(date +%s) - LIST_OLDEST_T))
        local days=$((interval / 60 / 60 / 24))

        (
            echo
            printf  "      Found list files: %s\n"  "$LIST_FOUND_CNT"
            printf  "    Missing list files: %s\n"  "$LIST_ERR_CNT"
            #printf  " Oldest list file time: %s\n"  "$LIST_OLDEST_T"
            #printf  "  Seconds since update: %s\n"  "$interval"
            #printf  "     Days since update: %s\n"  "$days"
            echo
        ) >> $LOG_FILE

        local apt_get=$(pq apt-get update)
        local run_now=$(printf "Run %s now?" "$apt_get")

        if [ $LIST_FOUND_CNT -eq 0 ]; then
            Msg "No list files were found"
            Msg "An %s is required in order to continue" "$apt_get"
            # FIXME BACK_TO_MAIN
            YES_no "$run_now" || return 1

        elif [ $LIST_ERR_CNT -gt 0 ]; then
            Msg "It appears that at least one list file is missing"
            Msg "You should probably run %s now" "$apt_get"
            YES_no "$run_now" || return 0

    #--    elif [ $interval -gt $max_interval ]; then
    #--        Msg "At least one list file is at least %s day(s) old" "$days"
    #--        Msg "You should probably run %s now" "$apt_get"
    #--        YES_no "$run_now" || return
        else
            return 0
        fi

        do_update
    done
}

#------------------------------------------------------------------------------
# In addition to finding the file, we also record its modded time and bumped
# the oldest modded time.  But this seems to be utterly pointless since the
# dates on the .list files are wildly inaccurate (for these purposes).
#------------------------------------------------------------------------------
find_list_file() {
    local file=$1

    test -e $file || return 1
    local modded=$(stat -c %Y $file)
    LIST_FOUND_CNT=$((LIST_FOUND_CNT + 1))
    [ $LIST_OLDEST_T -gt $modded ] && LIST_OLDEST_T=$modded
    return 0
}

#------------------------------------------------------------------------------
# Find the file or record an error
#------------------------------------------------------------------------------
test_list_file() {
    #echo "  $(basename $1)"
    find_list_file $1 || list_error $(basename $1)
}

#------------------------------------------------------------------------------
# Print out an error message and add to the count of errors
#------------------------------------------------------------------------------
list_error() {
    local name=$1
    LIST_ERR_CNT=$((LIST_ERR_CNT + 1))
    echo "Missing list file(s): $name" >> $LOG_FILE
}

#=== End of List File Stuff ===================================================

#------------------------------------------------------------------------------
# Do an "apt-get update" and then trigger a check for upgrades and finally
# regenerate our own database files
#------------------------------------------------------------------------------
do_update() {
    msg "Doing %s" "$(pq apt-get update)"

    apt_get_cmd update
    local ret=$?

    if [ $ret -ne 0 ]; then
        warn "There was a problem running %s" "$(pqw apt-get update)"
        yes_NO "Do you want to continue anyway" || return 1
    fi
    UPGRADE_CNT=

    [ "$JUST_UPDATE" ] && return

    check_for_upgrades
    generate_database_files
}

#------------------------------------------------------------------------------
# Use a small Perl script (for hashing) to create a list of all packages with
# the installed packages marked with a leading "I".  We also make a marked
# version of the list of suggested files.
#------------------------------------------------------------------------------
generate_database_files() {
    msg "Creating database ..."
    start_timer
    local db=${1:-$DB_FILE}   installed=${2:-$INSTALLED_FILE}
    list_installed > $installed
    list_all | mark-installed-debs $installed > $db
    #cat $SUGGESTED_SRC | only_valid_packages | mark-installed-debs $installed > $SUGGESTED_FILE
    cat $SUGGESTED_SRC | mark-installed-debs $installed > $SUGGESTED_FILE

    msg_elapsed_t "database generation"
}

#------------------------------------------------------------------------------
# Update the installed status of a single package.
#------------------------------------------------------------------------------
update_package_status() {
    local pack=$1
    if really_is_installed $pack; then
        mark_as_installed $pack
    else
        mark_as_uninstalled $pack
    fi
}

#------------------------------------------------------------------------------
# Get "ground truth" on whether a package really is installed or not (instead
# of relying on how it was marked in a file.
#------------------------------------------------------------------------------
really_is_installed() {
    local status  pack=$1
    testing && return 0
    status=$(dpkg-query -f '${db:Status-Status}' --show "$pack" 2>/dev/null) \
        || return 1
    [ "$status" = 'installed' ]
    return $?
}

#------------------------------------------------------------------------------
# Mark a package installed
#------------------------------------------------------------------------------
mark_as_installed() {
    local pack=$1  files=${2:-$DB_FILE $SUGGESTED_FILE}
    sed -i -r -e "s/^($pack )/I \1/"  -e "s/^M ($pack )/MI \1/" $files
}

#------------------------------------------------------------------------------
# Mark a package uninstalled
#------------------------------------------------------------------------------
mark_as_uninstalled() {
    local pack=$1  files=${2:-$DB_FILE $SUGGESTED_FILE}
    sed -i -r -e "s/^I ($pack )/\1/" -e "s/^MI ($pack )/M \1/" $files
}

#------------------------------------------------------------------------------
# Mark a package unmarked and installed, for use in install_marked()
#------------------------------------------------------------------------------
unmark_and_mark_installed() {
    local pack ilist files="$DB_FILE $SUGGESTED_FILE"

    start_timer
    msg "Updating database ..."

    # Build a regular expression so we only have to edit each file once
    for pack; do
        really_is_installed $pack && ilist=$ilist${ilist:+|}$pack
    done

    #echo "ilist: $ilist"
    sed -i -r -e "s/^M?I? ?(($ilist) )/I \1/" $files
    MARK_CNT=$(grep --count "^M" $db)

    msg_elapsed_t "database update"
}

#------------------------------------------------------------------------------
# Mark a package and all uninstalled dependencies
#------------------------------------------------------------------------------
mark_package_and_deps() {
    local pack=$(package_name "$1")  db=$DB_FILE sug=$SUGGESTED_FILE
    _mark_package "$pack"

    #testing && return

    # ignore <...> depends for now
    local all_deps=$(list_depends "$pack")
    msg "found %s dependencies" "$(nq $(echo $all_deps | wc -w))"
    local regex=$(echo $all_deps | tr ' ' '|')
    #echo "regex: $regex"
    local cnt=$(egrep --count "^($regex) " $db)
    arrow_msg "Marking %s uninstalled dependencies" "$(nq $cnt)"
    echo $(egrep "^($regex) " $db | sed "s/ .*//")
    sed -i -r "s/^($regex) /M \1 /" $db $sug
    MARK_CNT=$((MARK_CNT + cnt))
}

#------------------------------------------------------------------------------
# Mark a package and report dependencies
#------------------------------------------------------------------------------
mark_package() {
    local pack=$(package_name "$1")  db=$DB_FILE sug=$SUGGESTED_FILE
    _mark_package "$pack"

    #testing && return

    local all_deps=$(list_depends "$pack")
    msg "found %s dependencies" "$(nq $(echo $all_deps | wc -w))"
    local regex=$(echo $all_deps | tr ' ' '|')
    #echo "regex: $regex"
    local cnt=$(egrep --count "^($regex) " $db)
    msg "found %s unmet dependencies" "$(nq $cnt)"
    local unmet_deps=$(egrep "^($regex) " $db | sed "s/ .*//")

    local fold_width=$(($(screen_width) - 5))
    log_it_q echo $unmet_deps | fold -w $fold_width -s

    local size=$(package_size $pack $unmet_deps)
    msg "This package and unmet dependencies will take %s Meg" "$(nq $size)"
}


#------------------------------------------------------------------------------
# Mark a single package if it is not already marked
#------------------------------------------------------------------------------
_mark_package() {
    local pack=$1  db=${2:-$DB_FILE}

    # Don't mark an already marked package
    egrep -q "^(I )?$pack " $db || return

    arrow_msg "Marking package %s" "$(pq $pack)"
    sed -i -r -e "s/^(I $pack )/M\1/" -e "s/^($pack )/M \1/" $db
    MARK_CNT=$((MARK_CNT + 1))
}

#------------------------------------------------------------------------------
# Umark a single package if it is marked
#------------------------------------------------------------------------------
unmark_package() {
    local pack=$(package_name "$1")  db=${2:-$DB_FILE}
    grep -q "^MI? $pack " $db && return

    arrow_msg "Unmark package %s" "$(pq $pack)"
    sed -i -r -e "s/^M(I $pack )/\1/" -e "s/^M ($pack )/\1/" $db
    MARK_CNT=$((MARK_CNT - 1))
}


#------------------------------------------------------------------------------
# Unmark all packages
#------------------------------------------------------------------------------
unmark_all() {
    local db=$DB_FILE  sug=$SUGGESTED_FILE
    local cnt=$(grep --count "^M" $db)

    sed -i -r "s/^M ?//" $db $sug
    arrow_msg "Unmarked %s package(s)" "$(nq $cnt)"

    MARK_CNT=0
}

#------------------------------------------------------------------------------
# Provide information about a package
#------------------------------------------------------------------------------
package_info() {
    local full=$1  pack=$(package_name "$1") db=$DB_FILE

    #shout_subtitle "Package Info"

    show_package "$full"

    local all_deps=$(list_depends "$pack")
    local all_cnt=$(echo $all_deps | wc -w)
    msg "found %s dependencies" "$(nq $all_cnt)"

    local regex=$(echo $all_deps | tr ' ' '|')

    # want only un-installed packages
    local unmet_deps
    if [ $all_cnt -gt 0 ]; then
        unmet_deps=$(egrep "^(M )?($regex) " $db | sed -r -e "s/^MI? //" -e "s/ .*//")
        local real_cnt=$(echo "$unmet_deps" | wc -l)
        msg "found %s unmet dependencies" "$(nq $real_cnt)"
    fi

    local unmet_deps=$(egrep "^($regex) " $db | sed "s/ .*//")

    local fold_width=$(($(screen_width) - 5))
    log_it_q echo $unmet_deps | fold -w $fold_width -s

    local size=$(package_size $pack $unmet_deps)
    msg "This package and dependencies will take %s Meg" "$(nq $size)"

    if testing; then
        press_enter
        return
    fi

    apt-cache show $pack | show_deb_info

    yes_NO "Do you want to see more detailed information?" || return

    apt-cache show $pack | sed -r "s/^([A-Za-z0-9-]+):/$m_co\1:$nc_co/" | less -MSFR

    #press_enter
}

#------------------------------------------------------------------------------
# Display text between "Description-en:" and the next "Whatever:"
#------------------------------------------------------------------------------
show_deb_info() {
    while read line; do
        case $line in
            Description-en:*) ;;
            *) continue;
        esac
        echo "$cyab${line#Description-en: }$nc_co"
        break
    done

    while read line; do
        echo "$line" | egrep -q "^[A-Za-z0-9-]+:" && break
        echo "${line#.}"
    done
}
#------------------------------------------------------------------------------
# Install a single package
#------------------------------------------------------------------------------
install_package() {
    local full=$1  pack=$(package_name "$1")  db=${2:-$DB_FILE}

    shout_subtitle "Install package"

    show_package "$full"

    local all_deps=$(list_depends "$pack")
    local all_cnt=$(echo $all_deps | wc -w)
    msg "found %s dependencies" "$(nq $all_cnt)"

    local regex=$(echo $all_deps | tr ' ' '|')

    # want only un-installed packages
    local unmet_deps
    if [ $all_cnt -gt 0 ]; then
        unmet_deps=$(egrep "^(M )?($regex) " $db | sed -r -e "s/^MI? //" -e "s/ .*//")
        local real_cnt=$(echo "$unmet_deps" | wc -l)
        msg "found %s unmet dependencies that will also get installed" "$(nq $real_cnt)"
    fi

    local unmet_deps=$(egrep "^($regex) " $db | sed "s/ .*//")

    local fold_width=$(($(screen_width) - 5))
    log_it_q echo $unmet_deps | fold -w $fold_width -s

    local size=$(package_size $pack $unmet_deps)
    msg "This package and dependencies will take %s Meg" "$(nq $size)"
    YES_no "Do you want to install this package now?" || return

    msg "Installing %s" "$(pq $pack)"
    apt_get_cmd install "$pack $(echo $unmet_deps)"

    unmark_and_mark_installed $pack $unmet_deps
    press_enter
}

#------------------------------------------------------------------------------
# Install all files marked in the main database that are NOT yet installed.
#------------------------------------------------------------------------------
install_marked() {
    local file=${1:-$DB_FILE}  db=${1:-$DB_FILE}

    local packs=$(grep "^M " $file | sed -e "s/^M //" -e "s/ .*//")
    local cnt=$(echo $packs | wc -w)
    msg "Found %s un-installed marked packages" "$(nq $cnt)"

    if [ $cnt -le 0 ]; then
        warn "Nothing to install"
        return
    fi

    local fold_width=$(($(screen_width) - 5))
    echo $packs | fold -s -w $fold_width

    local all_deps=$(list_depends $packs)
    local all_cnt=$(echo $all_deps | wc -w)
    msg "found %s dependencies" "$(nq $all_cnt)"

    local regex=$(echo $all_deps | tr ' ' '|')

    # want only un-installed packages
    local unmet_deps
    if [ $all_cnt -gt 0 ]; then
        unmet_deps=$(egrep "^(M )?($regex) " $db | sed -r -e "s/^MI? //" -e "s/ .*//")
        local real_cnt=$(echo "$unmet_deps" | wc -l)
        msg "found %s unmet dependencies that will also get installed" "$(nq $real_cnt)"
    fi

    local fold_width=$(($(screen_width) - 5))
    log_it_q echo $unmet_deps | fold -w $fold_width -s

    local size=$(package_size $packs $unmet_deps)
    msg "The package(s) and dependencies will take %s Meg" "$(nq $size)"

    YES_no "Install these packages?" || return
    apt_get_cmd install $(echo $packs)

    unmark_and_mark_installed $(echo $packs $unmet_depends)
    press_enter
}

#------------------------------------------------------------------------------
# Size of all marked packages in a file
#------------------------------------------------------------------------------
marked_size() {
    local file=${1:-$DB_FILE}  db=${1:-$DB_FILE}

    local packs=$(grep "^M " $file | sed -e "s/^M //" -e "s/ .*//")
    if testing; then
        echo 1777
    else
        apt-cache show $(echo $packs) | grep -i "^installed-size:" \
            | awk '{tot = tot + $2} END{printf "%4.2f\n", (tot / 1024)}'
    fi
}

#------------------------------------------------------------------------------
# The installed size of one or more packages
#------------------------------------------------------------------------------
package_size() {
    if testing; then
        echo 1777
        return
    fi
    apt-cache show $* | grep -i "^installed-size:" \
        | awk '{tot = tot + $2} END{printf "%4.2f\n", (tot / 1024)}'
}

#------------------------------------------------------------------------------
# This menu gives options inside of do_view_marked().
#------------------------------------------------------------------------------
mark_menu() {
    [ $MARK_CNT -gt 0 ] || return
    local cnt=$MARK_CNT
    menu_printf_plural install-marked "$cnt" "Install %s marked package"  "Install all %s marked packages"
    menu_printf_plural    view-marked "$cnt" "View %s marked package"     "View all %s marked packages"
    menu_printf_plural     unmark-all "$cnt" "Unmark %s marked package"   "Unmark all %s marked packages"
}

#------------------------------------------------------------------------------
# Optional view, install, and unmark, the marked files.
#------------------------------------------------------------------------------
do_view_marked() {
    local db=${1:-$DB_FILE}  local mark_co=

    shout_subtitle "View or Install Marked Packages"

    while true; do

        grep "^M" $db > $SEARCH_FILE
        local cnt=$(cat $SEARCH_FILE | wc -l)
        if [ $cnt -ne $MARK_CNT ]; then
            warn "Mark count adjusted from %s to %s" $MARK_CNT $cnt
            MARK_CNT=$cnt
        fi

        if [ $cnt -le 0 ]; then
            warn "No marked packages were found"
            return
        fi

        local menu=$(
            mark_menu
            menu_printf quit "Back to main"
        )

        local marked_size=$(marked_size)
        msg "Marked packages(s) will add %s Meg" "$(nq $marked_size)"

        local ans
        my_select ans "Please select an action" "$menu"

        case $ans in
             view-marked)                         ;;
          install-marked) install_marked ; return ;;
              unmark-all) unmark_all     ; return ;;
                    quit) return                  ;;
                    *) internal_error "view mark menu" "$ans"
        esac

        local height=$(screen_height)
        if [ $cnt -lt $((height - 5)) ]; then
            cat $SEARCH_FILE | color_results | less -EXRS
            press_enter
        else
            local blanks=5
            (local i; for i in $(seq 1 $blanks); do echo; done)  > $SEARCH_FILE.nl
            cat $SEARCH_FILE | color_results                    >> $SEARCH_FILE.nl

            msg "%s packages were found." $cnt
            msg
            questn "In the next step you will been shown a list of packages that you can scroll"

            questn "Use %s, %s, %s, and %s to scroll the list" \
                "<$(bqq up-arrow)>" "<$(bqq down-arrow)>" "<$(bqq page-up)>" "<$(bqq page-down)>"

            questn "Position the package you want near the bottom and then press 'q' to continue"
            msg
            questn "Press %s to see the list, %s to go the main menu, %s to do another search" \
                "<$(bqq "Enter")>" "'$(bqq q)'" "'$(bqq s)'"

            read ans
            case $ans in
                [qQ]*) return ;;
                [sS]*) break  ;;
            esac

            # Never start with blank lines under the last line
            local offset=$((blanks + 1))
            [ $((offset + height)) -gt $((blanks + cnt + 2)) ] && offset=$((cnt + blanks - height + 2))

            less -RXSm +$offset --shift=.20 $SEARCH_FILE.nl
        fi
    done
}

#------------------------------------------------------------------------------
# Show a list of suggested packages and allow user to mark and/or install them
#
#------------------------------------------------------------------------------
do_suggested() {
    local db=${1:-$DB_FILE}  local

    local mark_cnt=0
    local ans
    while true; do

        shout_subtitle "Suggested Packages"

        local   all_str="^[MIa-z0-9]"
        local  five_str="^[MIa-z0-9].*\*{5}"
        local  four_str="^[MIa-z0-9].*\*{4}"
        local three_str="^[MIa-z0-9].*\*{3}"
        local   two_str="^[MIa-z0-9].*\*{2}"
        local   one_str="^[MIa-z0-9].*\*{1}"
        local  zero_str="^[MIa-z0-9][^\*]+$"

        local all_cnt=    five_cnt=    four_cnt=    three_cnt=    two_cnt=    one_cnt=
        local all_match=  five_match=  four_match=  three_match=  two_match=  one_match=

        local TOTAL_MATCHES=0  FOUND_MATCH=
        count_star_matches   all   "total suggestion" "total suggestions"
        count_star_matches  five   "star package"     "star packages"  "*****"
        count_star_matches  four   "star package"     "star packages"  "****+"
        count_star_matches three   "star package"     "star packages"  "***+"
        #count_star_matches   two   "package"     "star packages  "**+""
        #count_star_matches   one   "package"     "star packages" "*+"
        count_star_matches   zero  "zero star package"  "no star packages" ""

        if [ $TOTAL_MATCHES -eq 0 ]; then
            warn "No suggestions were found!"
            warn "Check for the file %s" $SUGGESTED_FILE
            return
        fi

       # Need to use a file because the commands below are in a subshell
        rm -f $FOUND_MATCH_FILE
        local star_menu=$(
            [ $mark_cnt -gt 0 ] && add_mark_entry
            add_to_star_menu all    "View all" "suggestions"
            add_to_star_menu five   "View"     "star packages"      "*****"
            add_to_star_menu four   "View"     "star packages"      "****+"
            add_to_star_menu three  "View"     "star packages"      "***+"
            add_to_star_menu zero   "View"     "zero star packages" ""
            menu_printf             skip       "Skip to next step (don't view packages)"
            #add_to_star_menu
            menu_printf  'quit'    "Go back to main menu"
        )

        local ans
        my_select ans "Please select which packages to view" "$star_menu"

        local action=${ans#*-}
        local search_type=${ans%%-*}

        local skip=  regex=
        case $ans in
         view-marked) do_view_marked ; return         ;;
            all-view) regex="^"                       ;;
              *-view) eval regex=\$${search_type}_str ;;
                skip) skip=true                       ;;
                quit) return                          ;;
                   *) internal_error "star menu 1" "$ans" ;;
        esac

        if [ "$action" = 'view' ]; then
            #star_search "$regex" -e "^#" -e "^$" -e > $SEARCH_FILE
            star_search "$regex" > $SEARCH_FILE
            local pack_cnt=$(egrep --count "^[MI0-9a-z]" $SEARCH_FILE)
            local line_cnt=$(cat $SEARCH_FILE | wc -l)
            local height=$(screen_height)
        fi

        if [ "$skip" ]; then
            :

        elif [ $line_cnt -lt $((height - 5)) ]; then
            echo
            cat $SEARCH_FILE | number_suggestions | less -EXRS
        else
            local blanks=5
            (local i; for i in $(seq 1 $blanks); do echo; done)  > $SEARCH_FILE.nl
            cat $SEARCH_FILE | number_suggestions               >> $SEARCH_FILE.nl

            local enter="$(bqq "Enter")"
            local ucnt=$(egrep --count "^(M |[a-z0-9])" $SEARCH_FILE)

            msg "%s packages were found (%s uninstalled)." "$(nq $pack_cnt)" "$(nq $ucnt)"

            msg
            questn "In the next step you will been shown a list of packages that you can scroll"
            questn "Use <up-arrow>, <down-arrow>, <page-up>, and <page down> to scroll the list"
            questn "Position the package you want near the bottom and then press 'q' to continue"
            msg
            questn "press %s to see the list, %s to go the main menu, %s to search again" \
                "<$enter>" "$(bqq q)" "$(bqq r)"

            local xxx
            read -n1 ans
            #read xxx -t .01
            case $ans in
                [qQ]*) return   ;;
                [rR]*) continue ;;
            esac

            # Never start with blank lines under the last line
            local offset=$((blanks + 1))
            [ $((offset + height)) -gt $((blanks + line_cnt + 2)) ] && offset=$((line_cnt + blanks - height + 2))

            less -RXSm +$offset --shift=.20 $SEARCH_FILE.nl
        fi

        local action=  enter=$(bqq "Enter")

        # LOOP: enter a number
        while true; do
            [ "$skip" ] && break
            questn "Press %s to skip picking a package" "<$enter>"
            questn "Or enter the number of the package you want to mark or install"
            questn "Use %s to go to main menu, %s to see the results again" "$(bqq q)<$enter>" "$(bqq r)<$enter>"
            quest "> "

            local number
            read number

            case $number in
                   "")  action=skip; break ;;
                [qQ]*) return   ;;
                [rR]*) action=review ; break ;;
                #[sS]*) action=search ; break ;;
                [1-9]|[1-9][0-9]|[1-9][0-9][0-9]) ;;
                            [1-9][0-9][0-9][0-9]) ;;
                       [1-9][0-9][0-9][0-9][0-9]) ;;
                  [1-9][0-9][0-9][0-9][0-9][0-9]) ;;
                *) warn "Invalid input.  Please try again" ; continue ;;
            esac

            if [ $number -gt $pack_cnt ]; then
                warn "That number was out of range, please try again"
                continue
            fi

            break
        done

        case $action in
            review) continue ;;
        esac

        local full_package=

        if [ "$number" ]; then
            full_package=$(cat $SEARCH_FILE | my_nl | sed -n -r "s/^\s*$number\s+//p")
            if [ -z "$full_package" ]; then
                warn "Could not find package number %s" "$number"
                break
            fi

            msg "Selected package number %s" "$(bq $number)"
            show_package "$full_package"
        fi

        local action=
        # LOOP: package info
        while true; do

            local menu=$(
                mark_or_install_menu "$full_package"
                mark_all_menu        $SEARCH_FILE
                install_all_menu     $SEARCH_FILE
                menu_printf suggest  $"See suggestions again"
                menu_printf quit     $"Back to main menu"
            )

            my_select ans "Please select an action" "$menu"

            case $ans in
                      info)        package_info "$full_package" ; continue ;;
                      mark)        mark_package "$full_package"            ;;
                    unmark)      unmark_package "$full_package"            ;;
                   install)     install_package "$full_package"            ;;
                 uninstall)   uninstall_package "$full_package"            ;;
                  mark-all)       mark_all_file $SEARCH_FILE               ;;
                unmark-all)     unmark_all_file $SEARCH_FILE               ;;
            install-marked)     install_marked  $SEARCH_FILE               ;;
                   suggest) action=continue                                ;;
                      quit) return                                         ;;
                        *) internal_error "bottom suggestion menu" "$ans" ;;
            esac

            mark_cnt=$(grep --count "^M" $SUGGESTED_FILE)

            break
        done

        case $action in
            continue) continue;;
        esac
    done
}

#------------------------------------------------------------------------------
# Mark all unmarked, uninstalled packages that are listed in a file
#------------------------------------------------------------------------------
mark_all_file_and_deps() {
    local file=$1  db=$DB_FILE  files="$DB_FILE $SUGGESTED_FILE"
    local packs=$(egrep "^[a-z0-9]" $file | sed -e "s/I //" -e "s/ .*//")
    local regex=$(echo $packs | tr ' ' '|')
    #echo $regex
    #sed -r -i -e "s/^I ($regex) /MI \1 /" -e "s/^($regex) /M \1 /" $files
    sed -r -i "s/^($regex) /M \1 /" $files
    local cnt=$(echo $packs | wc -w)
    arrow_msg "Marked %s previously unmarked packages" "$(nq $cnt)"
    MARK_CNT=$((MARK_CNT + cnt))

    local all_deps=$(list_depends $(echo $packs))

    msg "found %s dependencies" "$(nq $(echo $all_deps | wc -w))"
    regex=$(echo $all_deps | tr ' ' '|')
    local unmet_deps=$(egrep "^($regex) " $db | sed "s/ .*//")
    cnt=$(echo "$unmet_deps" | wc -l)
    arrow_msg "Marked %s unmet dependencies" "$(nq $cnt)"
    sed -i -r "s/^($regex) /M \1 /" $files
    #echo "real marked: $(grep --count "^M" $db)"
    MARK_CNT=$((MARK_CNT + cnt))
}

#------------------------------------------------------------------------------
# Mark all uninstalled packages listed in a file.  Report on deps
#------------------------------------------------------------------------------
mark_all_file() {
    local file=$1  db=$DB_FILE  files="$DB_FILE $SUGGESTED_FILE"
    local packs=$(egrep "^[a-z0-9]" $file | sed -e "s/I //" -e "s/ .*//")
    local regex=$(echo $packs | tr ' ' '|')
    #echo $regex
    #sed -r -i -e "s/^I ($regex) /MI \1 /" -e "s/^($regex) /M \1 /" $files
    sed -r -i "s/^($regex) /M \1 /" $files
    local cnt=$(echo $packs | wc -w)
    arrow_msg "Marked %s previously unmarked packages" "$(nq $cnt)"
    MARK_CNT=$((MARK_CNT + cnt))

    local all_deps=$(list_depends $(echo $packs))

    msg "found %s dependencies" "$(nq $(echo $all_deps | wc -w))"
    regex=$(echo $all_deps | tr ' ' '|')
    local unmet_deps=$(egrep "^($regex) " $db | sed "s/ .*//")
    cnt=$(echo "$unmet_deps" | wc -l)
    msg "found %s unmet dependencies" "$(nq $cnt)"

    local fold_width=$(($(screen_width) - 5))
    log_it_q echo $unmet_deps | fold -w $fold_width -s

    local size=$(package_size $pack $unmet_deps)
    msg "This package and unmet dependencies will take %s Meg" "$(nq $size)"
}

#------------------------------------------------------------------------------
# Unmark all packages listed inside of a file
#------------------------------------------------------------------------------
unmark_all_file() {
    local file=$1  files=${2:-$DB_FILE $SUGGESTED_FILE}
    local packs=$(egrep "^MI? [a-z0-9]" $file | sed -r -e "s/MI? //" -e "s/ .*//")
    local regex=$(echo $packs | tr ' ' '|')
    #echo $regex
    sed -r -i -e "s/^M ($regex) /\1 /" -e "s/^MI ($regex) /I \1 /"  $files

    local cnt=$(echo $packs | wc -w)
    arrow_msg "Unmarked all %s previously marked packages" "$(nq $cnt)"

    local mcnt=$(grep --count "^M" $DB_FILE)
    if [ $mcnt -gt 0 ]; then
        msg "%s package(s) are still marked" "$(nq $mcnt)"
    fi

    MARK_CNT=$((MARK_CNT - cnt))
}

#------------------------------------------------------------------------------
# menu for marking or unmarking all packages within a file.  Used in both
# do_suggested() and do_search()
#------------------------------------------------------------------------------
mark_all_menu() {
    local file=$1
    test -r $file || return

    #local    total=$(egrep --count "^[IMa-z0-9]" $file)
    local can_mark=$(egrep --count "^[a-z0-9]"   $file)
    local   marked=$(egrep --count "^M"          $file)

    [ $can_mark -gt 0 ] \
        && menu_printf_plural   mark-all $can_mark "Mark %s unmarked, uninstalled package" "Mark %s unmarked, uninstalled packages"

    [ $marked -gt 0 ]\
        && menu_printf_plural unmark-all $marked   "Unmark %s marked package" "Unmark %s marked packages"

    #[ $marked -gt 0 ] \
    #    && menu_printf_plural install-marked "$marked" "Install %s marked package"  "Install all %s marked packages"
}

#------------------------------------------------------------------------------
#
#------------------------------------------------------------------------------
install_all_menu() {
    local file=$1
    test -r $file || return

    local    total=$(egrep --count "^[IMa-z0-9]" $file)
    local can_mark=$(egrep --count "^[a-z0-9]"   $file)
    local   marked=$(egrep --count "^M"          $file)

    [ $marked -gt 0 ] \
        && menu_printf_plural install-marked "$marked" "Install %s marked package"  "Install all %s marked packages"
}
#------------------------------------------------------------------------------
# Used for number only packages inside of the suggestion file.  We only number
# a line if it starts with a char other than "#".  This looseness allows us
# to run on colorized output (as long as the headers aren't colored).
#------------------------------------------------------------------------------
my_nl() {
    local cnt=1  line  blank_cnt=0

    while read line; do

        # Limit consectutive blank lines
        if [ -z "$line" ]; then
            blank_cnt=$((blank_cnt + 1))
            [ $blank_cnt -le 1 ] && echo
            continue
        fi
        blank_cnt=0

        # Only number packages
        if [ -n "$line" -a -z "${line##[^#]*}" ]; then
            #printf "$m_co%5s$quest_co)$nc_co %s\n" "$cnt" "$line"
            printf "%3s %s\n" "$cnt" "$line"
            cnt=$((cnt + 1))
        else
            printf "%s\n" "$line"
        fi
    done
}

#------------------------------------------------------------------------------
# Filter out invalid packages but pass through non-package lines
#------------------------------------------------------------------------------
only_valid_packages() {
    local line
    while read line; do
        if [ -z "$line" -o -n "${line##[a-z0-9]*}" ]; then
            echo "$line"
            continue
        fi

        local pack=${line%% *}
        if package_exists "$pack"; then
            echo "$line"
        else
            printf "Missing suggested package %s\n" "$pack" >> $LOG_FILE
        fi
    done
}


#------------------------------------------------------------------------------
#
#------------------------------------------------------------------------------
package_exists() {
    local pack=$1
    local desc=$(apt-cache search "^$pack$")
    [ -n "$desc" ]
    return $?
}

#------------------------------------------------------------------------------
# List all packages.  Used to generate our database file.
#------------------------------------------------------------------------------
list_all() {  apt-cache search . | sort; }

#------------------------------------------------------------------------------
# List only installed packages.  Also used to generate our database file.
#------------------------------------------------------------------------------
list_installed() { dpkg-query --show | sort | cut -f1; }

#------------------------------------------------------------------------------
# Count the number of packages that can/will be upgraded
#------------------------------------------------------------------------------
count_upgrades() { apt list --upgradable 2>/dev/null | grep -v "^Listing" | wc -l;  }

#------------------------------------------------------------------------------
# Check to for system upgrades
#------------------------------------------------------------------------------
check_for_upgrades() {
    msg "Checking for package upgrades ..."
    UPGRADE_CNT=$(count_upgrades)
    if [ $UPGRADE_CNT -eq 0 ]; then
        #msg "nothing to upgrade"
        return
    fi
    msg "There are %s packages that can be upgraded" "$(nq $UPGRADE_CNT)"
    do_upgrade_menu 3
}

#------------------------------------------------------------------------------
# Allow user to perform a system upgrade if they want
#------------------------------------------------------------------------------
do_upgrade_menu() {
    local default_entry=$1

    : ${UPGRADE_CNT:=$(count_upgrades)}
    if [ $UPGRADE_CNT -eq 0 ]; then
        warn "There are no packages to upgrade"
        return
    fi

    shout_subtitle "System Upgrade Menu"

    while true; do
        local ans
        my_select ans "Please select an action" "$(upgrade_menu $UPGRADE_CNT)" "" $default_entry
        case $ans in
             ignore) return              ;;
               quit) return              ;;
            upgrade) do_upgrade ; return ;;
               view) view_upgrades       ;;
                  *) internal_error "upgrade_menu" "$ans" ;;
        esac
    done
}

#------------------------------------------------------------------------------
# Simply list all packages that would be upgraded
#------------------------------------------------------------------------------
view_upgrades() {
    # FIXME: Use UPGRADE_CNT to optimize how we use "less".
    apt list --upgradable 2>/dev/null | less
}

#------------------------------------------------------------------------------
# Do the upgrade and then regenerate our database
#------------------------------------------------------------------------------
do_upgrade() {
    apt_get_cmd upgrade

    [ "$JUST_UPDATE" ] && return
    generate_database_files
}

#------------------------------------------------------------------------------
# Set BACK_TO_MAIN as an empty local variable.  This affects how we deal with
# the user pressing 'q' when we are waiting for input.
#------------------------------------------------------------------------------
run_outer() { local BACK_TO_MAIN= ; "$@" ; }

#------------------------------------------------------------------------------
# Start logging by appending a simple header
#------------------------------------------------------------------------------
start_log() {
    local args=$1 cmds=${2# }

    LOG_FILE=$THE_LOG_FILE

    cat <<Start_Log >> $LOG_FILE
---------------------------------------------------------------------
$0
        started: $(date)
        version: $VERSION ($VERSION_DATE)
    comand line: $args
      found lib: $FOUND_LIB
     TEXTDOMAIN: $TEXTDOMAIN
  TEXTDOMAINDIR: $TEXTDOMAINDIR
     SOURCE_DIR: $SOURCE_DIR
       LIST_DIR: $LIST_DIR

Start_Log
}

#------------------------------------------------------------------------------
# Return the height / width of the screen in chars.
#------------------------------------------------------------------------------
screen_height() { stty size | cut -d" " -f1; }
screen_width()  { stty size | cut -d" " -f2; }

#------------------------------------------------------------------------------
# Convenience routine to say if we are testing or not
#------------------------------------------------------------------------------
testing() { [ -n "$TEST" ]; return $?; }

#------------------------------------------------------------------------------
# Print a bold ==> arrow before the message
#------------------------------------------------------------------------------
arrow_msg() {
    local fmt=$1 ; shift
    msg "$(bq "==>") $fmt" "$@"
}

#------------------------------------------------------------------------------
# Load the lib either from a neighboring repo or from the standard location.
#------------------------------------------------------------------------------
load_lib() {
    local file=$1  path=$2
    unset FOUND_LIB

    local dir lib found IFS=:
    for dir in $path; do
        lib=$dir/$file
        test -r $lib || continue
        if ! . $lib; then
            printf "Error when loading library %s\n" "$lib" >&2
            printf "This is a fatal error\n" >&2
            exit 15
        fi
        FOUND_LIB=$lib
        return 0
    done

    printf "Could not find library '%s' on path '%s'\n" "$file" "$path" >&2
    printf "This is a fatal error\n" >&2
    exit 17
}

#------------------------------------------------------------------------------
# Mount the work directory as tmpfs.  This should make our searches slightly
# faster.
#------------------------------------------------------------------------------
mount_work_dir() {
    local dir=${1:_$WORK_DIR}

    if is_mountpoint $WORK_DIR; then
        warn "The directory %s is already a mount point"
        return
    fi
    mount -t tmpfs tmpfs $WORK_DIR || fatal "Could not mount tmpfs at %s" $dir
}

#------------------------------------------------------------------------------
# Unmount the work directory
#------------------------------------------------------------------------------
umount_work_dir() {
    local dir=${1:-$WORK_DIR}
    sync
    is_mountpoint "$dir" || return 0

    local try
    for try in $(seq 1 30); do
        umount --recursive "$dir"  2>/dev/null
        is_mountpoint "$dir" || return 0
        sleep .1
    done
    rmdir "$dir"
}

on_exit() {
    testing || umount_work_dir
    testing || unflock
}

#===== Start Here =============================================================

load_lib "$SHELL_LIB" "$LIB_PATH"

set_colors "$1"

main "$@"
