#!/bin/bash

# Options are passed on to deborphan
# 
# Orphaner is a neat frontend for deborphan displaying a list of
# orphaned packages with dialog. Packages may be selected for removal
# with apt-get which is then called to do the work. After removal a new
# list of orphaned packages is gathered from deborphan. The program ends
# when either `Cancel' is pressed or no package is marked for removal.
#
# (c) 2000 Goswin Brederlow <goswin.brederlow@student.uni-tuebingen.de>
# (c) 2000,2003 Peter Palfrader <peter@palfrader.org>
# (c) 2003,2004 Jrg Sommer <joerg@alea.gnuu.de>
#
# This program is dual licensed either unter the GNU GPL as published
# by the Free Software Foundation; either version 2, or (at your option)
# any later version,
# _OR_ (at your opinion)
# the Artistic license (under which deborphan itself is distributed).
#
# The full text of both can be found in /usr/share/common-licenses on
# Debian systems. If you have problems obtaining the files please
# write to the authors.
#
# $Id: orphaner 420 2004-04-28 02:29:15Z jo-guest $

set -e

REVISION='$Rev: 420 $';
VERSION="(r${REVISION#\$Rev: }"
VERSION="${VERSION% \$})"

OPTIONS=$@
VALIDOPTIONS='^-([aDHns]|-libdevel|-guess-(.+)|-find-(.+)|-nice-mode|-all-packages|-priority(.+)|p(.+)|-show-section|-force-hold)[[:space:]]$'
VALIDKEEPOPTIONS='^-([aDHns]|-libdevel|-guess-(.+)|-find-(.+)|-nice-mode|-all-packages|-priority(.+)|p(.+)|-show-section|-force-hold)[[:space:]]$'

if ! which dialog >/dev/null ; then
  echo "$0: You need dialog in \$PATH to run this frontend." >&2
  exit 1
fi

# Plea for help?
case " $OPTIONS " in
  *" --help "*|*" -h "*)
    echo "Usage: $0 [--help|--purge] [deborphan options]"
    echo
    echo "See orphaner(8) and deborphan(1) for a list of valid options."
    exit 0
    ;;
esac

# Adapt to terminal size
if [ -n "${LINES:-}" -a -n "${COLUMNS:-}" ]; then
    # Can't use LINES, because it colides with magic variable
    # COLUMNS ditto
    lines=$(($LINES - 7))
    columns=$((COLUMNS - 10))
    
    # unset these magic variables to avoid unwished effects
    unset LINES COLUMNS
else
    size=$(stty size)
    lines=$((${size% *} - 7))
    columns=$((${size#* } - 10))

    function sigwinch_handle
    {
        size=$(stty size)
	lines=$((${size% *} - 7))
	columns=$((${size#* } - 10))

 	if [ $lines -ge 12 -a $columns -ge 50 ]; then
            LISTSIZE="$lines $columns $(($lines - 7))"
	    BOXSIZE="$lines $columns"
        fi
    }

    trap sigwinch_handle SIGWINCH
fi

if [ $lines -lt 12 -o $columns -lt 50 ]; then
    echo 'Screen to small or set $LINES and $COLUMNS' >&2
    exit 1
fi

LISTSIZE="$lines $columns $(($lines - 7))"
BOXSIZE="$lines $columns"

editkeepers() {
  for each in $OPTIONS; do
    if [ "$SKIPONE" = "1" ]; then
      SKIPONE=0;
    elif [ " $each" = " --keep-file" -o " $each" = " -k" ]; then
      SKIPONE=1;
    elif [ " $each" = " --status-file" -o " $each" = " -f" ]; then
      SKIPONE=1;
    elif ! echo "$each " | egrep $VALIDKEEPOPTIONS >/dev/null; then
      case "$each" in
        --status-file* | -f* | --keep-file* | -k*)
          ;;
        *)
          echo "$0: Invalid option: $each." >&2
          exit 1
          ;;
      esac;
    fi
  done

    ORPHANED=`keeping_list $OPTIONS | sort`;
    # insert clever error handling

    if [ -n "$ORPHANED" ]; then
      PACKAGES=`tempfile`;
      dialog \
	--backtitle "Orphaner $VERSION" \
	--separate-output \
	--title "Orphaner $VERSION" \
	--checklist "Select Packages that should never be recommended for removal in deborphan:" \
	$LISTSIZE \
	$ORPHANED \
	2> $PACKAGES || true
	
      ### What happens in case cancel was pressed?

      if deborphan --help | grep -q 'Do not read debfoster'; then
    	NODF="--df-keep"
      fi

      deborphan ${NODF} --zero-keep $OPTIONS
      if [ -s $PACKAGES ]; then
        deborphan --add-keep - $OPTIONS < $PACKAGES
      fi
      rm $PACKAGES
    fi
}

keeping_list() {
    {
      { deborphan -a $@ || echo "ERROR"; } \
        | while read SECTION PACKAGE; do 
       	      echo $PACKAGE $SECTION off
     	  done;
      { deborphan -L $@ 2>/dev/null|| echo "ERROR"; } \
        | while read PACKAGE; do 
              echo $PACKAGE "." on
          done;
    } | sort;
};

deborphan_list() {
    { deborphan -s $@ || echo "ERROR"; } \
      | while read SECTION PACKAGE; do 
            echo $PACKAGE $SECTION off
        done
}

doorphans() {
  # Check options
  skipone=0
  for each in $OPTIONS; do
    if [ "$skipone" = "1" ]; then
      skipone=0;
    elif [ " $each" = " --keep-file" -o " $each" = " -k" ]; then
      skipone=1;
    elif [ " $each" = " --status-file" -o " $each" = " -f" ]; then
      skipone=1;
    elif ! echo "$each " | egrep -q $VALIDOPTIONS; then
      case "$each" in
        --status-file* | -f* | --keep-file* | -k*)
          ;;
        *)
          echo "$0: Invalid option: $each." >&2
          exit 1
          ;;
      esac;
    fi
  done

  TMPFILE=`tempfile`
  trap "if [ -e $TMPFILE ]; then rm $TMPFILE; fi" EXIT

  EXCLUDE=
  ORPHANED=
  # Don't touch the next two lines! This is correct! NL should be the newline
  # character
  NL='
'
  while true; do
    OLD_ORPHANED="$ORPHANED"
    ORPHANED=$(deborphan_list $OPTIONS ${EXCLUDE:+--exclude=$EXCLUDE}| LANG=C sort)
    if [ "$ORPHANED" = "ERROR off" ] ; then
      echo "deborphan returned with error" >&2
      exit 1
    fi

    if [ -z "$ORPHANED$EXCLUDE" ]; then
        dialog \
	  --backtitle "Orphaner $VERSION" \
	  --title "Orphaner $VERSION" \
	  --msgbox "No orphaned packages found." \
	  $BOXSIZE
        break
    elif [ -z "$OLD_ORPHANED" ]; then
	# it's the first loop cycle
	SPLIT_NEW=
	SPLIT_OLD="$ORPHANED"
    elif [ -z "$ORPHANED" ]; then
	# maybe we have excluded all packages and no new packages where
	# orphaned
	ORPHANED="$OLD_ORPHANED"
	SPLIT_NEW=
	SPLIT_OLD=
	while read LINE; do
	    SPLIT_OLD="$SPLIT_OLD$NL${LINE%off}on"
	done <<__OORPH_EOT
$OLD_ORPHANED
__OORPH_EOT

	SPLIT_OLD="${SPLIT_OLD#$NL}"	# trim leading newline character
     else
        # Idea: you have two sorted lists: the list of the orphaned packages
	# in the last cycle and the list of orphaned packages in this cycle.
	# Now you compare element by element if the lists differ.
        exec 3<<__ORPH_EOT 4<<__OORPH_EOT
$ORPHANED
__ORPH_EOT
$OLD_ORPHANED
__OORPH_EOT
	read LINE <&3
	read OLD_LINE <&4
	SPLIT_NEW=
	SPLIT_OLD=
	if [ -n "$EXCLUDE" ]; then
	    # If we exclude some packages, the list of orphaned packages
	    # is incomplete. So we build up the list from scratch
	    ORPHANED=
	fi
	while true; do
	    if [ "$LINE" ">" "$OLD_LINE" ]; then
		# The package from the old orphaned list was removed
		if [ -n "$EXCLUDE" ]; then
		    # ...but not really, it is only excluded
		    ORPHANED="$ORPHANED$NL$OLD_LINE"
		    SPLIT_OLD="$SPLIT_OLD$NL${OLD_LINE%off}on"
		fi

		read OLD_LINE <&4 || break
	    else
		if [ -n "$EXCLUDE" ]; then
		    ORPHANED="$ORPHANED$NL$LINE"
		fi

		if [ "$LINE" = "$OLD_LINE" ]; then
	            # ophaned packages are equal no changes
		    SPLIT_OLD="$SPLIT_OLD$NL$LINE"
		    LINE=
		    read OLD_LINE <&4 || break
		else # $LINE < $OLD_LINE
		    # there is a new package in the orphaned list
		    SPLIT_NEW="$SPLIT_NEW$NL$LINE"
		fi

		if ! read LINE <&3; then
		    # the new orphaned list reached the end, all packages
		    # from the old orphaned list are removed
		    if [ -n "$EXCLUDE" ]; then
		        # ...but not really, they are only excluded
		        ORPHANED="$ORPHANED$NL$OLD_LINE"
		    	SPLIT_OLD="$SPLIT_OLD$NL${OLD_LINE%off}on"
			while read OLD_LINE; do
		            ORPHANED="$ORPHANED$NL$OLD_LINE"
		    	    SPLIT_OLD="$SPLIT_OLD$NL${OLD_LINE%off}on"
			done <&4
		    fi
		    break
		fi
	    fi
	done
	exec 4<&-

	# The list of old orphaned packages reached the end. So all
	# remaining new orphaned packages are new
	if [ -n "$LINE" ]; then
	    if [ -n "$EXCLUDE" ]; then
		ORPHANED="$ORPHANED$NL$LINE"
	    fi
	    SPLIT_NEW="$SPLIT_NEW$NL$LINE"
	fi
	while read LINE; do
	    if [ -n "$EXCLUDE" ]; then
		ORPHANED="$ORPHANED$NL$LINE"
	    fi
	    SPLIT_NEW="$SPLIT_NEW$NL$LINE"
	done <&3
	exec 3<&-
	    
	# trim leading newline characters
	ORPHANED="${ORPHANED#$NL}"
	SPLIT_OLD="${SPLIT_OLD#$NL}"
	SPLIT_NEW="${SPLIT_NEW#$NL}"
    fi

    while true; do
       ERROR=0
       dialog --backtitle "Orphaner $VERSION" \
               --defaultno \
	       ${DEFAULT_PKG:+--default-item $DEFAULT_PKG} \
	       --separate-output \
	       --title "Orphaner $VERSION" \
	       --help-button --help-status --extra-button --extra-label Simulate \
	       --checklist "Select Packages for removal or cancel to quit:" \
	       $LISTSIZE ${SPLIT_NEW:+$SPLIT_NEW ---- _new_packages_above_ off} \
	       $SPLIT_OLD 2> $TMPFILE || ERROR=$?

       unset DEFAULT_PKG EXCLUDE

       case $ERROR in
	  0) # OK-Button
	       if [ ! -s $TMPFILE ]; then
		       # nothing's selected
		       break 2
	       fi
	       clear
	       echo Removing $(cat $TMPFILE)
	       apt-get $PURGE --show-upgraded --assume-yes remove \
		       $(cat $TMPFILE) || ERROR=$?
	       if [ $ERROR -ne 0 ]; then
		       echo "apt-get returned with exitcode $ERROR" >&2
		       exit 1
	       fi
	       if ! which deborphan >/dev/null 2>&1; then
		      echo "deborphan got removed.  exiting.";
		      exit 0;
	       fi
	       if ! which apt-get >/dev/null 2>&1; then
		      echo "apt got removed.  exiting.";
		      exit 0;
	       fi
	       break
	       ;;
	   1) # Cancel-Button
	       break 2
	       ;;
	   2) # Help-Button
	       SEL_LIST=
	       while read pkg; do
	       	   case "$pkg" in
		     "HELP "*)
		        # DEFAULT_PKG is default item in the next dialog
		        DEFAULT_PKG=${pkg#HELP }
		        ;;
		     *)
		        SEL_LIST="$SEL_LIST $pkg"
			;;
		   esac
	       done < $TMPFILE

	       if test -n "$SPLIT_NEW"; then
	           while read pkg rest; do
	               new_SPLIT_NEW="$new_SPLIT_NEW$NL$pkg $rest"
	               # check if the selection for every new orphaned package
		       # changed
		       case "$SEL_LIST " in
		         *" $pkg "*)  # now it is selected...
			   case "$rest" in
			     *' off') # ...but wasn't before
			       new_SPLIT_NEW="${new_SPLIT_NEW%off}on"
			   esac
		           ;;
		         *) # now it is deselected...
		           case "$rest" in
		             *' on') # ...but it was selected before
			       new_SPLIT_NEW="${new_SPLIT_NEW%on}off"
		           esac
		           ;;
		       esac
	           done <<__EOT
$SPLIT_NEW
__EOT
	           SPLIT_NEW="${new_SPLIT_NEW#$NL}"
		   unset new_SPLIT_NEW
	       fi

	       while read pkg rest; do
	           new_SPLIT_OLD="$new_SPLIT_OLD$NL$pkg $rest"
	           # check if the selection for every old ophaned package
		   # changed
	           case "$SEL_LIST " in
		     *" $pkg "*)  # now it is selected...
		       case "$rest" in
		         *' off') # ...but wasn't before
			   new_SPLIT_OLD="${new_SPLIT_OLD%off}on"
		       esac
		       ;;
		     *) # now it is deselected...
		       case "$rest" in
		         *' on') # ...but it was selected before
			   new_SPLIT_OLD="${new_SPLIT_OLD%on}off"
		       esac
		       ;;
		   esac
	       done <<__EOT
$SPLIT_OLD
__EOT
	       SPLIT_OLD="${new_SPLIT_OLD#$NL}"
               unset new_SPLIT_OLD

	       dpkg -s $DEFAULT_PKG > $TMPFILE
	       dialog  --backtitle "Orphaner $VERSION" \
		       --title "Orphaner $VERSION" \
		       --textbox $TMPFILE $BOXSIZE
	       ;;
	   3) # Simulate-Button
	       EXCLUDE=$(while read pkg; do printf $pkg,; done < $TMPFILE)
	       EXCLUDE=${EXCLUDE%,}
	       break
	       ;;
	   *)
	       echo "dialog returned with exitcode $ERROR:" >&2
	       cat $TMPFILE
	       exit 1
       esac
      done
  done
}

# purge set?
case " $OPTIONS " in
  *" --purge "*)
    OPTIONS="${OPTIONS%--purge*}${OPTIONS#*--purge}"
    PURGE=--purge
    ;;
esac

case $0 in
  *orphaner) doorphans;;
  *editkeep) editkeepers;;
  *)
    echo "Invalid basename: $0" >&2
    exit 1
    ;;
esac

clear
