#!/usr/bin/perl
# vim: ts=4:noet
#
# sbotool
# a dialog-based front-end for sbotools
#
# author: K. Eugene Carlson <kvngncrlsn@gmail.com>
# license: MIT License

use strict;
use warnings;

use SBO::Lib qw/ :colors :config :const :help auto_reverse check_multilib check_x32 check_x64 clear_info_store get_all_available get_available_updates get_build_queue get_from_info get_full_reverse get_installed_packages get_optional get_orig_location get_readme_contents get_reverse_reqs get_sbo_description get_sbo_location get_slack_version in in_regexp ineligible_compat is_local lint_sbo_config on_blacklist prompt read_config read_hints renew_sbo_locations save_options script_error show_version slurp series_check solib_check uniq update_known_solibs %concluded @reverse_concluded %old_libs $repo_path $slackbuilds_txt $tmpd $tempdir $descriptions_generated %warnings /;

use File::Basename;
use File::Find;
use File::Path qw/ make_path remove_tree /;
use File::Temp qw/ tempdir /;
use Getopt::Long qw/ :config no_ignore_case_always /;
use Time::HiRes qw/ time /;

my $self = basename $0;
$is_sbotool = 1;

my $exit;
usage_error("dialog is not installed.") unless -x "/usr/bin/dialog";
my ($help, $show_version, $dialogrc, $config_menu);
GetOptions( 'help|h' => \$help, 'version|v' => \$show_version, 'dialogrc|d=s' => \$dialogrc, 'config' => \$config_menu);
if ($help) { show_usage(); exit 0; }
if ($show_version) { show_version(); exit 0; }

lint_sbo_config($self, %config);

my $version = $SBO::Lib::VERSION;

my $backtitle = "sbotools-$version";
$backtitle = $< == 0 ? "$backtitle (root mode)" : "$backtitle (normal user mode)";
my $dialog_base = "/usr/bin/dialog --keep-window --cancel-label \"Back\" --exit-label \"Back\" --backtitle \"$backtitle\"";
my $dialog_prefix = $dialog_base;
if (defined $dialogrc and -s $dialogrc) {
	$dialog_prefix = "DIALOGRC=\"$dialogrc\" $dialog_prefix";
} else {
	$dialog_prefix = ($config{DIALOGRC} ne "FALSE" and -s $config{DIALOGRC}) ? "DIALOGRC=\"$config{DIALOGRC}\" $dialog_prefix" : $dialog_prefix;
}
my $save_dialogrc = $config{DIALOGRC};

# Account for potential dialog-related environment variables.
my $code_dialog_ok = defined $ENV{"DIALOG_OK"} ? $ENV{"DIALOG_OK"} : 0;
my $code_dialog_cancel = defined $ENV{"DIALOG_CANCEL"} ? $ENV{"DIALOG_CANCEL"} : 1;
my $code_dialog_help = defined $ENV{"DIALOG_HELP"} ? $ENV{"DIALOG_HELP"} : 2;
my $code_dialog_item_help = defined $ENV{"DIALOG_ITEM_HELP"} ? $ENV{"DIALOG_ITEM_HELP"} : $code_dialog_help;
my $code_dialog_extra = defined $ENV{"DIALOG_EXTRA"} ? $ENV{"DIALOG_EXTRA"} : 3;
my $code_dialog_error = defined $ENV{"DIALOG_ERROR"} ? $ENV{"DIALOG_ERROR"} : -1;

my $overrides_available = 1 if $config{LOCAL_OVERRIDES} ne "FALSE" and -d -w $config{LOCAL_OVERRIDES};

my ($terminal_height, $terminal_width);
my $max_width = 125;
my ($normal, $wide, $narrow); # All set in terminal_check.
terminal_check();
dialog_check();
system("$dialog_prefix --infobox \"Loading...\" 3 15");

my $editor = $ENV{EDITOR};
$editor = $ENV{VISUAL} unless $editor;
# vi is in the POSIX standard and emacs, nano, etc. are not.
# That's the only reason.
$editor = "vi" unless $editor;

my (@available, $available, $all_fulldeps, @installed, $inst_pkgs, $inst_vers, %inst_names, $inst_pkgs_std, $inst_vers_std, %inst_names_std, $fulldeps, @installed_std, $updates, @update_names, @overrides);

my (@install_list, @template_list, @upgrade_list, @remove_list);
my (@solibs_missing, @incompatible_perl, @incompatible_python, @incompatible_ruby);

# Finding as much of this stuff as possible at the start
# gets the cache going a little and makes navigation snappier
# afterwards. "Loading..." takes less time after the first run.
if (-s $slackbuilds_txt) {
	@available = get_all_available();
	$available = +{ map {; $_, $_ } @available };
	for (@available) { push @overrides, $_ if is_local($_); }
	$all_fulldeps = get_reverse_reqs($available);
	@installed = @{ get_installed_packages('SBO') };
	$inst_pkgs = +{ map {; $_->{name}, $_->{pkg} } @installed };
	$inst_vers = +{ map {; $_->{name}, $_->{version} } @installed };
	$inst_names{$_->{name}} = $_ for @installed;
	$fulldeps = get_reverse_reqs($inst_pkgs);
	@installed_std = @{ get_installed_packages('STD') };
	$inst_pkgs_std = +{ map {; $_->{name}, $_->{pkg} } @installed_std };
	$inst_names_std{$_->{name}} = $_ for @installed_std;
	$inst_vers_std = +{ map {; $_->{name}, $_->{version} } @installed_std };
	$updates = $config{BUILD_IGNORE} eq "TRUE" ? get_available_updates("VERS") : get_available_updates("BOTH");
	if ($updates) {
		for (@$updates) { push @update_names, $_->{name} unless on_blacklist $_->{name}; }
	}
}

# The temporary directory from Build.pm isn't needed; use
# a different one.
if ($< == 0) {
	remove_tree($tempdir) if -d $tempdir;
}
my $sbotool_tempdir = tempdir(CLEANUP => 1, DIR => "/tmp");
my $dialog_input = "$sbotool_tempdir/dialog_input";
my $dialog_output = "$sbotool_tempdir/dialog_output";
my $dialog_exit = "$sbotool_tempdir/dialog_exit";
my $command_output = "$sbotool_tempdir/command_output";

unless ($config_menu) {
	display_main();
} else {
	display_settings();
}

# Once a script has been selected, build a menu of available
# operations and carry out the user's choice. Accepts the name
# of a SlackBuild (mandatory) and a non-zero value to view the
# second Operations menu.
sub build_operations {
	script_error("build_operations requires at least one argument.") unless @_ >= 1;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	my ($sbo, $display_extras) = @_;
	my $base_name = $sbo;
	$base_name =~ s/-compat32$//;
	my $options_file = "/var/log/sbotools/$base_name";
	my $is_compat_build = $sbo =~ /-compat32$/;
	my $compat = $is_compat_build ? $sbo : "$sbo-compat32";
	splice @reverse_concluded;
	unlink $command_output;
	# In-tree SlackBuilds only.
	my $location = get_sbo_location($sbo);
	return unless $location;
	my ($unsupported, $compat_possible);
	if ($arch !~ /64/) {
		$unsupported = check_x64 $location;
	} elsif (check_multilib()) {
		$compat_possible = 1 unless ineligible_compat $location;
	} else {
		$unsupported = check_x32 $location;
	}
	my $sbo_version = get_from_info(LOCATION => $location, GET => 'VERSION')->[0];
	my $description = get_sbo_description($sbo);
	$description = "(No Description)" unless defined $description;
	$description =~ s/"/'/g;
	$description =~ s/\$/\\\$/g;
	# Top matter with basic information about the SlackBuild.
	my $msg = "$sbo $sbo_version\n$description";
	$msg .= "\nFound in local overrides." if is_local($sbo);
	$msg .= "\nBlacklisted." if on_blacklist($sbo);
	if (in $sbo, @update_names) {
		$msg .= "\nUpgradable. ";
		$msg .= $inst_vers->{$sbo} eq $sbo_version ? "(build bump)" : "($inst_vers->{$sbo} > $sbo_version)";
	} else {
		$msg .= "\nInstalled. ($inst_vers->{$sbo})" if $inst_names{$sbo};
	}
	$msg .= "\nInstalled, non-SBo. ($inst_vers_std->{$sbo})" if $inst_names_std{$sbo};
	unless ($is_compat_build) {
		$msg .= "\ncompat32 installed. ($inst_vers->{$compat})" if $inst_names{$compat};
	}
	$msg .= "\ncompat32 builds are unsupported by anyone in principle." if $is_compat_build;
	$msg .= "\nUnsupported." if $unsupported;
	$msg .= "\nOn the install list." if in $sbo, @install_list;
	$msg .= "\nOn the template list." if in $sbo, @template_list;
	$msg .= "\nOn the upgrade list." if in $sbo, @upgrade_list;
	$msg .= "\nOn the remove list." if in $sbo, @remove_list;
	$msg .= "\n\nRun as root for more functions." unless $< == 0;

	# Reverse dependency and queue information.
	my (@installed_reverse, @available_reverse, @pre_display);
	@pre_display = get_full_reverse($sbo, $inst_pkgs, $fulldeps);
	for (@pre_display) { push @installed_reverse, $_ if get_sbo_location($_); }
	splice @pre_display;
	splice @reverse_concluded;
	@pre_display = get_full_reverse($sbo, $available, $all_fulldeps);
	for (@pre_display) { push @available_reverse, $_ if get_sbo_location($_); }

	my $queue = get_build_queue([$sbo]);
	my @disp_queue;
	unless ($is_compat_build) {
		for (@$queue) { push @disp_queue, $_ if get_sbo_location($_) and $_ ne $sbo; }
	} else {
		for (@$queue) {
			next unless my $dep_loc = get_sbo_location($_);
			next if $_ =~ /-compat32/ and ineligible_compat $dep_loc;
			push @disp_queue, $_ if $_ ne $sbo;
		}
	}

	# @build_operations has the main options; @more_info is for others.
	# Only options with potentially useful information or actions are
	# displayed.
	my (@build_operations, @more_info);
	my @list_menu_tag;
	if ($< == 0) {
		push @list_menu_tag, "remove" if in $sbo, @remove_list;
		push @list_menu_tag, "template" if in $sbo, @template_list;
		push @list_menu_tag, "upgrade" if in $sbo, @upgrade_list;
		push @list_menu_tag, "install" if in $sbo, @install_list;
		unless (on_blacklist($sbo) or $unsupported) {
			if ($inst_names{$sbo}) {
				push @list_menu_tag, "remove", "template";
				push @list_menu_tag, "upgrade" if in $sbo, @update_names;
			} elsif (not $inst_names_std{$sbo}) {
				push @list_menu_tag, "install", "template";
			} else {
				push @list_menu_tag, "template";
			}
		}
		if (@list_menu_tag) {
			@list_menu_tag = uniq @list_menu_tag;
			my $list_menu_tag = @list_menu_tag == 4 ? "For list operations." : "For " . join(", ", @list_menu_tag) . ".";
			push @build_operations, "\"Lists\" \"$list_menu_tag\" \\";
		}
	} else {
		if (in $sbo, @template_list) {
			push @build_operations, "\"Template List (-)\" \"Clear from the Template List.\" \\";
		} else {
			push @build_operations, "\"Template List (+)\" \"Add to the Template List.\" \\" unless on_blacklist($sbo) or $unsupported;
		}
	}
	if ($compat_possible and not $is_compat_build) {
		push @build_operations, "\"compat32\" \"Select -compat32 operations.\" \\" if $inst_names{$compat};
		push @more_info, "\"compat32\" \"Select -compat32 operations.\" \\" unless $inst_names{$compat};
	}
	push @build_operations, "\"View File\" \"View any file in the script directory.\" \\";
	push @build_operations, "\"RevDep (installed)\" \"Dependent packages on the system.\" \\" if @installed_reverse;
	push @more_info, "\"RevDep (all)\" \"All dependent packages in the repo.\" \\" if @available_reverse;
	if (on_blacklist($sbo) or auto_reverse($sbo) or get_optional($sbo) and $< != 0) {
		push @build_operations, "\"Hints\" \"View package hints.\" \\";
	}
	unless (on_blacklist($sbo)) {
		push @more_info, "\"Queue\" \"Select from the build queue.\" \\" if @disp_queue;
		push @more_info, "\"Dry Run (reverse)\" \"How reverse deps would be rebuilt.\" \\" if $inst_names{$sbo} and @installed_reverse;
		unless ($inst_names_std{$sbo} or $unsupported) {
			my $built_or_upgraded;
			if (in $sbo, @update_names) {
				$built_or_upgraded = "upgraded";
			} elsif ($inst_names{$sbo}) {
				$built_or_upgraded = "rebuilt";
			} else {
				$built_or_upgraded = "built";
			}
			push @build_operations, "\"Dry Run\" \"How this package would be $built_or_upgraded.\" \\";
		}
	}
	if (-s $options_file) {
		if ($< == 0) {
			push @build_operations, "\"Build Options\" \"Edit saved build options.\" \\";
		} else {
			push @build_operations, "\"Build Options\" \"View saved build options.\" \\";
		}
	} else {
		push @build_operations, "\"Build Options\" \"Save build options.\" \\" if $< == 0;
	}
	push @more_info, "\"Add Override\" \"Make a local override copy for editing.\" \\" if $overrides_available and not is_local($sbo);
	if (is_local($sbo) and $overrides_available) {
		push @build_operations, "\"Edit Override\" \"\\\$EDITOR, \\\$VISUAL or vi, in that order.\" \\";
		push @more_info, "\"Remove Override\" \"Delete the script from local overrides.\" \\";
	}
	push @more_info, "\"Package Tests\" \"Check solibs, perl, python and ruby.\" \\" if $inst_names{$sbo} or $inst_names_std{$sbo};
	push @build_operations, "\"Edit Hints\" \"Blacklist, auto-rebuild and dependencies.\" \\" if $< == 0;
	if ($inst_names{$sbo} and $< == 0 and not on_blacklist($sbo)) {
		push @build_operations, "\"Remove\" \"Remove with unneeded dependencies.\" \\";
		if (in $sbo, @update_names) {
			push @build_operations, "\"Upgrade\" \"Upgrade the package.\" \\";
			push @more_info, "\"Upgrade (reverse)\" \"Upgrade and rebuild reverse deps.\" \\" if @installed_reverse;
		} else {
			push @more_info, "\"Reverse Rebuild\" \"Rebuild all reverse deps.\" \\" if @installed_reverse;
			if (@disp_queue) {
				push @build_operations,	"\"Reinstall\" \"Reinstall package and deps (optional).\" \\";
			} else {
				push @build_operations,	"\"Reinstall\" \"Reinstall package.\" \\";
			}
		}
	} elsif ($< == 0 and not $inst_names_std{$sbo} and not $unsupported and not on_blacklist($sbo)) {
		push @build_operations, "\"Install\" \"Install the package.\" \\";
	}
	if ($< == 0 and $inst_names_std{$sbo} and not $unsupported and not on_blacklist($sbo)) {
		push @build_operations, "\"Replace\" \"Reinstall from SBo; batch unavailable.\" \\";
	}

	# Don't bother hiding extras unless there are at least
	# nine items total and two "less-common" items.
	my $array_sum = @more_info + @build_operations;
	my $extra_ops = @more_info;
	if (($array_sum < 9 or @more_info < 2) and not defined $display_extras) {
		push @build_operations, @more_info;
		splice @more_info;
	}

	my $build_operations;
	if (defined $display_extras and @more_info) {
		@more_info = sort @more_info;
		push @build_operations, @more_info;
	}
	@build_operations = sort @build_operations;
	if (@more_info) {
		if (defined $display_extras) {
			push @build_operations, "\"less\" \"Show $extra_ops fewer operations.\" \\";
		} else {
			push @build_operations, "\"more\" \"Show $extra_ops more operations.\" \\" if @more_info;
		}
	}

	$build_operations[-1] =~ s/\\$//;
	$build_operations = join "\n", @build_operations;
	($height, $width, $begin_y, $begin_x) = calculate_position("wide", $build_operations, $msg);
	($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --extra-button --extra-label \"Main\" --title \"$sbo Operations\" --menu \"$msg\" $height $width $height $build_operations", @help_operations);
	return if $res == $code_dialog_cancel or $res == $code_dialog_error;
	display_main() if $res == $code_dialog_extra;
	my $option = $output;
	return unless $option;

	if ($option eq "more") { build_operations($sbo, 1); }
	if ($option eq "less") { return; }
	if ($option eq "compat32") { build_operations($compat); }

	if ($option eq "Add Override") {
		unless (-l "$config{LOCAL_OVERRIDES}/$sbo" or -f "$config{LOCAL_OVERRIDES}/$sbo" or is_local $sbo) {
			make_path("$config{LOCAL_OVERRIDES}") unless -d $config{LOCAL_OVERRIDES};
			system("cp -r $location $config{LOCAL_OVERRIDES}") unless -l "$config{LOCAL_OVERRIDES}/$sbo" or -f "$config{LOCAL_OVERRIDES}/$sbo" or is_local $sbo;
			clear_info_store();
			new_package_information();
		}
		if (-l "$config{LOCAL_OVERRIDES}/$sbo" or -f "$config{LOCAL_OVERRIDES}/$sbo") {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide", "Non-directory\n\nAdd");
			system("$dialog_prefix --begin $begin_y $begin_x --ok-label \"Back\" --msgbox \"A non-directory exists at $config{LOCAL_OVERRIDES}/$sbo.\n\nRemove it before adding to Local Overrides.\" $height $width");
		}
	}

	if ($option eq "Dry Run" or $option eq "Dry Run (reverse)") {
		unlink $command_output;
		if ($option eq "Dry Run (reverse)") {
			system("/usr/sbin/sboinstall --wrap --nocolor -D --reverse-rebuild $sbo > $command_output");
		} else {
			if (in $sbo, @update_names) {
				system("/usr/sbin/sboupgrade --wrap --nocolor -D $sbo > $command_output");
			} elsif ($inst_names{$sbo}) {
				system("/usr/sbin/sboinstall --reinstall --wrap --nocolor -D $sbo > $command_output");
			} else {
				system("/usr/sbin/sboinstall --wrap --nocolor -D $sbo > $command_output");
			}
		}
		my $check = slurp $command_output;
		if ($check =~ /\w/) {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide_cond", $check);
			system("$dialog_prefix --begin $begin_y $begin_x --title \"$sbo: Dry Run\" --textbox $command_output $height $width");
		}
	}

	if ($option eq "Edit Hints") { edit_hint($sbo); }

	# Root can edit build options; others can only view.
	if ($option eq "Build Options" and $< != 0) {
		my $build_options = slurp $options_file if -s $options_file;
		if ($build_options) {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide", $build_options, "Run as root\n\n");
			system("DIALOG_CANCEL=$code_dialog_help $dialog_prefix --begin $begin_y $begin_x --ok-label \"Back\" --title \"$sbo: Saved Build Options\" --msgbox \"Run as root to edit build options.\n\n$build_options\" $height $width", @help_options);
		}
	}

	if ($option eq "Build Options" and $< == 0) {
		my $orig_options = slurp $options_file if -s $options_file;
		$orig_options =~ s/"/\\\"/g if defined $orig_options;
		my $no_remove;
		if ($orig_options) {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide", $orig_options, "Remove?\n\n");
			($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --yes-label \"Edit\" --no-label \"Back\" --extra-button --extra-label \"Clear\" --title \"$options_file\" --yesno \"Edit or clear build options for $base_name?\n\n$orig_options\" $height $width", @help_options);
			$no_remove = $res;
			if ($no_remove == $code_dialog_extra) {
				unlink $options_file;
				($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "Options cleared");
				system("$dialog_prefix --begin $begin_y $begin_x --msgbox \"Cleared options for $sbo.\" $height $width") unless -s $options_file;
				build_operations($sbo, $display_extras);
				return;
			} elsif ($no_remove == $code_dialog_cancel) {
				build_operations($sbo, $display_extras);
				return;
			}
		}
		if (defined $no_remove or not $orig_options) {
			my $disp_options = $orig_options if $orig_options;
			BACK: if (defined $disp_options) { # Retain the text box after viewing README.
				($height, $width, $begin_y, $begin_x) = calculate_position("wide", $orig_options, "Current\n\nInput here\n\nLeave blank");
				($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --extra-button --extra-label \"README\" --title \"$sbo: Build Options\" --inputbox \"Enter build options for $sbo in the form VAR=VALUE.\n\n$orig_options\n\nLeave blank to retain existing options.\" $height $width \"$disp_options\"", @help_options);
			} else {
				($height, $width, $begin_y, $begin_x) = calculate_position("wide", "Enter build\n\nInput here");
				($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --extra-button --extra-label \"README\" --title \"$sbo: Build Options\" --inputbox \"Enter build options for $sbo in the form VAR=VALUE.\" $height $width", @help_options);
			}
			# Definitely don't want to continue in this case.
			if ($res == $code_dialog_cancel or $res == $code_dialog_error) {
				build_operations($sbo, $display_extras);
				return;
			}
			my $new_options = $output;
			if ($new_options =~ /\w/) {
				$disp_options = $new_options;
				$disp_options =~ s/"/\\\"/g;
			}
			if ($res == $code_dialog_extra) {
				my $readme = get_readme_contents($location);
				($height, $width, $begin_y, $begin_x) = calculate_position("normal", $readme);
				system("$dialog_prefix --ok-label \"Back\" --begin $begin_y $begin_x --title \"$sbo: README\" --msgbox \"$readme\" $height $width");
				goto BACK;
			}
			if ($new_options =~ /\w/ and $new_options ne $orig_options) {
				($height, $width, $begin_y, $begin_x) = calculate_position("wide", "success or fail");
				unless (save_options($sbo, $new_options)) {
					system("$dialog_prefix --begin $begin_y $begin_x --title \"Failure\" --msgbox \"Saving options failed for $sbo.\" $height $width");
				} else {
					system("$dialog_prefix --begin $begin_y $begin_x --title \"Success\" --msgbox \"Saved build options for $sbo.\" $height $width");
				}
			}
		}
	}

	if ($option eq "Hints") {
		unlink $command_output;
		system("/usr/sbin/sbohints --wrap --nocolor -q $sbo > $command_output");
		my $check = slurp $command_output;
		if ($check =~ /\w/) {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide", $check);
			do_dialog("DIALOG_CANCEL=$code_dialog_help $dialog_prefix --begin $begin_y $begin_x --title \"$sbo: Hints\" --textbox $command_output $height $width", @help_hints);
		} else {
			($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "No hints found");
			do_dialog("DIALOG_CANCEL=$code_dialog_help $dialog_prefix --begin $begin_y $begin_x --ok-label \"Back\" --msgbox \"No hints found for $sbo.\" $height $width", @help_hints);
		}
	}

	if ($option eq "Install" or $option eq "Reinstall" or $option eq "Remove" or $option eq "Reverse Rebuild") {
		my $cmd = "/usr/sbin/sboinstall";
		# The user can decide whether to reinstall dependencies or not.
		if ($option eq "Reinstall") {
			if (@disp_queue) {
				my $disp_queue = join "\n", @disp_queue;
				($height, $width, $begin_y, $begin_x) = calculate_position("wide", "Reinstall deps?\n\n", $disp_queue);
				($res, $output) = do_dialog("$dialog_prefix --title \"Reinstalling $sbo\" --yesno \"Also reinstall dependencies?\n\n$disp_queue\" $height $width");
				$cmd .= " --norequirements" if $res == $code_dialog_cancel;
			}
		}
		$cmd .= " --reinstall" if $option eq "Reinstall";
		$cmd .= " --reverse-rebuild" if $option eq "Reverse Rebuild";
		$cmd .= " $sbo";
		$cmd = $option eq "Remove" ? "/usr/sbin/sboremove $sbo" : $cmd;
		run_command($cmd);
	}

	# @list_menu_tag indicates whether the SlackBuild makes sense in a
	# given list.
	if ($option eq "Lists") {
		my @lists;
		if (in $sbo, @install_list) {
			push @lists, "\"Install List (-)\" \"Clear from Install List.\" \"off\" \\";
		} else {
			push @lists, "\"Install List (+)\" \"Add to Install List.\" \"off\" \\" if in "install", @list_menu_tag;
		}
		if (in $sbo, @remove_list) {
			push @lists, "\"Remove List (-)\" \"Clear from Remove List.\" \"off\" \\";
		} else {
			push @lists, "\"Remove List (+)\" \"Add to Remove List.\" \"off\" \\" if in "remove", @list_menu_tag;
		}
		if (in $sbo, @template_list) {
			push @lists, "\"Template List (-)\" \"Clear from Template List.\" \"off\" \\";
		} else {
			push @lists, "\"Template List (+)\" \"Add to Template List.\" \"off\" \\" if in "template", @list_menu_tag;
		}
		if (in $sbo, @upgrade_list) {
			push @lists, "\"Upgrade List (-)\" \"Clear from Upgrade List.\" \"off\" \\";
		} else {
			push @lists, "\"Upgrade List (+)\" \"Add to Upgrade List.\" \"off\" \\" if in "upgrade", @list_menu_tag;
		}
		$lists[-1] =~ s/\\$//;
		my $lists = join "\n", @lists;
		my $list_option;
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $lists, "Choose one or more options.");
		($res, $list_option) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"$sbo: List Management\" --checklist \"Choose one or more options.\" $height $width $height $lists", @help_list_mgmt);
		if ($res == $code_dialog_cancel or $res == $code_dialog_error) {
			build_operations($sbo, $display_extras);
			return;
		}
		my @list_options = split "\n", $list_option;
		if (in "Install List (+)", @list_options) { push @install_list, $sbo; }
		if (in "Template List (+)", @list_options) { push @template_list, $sbo; }
		if (in "Upgrade List (+)", @list_options) { push @upgrade_list, $sbo; }
		if (in "Remove List (+)", @list_options) { push @remove_list, $sbo; }
		if (in "Install List (-)", @list_options) { @install_list =  grep { !/^\Q$sbo\E$/ } @install_list; }
		if (in "Template List (-)", @list_options) { @template_list =  grep { !/^\Q$sbo\E$/ } @template_list; }
		if (in "Upgrade List (-)", @list_options) { @upgrade_list =  grep { !/^\Q$sbo\E$/ } @upgrade_list; }
		if (in "Remove List (-)", @list_options) { @remove_list = grep { !/^\Q$sbo\E$/ } @remove_list; }
	}

	if ($option eq "Package Tests") {
		my $solib_check_pkg = $inst_names{$sbo} ? $inst_pkgs->{$sbo} : $inst_pkgs_std->{$sbo};
		my $msg;
		my $compatibility_ok = 1;
		unless (solib_check($solib_check_pkg)) {
			$msg .= "Missing shared objects found for $sbo:\n\n$old_libs{$solib_check_pkg}";
			$msg .= "\nPlease note that precompiled binaries do not require rebuilds.";
			push @solibs_missing, $sbo unless in $sbo, @solibs_missing;
		} else {
			$msg .= "No missing shared objects found.";
		}
		my @series_check_res = series_check($solib_check_pkg, "perl", "python", "ruby");
		unless ($series_check_res[0]) {
			$compatibility_ok = 0;
			$msg .= "\n\nIncompatible with system perl.";
			push @incompatible_perl, $sbo unless in $sbo, @incompatible_perl;
		}
		unless ($series_check_res[1]) {
			$compatibility_ok = 0;
			$msg .= "\n\nIncompatible with system python.";
			push @incompatible_python, $sbo unless in $sbo, @incompatible_python;
		}
		unless ($series_check_res[2]) {
			$compatibility_ok = 0;
			$msg .= "\n\nIncompatible with system ruby.";
			push @incompatible_ruby, $sbo unless in $sbo, @incompatible_ruby;
		}
		$msg .= "\n\nTests passed for perl, python and ruby." if $compatibility_ok;
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $msg);
		system("$dialog_prefix --begin $begin_y $begin_x --ok-label \"Back\" --no-collapse --title \"$sbo: Package Tests\" --msgbox \"$msg\" $height $width");
	}

	# The Queue option does not appear if it is a queue of one.
	if ($option eq "Queue") { display_builds("$sbo: Available Queue", @disp_queue); }

	if ($option eq "Remove Override") {
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", "Remove?");
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --yesno \"Remove $sbo from the Local Overrides directory?\" $height $width");
		if ($res == $code_dialog_ok) {
			remove_tree($location);
			clear_info_store();
			new_package_information();
			$location = get_sbo_location($sbo);
			unless ($location) {
				($height, $width, $begin_y, $begin_x) = calculate_position("wide", "No longer");
				($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --msgbox \"$sbo is no longer available.\" $height $width");
			}
		}
	}

	if ($option eq "Replace") {
		my $cmd = "/usr/bin/sboinstall --reinstall $sbo";
		run_command($cmd, 1);
	}

	if ($option eq "RevDep (all)") { display_builds("$sbo: Available Reverse Dependencies", @available_reverse); }

	if ($option eq "RevDep (installed)") { display_builds("$sbo: Installed Reverse Dependencies", @installed_reverse); }

	if ($option eq "Template List (+)") { push @template_list, $sbo; }
	if ($option eq "Template List (-)") { @template_list = grep { !/^\Q$sbo\E$/ } @template_list; }

	if ($option =~ /^Upgrade/) {
		my $cmd = "/usr/sbin/sboupgrade";
		$cmd .= " --reverse-rebuild" if $option =~ /reverse/;
		$cmd .= " $sbo";
		run_command($cmd);
	}

	# Text files in the SlackBuild directory.
	if ($option eq "View File" or $option eq "Edit Override") {
		my (@file_list, %file_locations, @display_list);
		FILES: splice @file_list;
		splice @display_list;
		%file_locations = ();
		unlink $dialog_output;
		find({ wanted => sub { push @file_list, $_ if -T $_ }, no_chdir => 1 }, $location);
		my $file_list = join "\n", @file_list;
		for (@file_list) {
			my $base = basename $_;
			$file_locations{$base} = $_;
			push @display_list, "\"$base\" \"\"";
		}
		@display_list = sort @display_list;
		my $display_list = join " ", @display_list;
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $file_list, "Select a");
		if ($option eq "View File") {
			($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"View File\" --menu \"Select a text file to read it.\" $height $width $height $display_list");
		} else {
			($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --extra-button --extra-label \"Delete\" --title \"Edit File\" --menu \"Select a text file to edit it.\" $height $width $height $display_list");
		}
		my $file = $output;
		if ($option eq "View File" and $file =~ /\w/) {
			chomp(my $file_text = slurp $file_locations{$file});
			my $wanted = $file =~ /^README/ ? "normal" : "wide";
			($height, $width, $begin_y, $begin_x) = calculate_position($wanted, $file_text);
			system("$dialog_prefix --begin $begin_y $begin_x --title \"$file\" --textbox $file_locations{$file} $height $width");
			goto FILES;
		} elsif ($option eq "Edit Override" and $file =~ /\w/) {
			unless (-w $file_locations{$file}) {
				($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "no write\nselect");
				system("$dialog_prefix --begin $begin_y $begin_x --title \"$file\" --msgbox \"No write permissions.\n\nSelect another file.\" $height $width");
				goto FILES;
			}
			if ($res == $code_dialog_extra) {
				if ($file eq "$base_name.info") {
					($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "needed\nuse");
					system("$dialog_prefix --begin $begin_y $begin_x --title \"$file\" --msgbox \"$file cannot be deleted here.\n\nUse Remove Override.\" $height $width");
					goto FILES;
				}
				($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "delete?");
				($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"$file\" --yesno \"Really delete $file?\" $height $width");
				unlink $file_locations{$file} if $res == $code_dialog_ok;
				goto FILES;
			}
			system("$editor $file_locations{$file}");
			clear_info_store();
			new_package_information();
			goto FILES;
		}
	}

	build_operations($sbo, $display_extras);
	return;
}

# Calculate an appropriate size and position for a dialog window based on
# the dimensions of the terminal window and the contents.
#
# In case the difference between the rows in the terminal and the height
# is odd, leave more space at the top. Horizontal space needs no adjustment
# because the difference is always even.
sub calculate_position {
	script_error("calculate_position requires at least two arguments; exiting.") unless @_;

	my $max_height = terminal_check();

	my $wanted_width = shift;
	my ($contents, $msg) = @_;
	my $height = $contents =~ tr/\n//;
	$height++ unless $contents =~ m/\n$/;
	if (defined $msg) {
		$height += $msg =~ tr/\n//;
		$height++ unless $msg =~ m/\n$/;
	}
	$height += 5;
	$height = $height > $max_height ? $max_height : $height;

	my $width;
	if ($wanted_width eq "wide") {
		$width = $wide;
	} elsif ($wanted_width eq "normal") {
		$width = $normal;
	} elsif ($wanted_width eq "narrow") {
		$width = $narrow;
	} elsif ($wanted_width eq "wide_cond") {
		$width = $normal < $wide ? $wide : $normal;
	} elsif ($wanted_width eq "normal_cond") {
		$width = $normal < $wide ? $normal : $wide;
	}
	script_error("Bad value of wanted_width passed to calculate_position.") unless defined $width;

	my $begin_y;
	if ($height < $max_height) {
		$begin_y = int(($terminal_height - $height) / 2);
	} else {
		$begin_y = int(0.5 + ($terminal_height - $height) / 2);
	}
	my $begin_x = int(($terminal_width - $width) / 2);

	return ($height, $width, $begin_y, $begin_x);
}

# Ensure that a provided dialogrc file is not malformed.
sub dialog_check {
	unlink $dialog_exit;
	my ($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "Checking...");
	system("$dialog_prefix --begin $begin_y $begin_x --infobox \"Checking dialogrc...\" $height $width > /dev/null ; echo \$? > $dialog_exit");
	chomp(my $res = slurp $dialog_exit);
	if ($res == $code_dialog_error) {
		($height, $width, $begin_y, $begin_x) = calculate_position("normal_cond", "Checking...");
		system("dialog --begin $begin_y $begin_x --msgbox \"The provided dialogrc is malformed. Using the system default.\" $height $width") unless $config{DIALOGRC} eq "FALSE";

		$config{DIALOGRC} = "FALSE";
		undef $dialogrc;
		$dialog_prefix = $dialog_base;
	}
}

# Given an array with SlackBuilds of interest, add descriptions,
# sort and get user input.
sub display_builds {
	script_error("display_builds requires two arguments.") unless @_ > 1;
	my ($label, @array_to_use) = @_;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	my @for_dialog;
	for my $sbo (@array_to_use) {
		next unless get_sbo_location($sbo);
		my $description = get_sbo_description($sbo);
		$description = "(No Description)" unless defined $description;
		$description =~ s/"/'/g;
		$description = is_local($sbo) ? "(LOCAL) $description" : $description unless $label =~ /Overrides/;
		$description = $inst_names_std{$sbo} ? "(NON-SBO) $description" : $description;
		if (in $sbo, @update_names) {
			my $sbo_version = get_from_info(LOCATION => get_sbo_location($sbo), GET => 'VERSION')->[0];
			my $upgrade_description = $inst_vers->{$sbo} eq $sbo_version ? "(build bump)" : "($inst_vers->{$sbo} > $sbo_version)";
			$description = "$upgrade_description $description";
		}
		push @for_dialog, "\"$sbo\" \"$description\" \"$description\" \\";
	}
	@for_dialog = sort @for_dialog unless $label =~ /(Available Queue|Search Results|Filtered\))$/;
	my $for_dialog = join "\n", @for_dialog;
	my $fh;
	open $fh, ">", $dialog_input or error_code("Failed to open $dialog_input for write.", _ERR_OPENFH);
	print {$fh} $for_dialog;
	close $fh;
	($height, $width, $begin_y, $begin_x) = calculate_position("wide", $for_dialog, "Select a SlackBuild");
	$height--;
	my $do_filter;
	if ($label =~ /(Missing|Perl|Python|Ruby)/ and $< == 0) {
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $for_dialog, "Select a SlackBuild\n\nuse\n\nplease");
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --extra-button --extra-label \"Reinstall\" --title \"$label\" --item-help --menu \"Select a SlackBuild for more options.\n\nUse Reinstall to reinstall everything on the list.\n\nPlease note that precompiled binaries do not require rebuilds.\" $height $width $height --file $dialog_input", @help_builds_fail_menu);
		if ($res == $code_dialog_extra) {
			my $cmd = "/usr/sbin/sboinstall --reinstall";
			my $need_dependency_prompt = 0;
			for my $missing (@array_to_use) {
				my $queue = get_build_queue([$missing]);
				my @disp_queue;
				unless ($missing =~ /compat32$/) {
					for (@$queue) { push @disp_queue, $_ if get_sbo_location($_) and $_ ne $missing; }
				} else {
					for (@$queue) {
						next unless my $dep_loc = get_sbo_location($_);
						next if $_ =~ /-compat32/ and ineligible_compat $dep_loc;
						push @disp_queue, $_ if $_ ne $missing;
					}
				}
				if (@disp_queue) {
					$need_dependency_prompt = 1;
					last;
				}
			}
			if ($need_dependency_prompt) {
				($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "Dependencies?");
				($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Rebuild Dependencies?\" --yesno \"Also rebuild dependencies?\" $height $width");
				$cmd .= " --norequirements" unless $res == $code_dialog_ok;
			}
			run_command("$cmd @array_to_use");
			return;
		}
	} elsif (@array_to_use > 1) {
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --extra-button --extra-label \"Filter\" --title \"$label\" --item-help --menu \"Select a SlackBuild for more options.\" $height $width $height --file $dialog_input", @help_builds);
		# Run a secondary search on the results if the user presses the
		# "Filter" button.
		$do_filter = 1 if $res == $code_dialog_extra;
	} else {
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"$label\" --item-help --menu \"Select a SlackBuild for more options.\" $height $width $height --file $dialog_input", @help_builds);
	}
	my $option = $output;
	return unless $option;
	if (defined $do_filter) {
		run_search($label, @array_to_use);
	} else {
		build_operations($option);
	}
	display_builds($label, @array_to_use);
	return;
}

# An interface for sboclean.
sub display_clean_menu {
	my (@clean_menu, $has_distfiles, $has_options, $has_working);
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	if (-d "$config{SBO_HOME}/distfiles") {
		opendir(my $handle, "$config{SBO_HOME}/distfiles");
		while (my $dir = readdir $handle) {
			unless (in_regexp($dir => qw/ . .. /)) { $has_distfiles = 1; last; }
		}
	}
	if (-d "$tmpd") {
		opendir(my $handle, $tmpd);
		while (my $dir = readdir $handle) {
			unless (in_regexp($dir => qw/ . .. /)) { $has_working = 1; last; }
		}
	}
	if (-d "/var/log/sbotools") {
		opendir(my $handle, "/var/log/sbotools");
		while (my $dir = readdir $handle) {
			unless (in_regexp($dir => qw/ . .. /)) { $has_options = 1; last; }
		}
	}

	push @clean_menu, "\"Distfiles\" \"Downloaded source files.\" \"off\" \\" if $has_distfiles;
	push @clean_menu, "\"Options\" \"Per-script build options.\" \"off\" \\" if $has_options;
	push @clean_menu, "\"Working\" \"Working directories.\" \"off\" \\" if $has_working;
	$clean_menu[-1] =~ s/\\$//;
	my $clean_menu = join "\n", @clean_menu;
	($height, $width, $begin_y, $begin_x) = calculate_position("wide", $clean_menu, "At least one");
	($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"sboclean\" --checklist \"At least one option must be checked.\" $height $width $height $clean_menu", @help_clean);
	return if $res == $code_dialog_cancel or $res == $code_dialog_error;
	my $options = $output;
	return unless $options =~ /\w/;
	my @options = split "\n", $options;
	my $cmd = "/usr/sbin/sboclean";
	$cmd .= " -d" if in "Distfiles", @options;
	$cmd .= " -w" if in "Working", @options;
	if (in "Options", @options) {
		my @with_options;
		for (glob "/var/log/sbotools/*") {
			next unless -T $_;
			my $name = basename($_);
			push @with_options, "\"$name\" \"\" \\";
		}
		unless (@with_options) {
			($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "one line");
			system("$dialog_prefix --begin $begin_y $begin_x --title \"sboclean\" --msgbox \"No text files found in /var/log/sbotools.\" $height $width");
			return;
		}
		@with_options = sort @with_options;
		push @with_options, "\"\" \"\" \\", "\"ALL\" \"Clear all saved options.\"" if @with_options > 1;
		$with_options[-1] =~ s/\\$// if @with_options;
		my $with_options = join "\n", @with_options;
		BACK: ($height, $width, $begin_y, $begin_x) = calculate_position("wide", $with_options, "Choose one");
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"sboclean\" --menu \"Choose one script for option clearing, or ALL.\" $height $width $height $with_options");
		return if $res == $code_dialog_cancel or $res == $code_dialog_error;
		my $script_choice = $output;
		goto BACK unless $script_choice =~ /\w/;
		$cmd .= " -o $script_choice";
	}

	run_command($cmd);
	display_main();
}

# Select and read a man page.
sub display_man_pages {
	unlink $dialog_output;
	my @man_pages;
	push @man_pages, "\"sbotools\" \"Executive summaries of all tools.\" \\",
					"\"sbotool\" \"The man page for this utility.\" \\",
					"\"sbocheck\" \"Update the repo and perform checks.\" \\",
					"\"sboclean\" \"Remove sbotools-related cruft.\" \\",
					"\"sboconfig\" \"A command line settings manager.\" \\",
					"\"sbofind\" \"Search the local repo for SlackBuilds.\" \\",
					"\"sbohints\" \"Query and modify per-script hints.\" \\",
					"\"sboinstall\" \"Install SlackBuilds and dependencies.\" \\",
					"\"sboremove\" \"Interactively remove SlackBuilds.\" \\",
					"\"sboupgrade\" \"Upgrade SBo SlackBuilds.\" \\",
					"\"sbotools.colors\" \"Customize sbotools output colors.\" \\",
					"\"sbotools.conf\" \"Set the configuration with this file.\" \\",
					"\"sbotools.hints\" \"Set hints with this file.\"";
	my $man_pages = join "\n", @man_pages;
	my $msg = "Each of the scripts in sbotools can run independently from the command line.\n\nSee the man pages for further details.";
	my ($height, $width, $begin_y, $begin_x) = calculate_position("wide", $man_pages, $msg);
	system("$dialog_prefix --begin $begin_y $begin_x --title \"Man Pages\" --menu \"$msg\" $height $width $height $man_pages 2> $dialog_output");
	chomp(my $option = slurp $dialog_output);
	return unless $option =~ /\w/;
	system("man $option");
	display_man_pages();
	return;
}

# Setting up and displaying the main menu; the setup is similar to
# build_operations() above; determine efficacious settings and push
# them to a menu array.
sub display_main {
	my @main_menu;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	if ($< == 0) {
		push @main_menu, "\"Settings\" \"View and edit sbotools settings.\" \\";
	} else {
		push @main_menu, "\"Settings\" \"View sbotools settings.\" \\";
	}
	my @list_message;
	my $list_string;
	push @list_message, "install" if @install_list;
	push @list_message, "make template" if @template_list;
	push @list_message, "upgrade" if @upgrade_list;
	push @list_message, "remove" if @remove_list;
	if (@list_message == 4) {
		$list_string = "install, make template, upgrade or remove";
	} elsif (@list_message == 3) {
		$list_string = "$list_message[0], $list_message[1] or $list_message[2]";
	} elsif (@list_message == 2) {
		$list_string = "$list_message[0] or $list_message[1]";
	} elsif (@list_message == 1) {
		$list_string = $list_message[0];
	}
	push @main_menu, "\"List Operations\" \"Act on the $list_string list.\" \\" if $list_string;
	push @main_menu, "\"Hints\" \"View per-script hints.\" \\" if $listings[0] ne "NULL";
	push @main_menu, "\"Missing Solibs\" \"Packages with known-missing solibs.\" \\" if @solibs_missing;
	push @main_menu, "\"Perl\" \"Incompatible perl packages.\" \\" if @incompatible_perl;
	push @main_menu, "\"Python\" \"Incompatible python packages.\" \\" if @incompatible_python;
	push @main_menu, "\"Ruby\" \"Incompatible ruby packages.\" \\" if @incompatible_ruby;
	push @main_menu, "\"Man Pages\" \"Read sbotools-related man pages.\" \\";
	push @main_menu, "\"Package Tests\" \"Solibs, perl, python and ruby checks.\" \\";
	my @for_installed_list;
	if (-s $slackbuilds_txt) {
		push @main_menu, "\"Browse Repository\" \"View scripts by series.\" \\";
		if (-s "$config{SBO_HOME}/repo/TAGS.txt") {
			push @main_menu, "\"Package Search\" \"Search by name, tag and description.\" \\";
		} else {
			push @main_menu, "\"Package Search\" \"Search by name and description.\" \\";
		}
		@for_installed_list = keys %inst_names if keys %inst_names;
		for (keys %inst_names_std) {
			push @for_installed_list, $_ if get_sbo_location($_);
		}
		push @main_menu, "\"Installed\" \"List installed SBo packages.\" \\",
						"\"Rebuilds\" \"Mass and per-series rebuilds.\" \\" if @for_installed_list;
		push @main_menu, "\"Upgradable\" \"Upgrades are available.\" \\" if @update_names;
		push @main_menu, "\"Overrides\" \"View scripts in local overrides.\" \\" if @overrides;
		if (@update_names) {
			push @main_menu, "\"Upgrade All (dry run)\" \"How all upgrades would be done.\" \\";
			push @main_menu, "\"Upgrade All\" \"Apply all available upgrades.\" \\" if $< == 0;
		}
	}
	if ($< == 0) {
		push @main_menu, "\"Fetch Repository\" \"Get or update the repository.\" \\";
		my (@clean_string, $clean_msg, $has_distfiles, $has_options, $has_working);
		if (-d "$config{SBO_HOME}/distfiles") {
			opendir(my $handle, "$config{SBO_HOME}/distfiles");
			while (my $dir = readdir $handle) {
				unless (in_regexp($dir => qw/ . .. /)) { $has_distfiles = 1; last; }
			}
		}
		if (-d "$tmpd") {
			opendir(my $handle, $tmpd);
			while (my $dir = readdir $handle) {
				unless (in_regexp($dir => qw/ . .. /)) { $has_working = 1; last; }
			}
		}
		if (-d "/var/log/sbotools") {
			opendir(my $handle, "/var/log/sbotools");
			while (my $dir = readdir $handle) {
				unless (in_regexp($dir => qw/ . .. /)) { $has_options = 1; last; }
			}
		}
		push @clean_string, "source" if $has_distfiles;
		push @clean_string, "working dirs" if $has_working;
		push @clean_string, "build options" if $has_options;
		if (@clean_string == 3) {
			$clean_msg = "Source, working dirs, build options.";
		} elsif (@clean_string == 2) {
			$clean_msg = "Saved $clean_string[0] and $clean_string[1].";
		} elsif (@clean_string == 1) {
			$clean_msg = "Saved $clean_string[0].";
			$clean_msg =~ s/working dirs/working directories/;
		}
		push @main_menu, "\"Clean sbotools Files\" \"$clean_msg\" \\" if $clean_msg;
	}
	@main_menu = sort @main_menu;
	# Refresh always goes at the bottom of the screen.
	push @main_menu, "\"\" \"\" \\", "\"Refresh\" \"Run in case of outside changes.\"";
	my $main_menu = join "\n", @main_menu;
	my $msg = "Welcome to the sbotools TUI.";
	unless (-s $slackbuilds_txt) {
		if ($< == 0) {
			$msg .= "\n\nSelect \\\"Fetch Repository\\\" to download a copy of the repo.";
		} else {
			$msg .= "\n\nAs root, select \\\"Fetch Repository\\\" for a copy of the repo.";
		}
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $main_menu, $msg);
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --cancel-label \"Exit\" --title \"Main Menu\" --menu \"$msg\" $height $width $height $main_menu", @help_main);
	} else {
		$msg .= " Navigate to scripts for actions.";
		$msg .= "\n\nFetch the repository to generate descriptions." unless $descriptions_generated;
		$msg .= "\n\nRun as root for more options." unless $< == 0;
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $main_menu, $msg);
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --cancel-label \"Exit\" --title \"Main Menu\" --menu \"$msg\" $height $width $height $main_menu", @help_main);
	}
	exit 0 if $res == $code_dialog_cancel or $res == $code_dialog_error;
	my $option = $output;
	display_main() unless $option;

	if ($option eq "Fetch Repository") {
		my $fetch_time = time();
		system("clear");
		my $fetch_res = system("/usr/sbin/sbocheck") == 0;
		clear_info_store();
		if ($fetch_res) {
			new_package_information();
			parse_solib_log("/var/log/sbocheck-solibs.log", $fetch_time);
			parse_solib_log("/var/log/sbocheck-perl.log", $fetch_time);
			parse_solib_log("/var/log/sbocheck-python.log", $fetch_time);
			parse_solib_log("/var/log/sbocheck-ruby.log", $fetch_time);
		}
		prompt $color_notice, "Press ENTER to continue.";
	}

	if ($option eq "Hints") {
		unlink $command_output;
		system("/usr/sbin/sbohints --wrap --nocolor --list > $command_output");
		my $hints_output = slurp $command_output;
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $hints_output);
		do_dialog("DIALOG_CANCEL=$code_dialog_help $dialog_prefix --begin $begin_y $begin_x --title \"Per-Script Hints\" --textbox $command_output $height $width", @help_hints);
	}

	if ($option eq "Refresh") {
		$config{$_} = "FALSE" for (keys %config);
		read_config();
		lint_sbo_config($self, %config);
		unless (defined $dialogrc and -s $dialogrc) {
			$dialog_prefix = ($config{DIALOGRC} ne "FALSE" and -s $config{DIALOGRC}) ? "DIALOGRC=\"$config{DIALOGRC}\" $dialog_base" : $dialog_base;
		}
		$repo_path = "$config{SBO_HOME}/repo";
		$slackbuilds_txt = "$repo_path/SLACKBUILDS.TXT";
		if (-s $slackbuilds_txt) {
			clear_info_store();
			new_package_information();
		} else {
			dialog_check();
		}
	}

	if ($option eq "Package Tests") {
		if (-s $slackbuilds_txt) {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide", "All Installed\nAll SBo", "Check missing");
			($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Package Tests\" --menu \"Check intalled packages for missing shared object\ndependencies and perl, python or ruby incompatibility.\" $height $width $height \"All Installed\" \"All installed: solibs, perl, python and ruby.\" \"All SBo\" \"All SBo: solibs, perl, python and ruby.\"", @help_solibs);
			display_main() if $res == $code_dialog_cancel or $res == $code_dialog_error;
			my $so_option = $output;
			if ($option =~ /\w/) {
				my $log_dir = $< == 0 ? '/var/log' : '/tmp';
				my $command_time = time();
				system("clear");
				system("/usr/sbin/sbocheck -X --type all") if $so_option eq "All SBo";
				system("/usr/sbin/sbocheck -C --type all") if $so_option eq "All Installed";
				parse_solib_log("$log_dir/sbocheck-solibs.log", $command_time);
				parse_solib_log("$log_dir/sbocheck-perl.log", $command_time);
				parse_solib_log("$log_dir/sbocheck-python.log", $command_time);
				parse_solib_log("$log_dir/sbocheck-ruby.log", $command_time);
				prompt $color_notice, "Press ENTER to continue.";
			}
		} else { # All-package checks can be done in all cases.
			system("clear");
			system("/usr/sbin/sbocheck -C --type all");
			prompt $color_notice, "Press ENTER to continue.";
		}
	}

	if ($option eq "Upgrade All (dry run)") {
		unlink $command_output;
		system("/usr/sbin/sboupgrade --wrap --nocolor -D --all > $command_output");
		my $dry_run_output = slurp $command_output;
		($height, $width, $begin_y, $begin_x) = calculate_position("wide_cond", $dry_run_output);
		system("$dialog_prefix --begin $begin_y $begin_x --title \"Upgrade All: Dry Run\" --textbox $command_output $height $width");
	}

	if ($option eq "Browse Repository") { display_series(); }
	if ($option eq "Clean sbotools Files") { display_clean_menu(); }
	if ($option eq "Installed") { display_builds("Installed SBo SlackBuilds", @for_installed_list); }
	if ($option eq "List Operations") { display_user_lists(); }
	if ($option eq "Man Pages") { display_man_pages(); }
	if ($option eq "Missing Solibs") { display_builds("SlackBuilds with Known Missing Solib Dependencies", @solibs_missing); }
	if ($option eq "Perl") { display_builds("SlackBuilds Built Against the Wrong Perl", @incompatible_perl); }
	if ($option eq "Python") { display_builds("SlackBuilds Built Against the Wrong Python", @incompatible_python); }
	if ($option eq "Overrides") { display_builds("SlackBuilds in Local Overrides", @overrides); }
	if ($option eq "Package Search") { run_search(); }
	if ($option eq "Rebuilds") { display_rebuild_menu(); }
	if ($option eq "Ruby") { display_builds("SlackBuilds Built Against the Wrong Ruby", @incompatible_ruby); }
	if ($option eq "Settings") { display_settings(); }
	if ($option eq "Upgradable") { display_builds("Upgradable SBo Packages", @update_names); }
	if ($option eq "Upgrade All") { run_command("/usr/sbin/sboupgrade --all"); }

	display_main();
	return;
}

# Perform a large-scale rebuild or dry run; for series,
# show the number of packages installed first.
sub display_rebuild_menu {
	unlink $command_output;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	my @rebuild_menu;
	push @rebuild_menu, "\"Mass Rebuild (dry run)\" \"Dry run for an all-SBo rebuild.\" \\",
						"\"Series Rebuild (dry run)\" \"Dry run for a series rebuild.\" \\",
						"\"Series Reverse (dry run)\" \"Dry run, series plus reverse deps.\" \\";
	if ($< == 0) {
		push @rebuild_menu, "\"Mass Rebuild\" \"Rebuild all SBo packages.\" \\",
							"\"Series Rebuild\" \"Rebuild all packages in a series.\" \\",
							"\"Series Reverse\" \"Rebuild a series with reverse deps.\" \\";
	}
	@rebuild_menu = sort @rebuild_menu;
	$rebuild_menu[-1] =~ s/\\$//;
	my $rebuild_menu = join "\n", @rebuild_menu;
	($height, $width, $begin_y, $begin_x) = calculate_position("wide", $rebuild_menu, "Large-scale rebuilds");
	($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Large-Scale Rebuilds\" --menu \"Rebuild all SBo packages, or everything in one or more series.\" $height $width $height $rebuild_menu", @help_rebuilds);
	return if $res == $code_dialog_cancel or $res == $code_dialog_error;
	my $option = $output;

	if ($option eq "Mass Rebuild (dry run)") {
		system("/usr/sbin/sboinstall --wrap --nocolor --mass-rebuild -D > $command_output");
		my $check = slurp $command_output;
		if ($check =~ /\w/) {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide_cond", $check);
			system("$dialog_prefix --begin $begin_y $begin_x --title \"Mass Rebuild Dry Run\" --textbox $command_output $height $width");
		}
	}
	if ($option eq "Mass Rebuild") {
		# If resume.temp is present, it indicates a previous failure;
		# make sure the user wants to pick up from there.
		($height, $width, $begin_y, $begin_x) = calculate_position("normal_cond", "Picking up");
		if (-s "$config{SBO_HOME}/resume.temp") {
			($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Mass Rebuild\" --yesno \"A previous mass rebuild failed. Pick up from the next script?\" $height $width");
			unless ($res == $code_dialog_ok) {
				($height, $width, $begin_y, $begin_x) = calculate_position("normal_cond", "Picking up");
				system("$dialog_prefix --begin $begin_y $begin_x --title \"Mass Rebuild\" --msgbox \"Delete the file at $config{SBO_HOME}/resume.temp to start over.\" $height $width");
				return;
			}
		}
		run_command("/usr/sbin/sboinstall --mass-rebuild");
		if (-s "$config{SBO_HOME}/resume.temp") {
			($height, $width, $begin_y, $begin_x) = calculate_position("normal_cond", "Picking up");
			system("$dialog_prefix --begin $begin_y $begin_x --ok-label \"OK\" --title \"Mass Rebuild\" --msgbox \"The next mass rebuild attempt will start from after the failed script.\" $height $width");
		}
	}

	if ($option =~ /^Series/) {
		unlink $command_output;
		my (@active_series, @series_selection_menu, %package_counts);
		for (keys %inst_names) {
			my $loc = get_orig_location($_);
			next unless $loc;
			my $full_series = dirname($loc);
			push @active_series, $full_series;
			if (exists $package_counts{$full_series}) {
				$package_counts{$full_series}++;
			} else {
				$package_counts{$full_series} = 1;
			}
		}
		@active_series = sort(uniq(@active_series));
		for (@active_series) {
			my $base = basename $_;
			push @series_selection_menu, "\"$base\" \"$package_counts{$_}\" \"off\" \\";
		}
		$series_selection_menu[-1] =~ s/\\$//;
		my $series_selection_menu = join "\n", @series_selection_menu;
		($height, $width, $begin_y, $begin_x) = calculate_position("narrow", $series_selection_menu, "The following\n\nAt least");
		SELECT_SERIES: ($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --cancel-label \"Cancel\" --title \"$option\" --checklist \"The following series have installed packages.\n\nSelect at least one.\" $height $width $height $series_selection_menu");
		return if $res == $code_dialog_cancel or $res == $code_dialog_error;
		my $series_choice = $output;
		goto SELECT_SERIES unless $series_choice =~ /\w/;
		$series_choice =~ s/\n/\,/g;
		$series_choice =~ s/\,$//g;

		if ($option =~ /dry run/) {
			my $cmd = "/usr/sbin/sboinstall --wrap --nocolor -D";
			$cmd .= $option =~ /Reverse/ ? " --reverse-rebuild --series-rebuild $series_choice" : " --series-rebuild $series_choice";
			system("$cmd > $command_output");
			my $check = slurp $command_output;
			if ($check =~ /\w/) {
				($height, $width, $begin_y, $begin_x) = calculate_position("wide_cond", $check);
				system("$dialog_prefix --begin $begin_y $begin_x --title \"Series Rebuild Dry Run\" --textbox $command_output $height $width");
			}
		}
		if ($option eq "Series Rebuild") { run_command("/usr/sbin/sboinstall --series-rebuild $series_choice"); }
		if ($option eq "Series Reverse") { run_command("/usr/sbin/sboinstall --reverse-rebuild --series-rebuild $series_choice"); }
	}

	display_rebuild_menu();
	return;
}

# Show a list of available series, get user input and make an array of
# included SlackBuilds for display_builds().
sub display_series {
	unlink $dialog_input;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	my @series_display;
	for (glob "$repo_path/*") {
		next if in_regexp $_, qw/ . .. /;
		next unless -d $_;
		my $series = basename $_;
		push @series_display, "\"$series\" \"$_\" \\";
	}
	display_main() unless @series_display;
	@series_display = sort @series_display;
	my $series_display = join "\n", @series_display;
	my $series_fh;
	open $series_fh, ">", $dialog_input or error_code("Failed to open $dialog_input for write.", _ERR_OPENFH);
	print {$series_fh} $series_display;

	my @to_display;
	($height, $width, $begin_y, $begin_x) = calculate_position("wide", $series_display, "Choose a series");
	($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Available Series\" --menu \"Choose a series.\" $height $width $height --file $dialog_input");
	display_main() if $res == $code_dialog_cancel or $res == $code_dialog_error;
	my $series = $output;
	for (@available) {
		my $orig_location = get_orig_location($_);
		next unless $orig_location;
		my $this_series = dirname($orig_location);
		push @to_display, $_ if $this_series =~ m/\/$series$/;
	}

	display_builds("SlackBuilds in $series", @to_display) if @to_display;
	display_series();
	return;
}

# Display settings with explanations; root users can pick one
# to edit in edit_setting(). TRUE/FALSE settings simply toggle.
#
# It may be long, but explanations are good for clarity.
sub display_settings {
	my ($height, $width, $begin_x, $begin_y, $res, $output);
	$config{DIALOGRC} = $save_dialogrc;
	my @config_values;
	my $msg = "Defaults are FALSE except for SBO_HOME; see sbotools.conf(5).";
	$msg = "$msg\n\nPress \\\"OK\\\" to change the highlighted setting.\n\nSettings marked with \\\"+\\\" change between \\\"TRUE\\\" and \\\"FALSE\\\"." if $< == 0;
	my $plus = $< == 0 ? " +" : "";
	push @config_values, "\"BUILD_IGNORE=$config{BUILD_IGNORE}$plus\" \"\" \"If TRUE, ignore build numbers when upgrading.\" \\",
						"\"CLASSIC=$config{CLASSIC}$plus\" \"\" \"If TRUE, use 2.7-style output; No color, rsync default, ignore build number.\" \\",
						"\"COLOR=$config{COLOR}$plus\" \"\" \"If TRUE, use sbotools color output.\" \\",
						"\"CPAN_IGNORE=$config{CPAN_IGNORE}$plus\" \"\" \"If TRUE, ignore perl modules installed from the CPAN.\" \\",
						"\"DIALOGRC=$config{DIALOGRC}\" \"\" \"If a file, use it as dialogrc for sbotool.\" \\",
						"\"DISTCLEAN=$config{DISTCLEAN}$plus\" \"\" \"If TRUE, remove the source and built package after building.\" \\",
						"\"ETC_PROFILE=$config{ETC_PROFILE}$plus\" \"\" \"If TRUE, source all executable *.sh in /etc/profile.d before building.\" \\",
						"\"GIT_BRANCH=$config{GIT_BRANCH}\" \"\" \"Use this non-default branch for git repos.\" \\",
						"\"GPG_VERIFY=$config{GPG_VERIFY}$plus\" \"\" \"If TRUE, verify the repo with GPG after fetching.\" \\",
						"\"JOBS=$config{JOBS}\" \"\" \"If a number, use this many jobs with make.\" \\",
						"\"LOCAL_OVERRIDES=$config{LOCAL_OVERRIDES}\" \"\" \"Use this directory for local override scripts.\" \\",
						"\"LOG_DIR=$config{LOG_DIR}\" \"\" \"If a directory, save build logs there. Logs are otherwise not saved.\" \\",
						"\"NOCLEAN=$config{NOCLEAN}$plus\" \"\" \"If TRUE, do not remove working directories after building.\" \\",
						"\"NOWRAP=$config{NOWRAP}$plus\" \"\" \"If TRUE, disable sbotools word wrapping.\" \\",
						"\"OBSOLETE_CHECK=$config{OBSOLETE_CHECK}$plus\" \"\" \"If TRUE, download the outdated script list and perl history on repo fetch.\" \\",
						"\"PKG_DIR=$config{PKG_DIR}\" \"\" \"If a directory, save built packages there.\" \\",
						"\"REPO=$config{REPO}\" \"\" \"Use this non-default git or rsync repository.\" \\",
						"\"RSYNC_DEFAULT=$config{RSYNC_DEFAULT}$plus\" \"\" \"If TRUE, the default repository is rsync, not git. No effect on -current.\" \\",
						"\"SBO_HOME=$config{SBO_HOME}\" \"\" \"Specify an sbotools directory (default /usr/sbo).\" \\",
						"\"SLACKWARE_VERSION=$config{SLACKWARE_VERSION}\" \"\" \"Use this Slackware version to determine the default repo.\" \\",
						"\"SO_CHECK=$config{SO_CHECK}$plus\" \"\" \"If TRUE, do a solib/perl/python/ruby check on fetch and upgrade.\" \\",
						"\"STRICT_UPGRADES=$config{STRICT_UPGRADES}$plus\" \"\" \"If TRUE, ignore apparent downgrades.\"";
	my $config_values = join "\n", @config_values;
	($height, $width, $begin_y, $begin_x) = calculate_position("wide", $config_values, $msg);
	$height--;
	my $cancel_label = $config_menu ? "Exit" : "Back";
	unless ($< == 0) {
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --ok-label \"$cancel_label\" --no-cancel --extra-button --extra-label \"Man Page\" --title \"Settings List\" --item-help --menu \"$msg\" $height $width $height $config_values");
	} else {
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Settings List\" --extra-button --extra-label \"Man Page\" --cancel-label \"$cancel_label\" --item-help --menu \"$msg\" $height $width $height $config_values");
	}
	unless ($config_menu) {
		display_main() if $res == $code_dialog_cancel or $res == $code_dialog_error;
	} else {
		exit if $res == $code_dialog_cancel or $res == $code_dialog_error;
	}
	if ($res == $code_dialog_extra) {
		system("clear");
		system("man sbotools.conf");
		display_settings();
	}
	my $option = $output;
	$option =~ s/ \+$//;
	if ($< == 0) {
		my ($explanation, $criterion, $flag);
		if ($option =~ /^BUILD_IGNORE/) {
			$explanation = "If TRUE, do not account for build number when doing upgrades.";
			$criterion = "TRUE or FALSE.";
			$flag = "b";
		}
		if ($option =~ /^CLASSIC/) {
			$explanation = "If TRUE, use version 2.7 messaging, ignore build numbers,\nrsync default repo.";
			$criterion = "TRUE or FALSE.";
			$flag = "C";
		}
		if ($option =~ /^COLOR/) {
			$explanation = "If TRUE, sbotools will use color output for emphasis and\nreadability.";
			$criterion = "TRUE or FALSE.";
			$flag = "K";
		}
		if ($option =~ /^CPAN_IGNORE/) {
			$explanation = "If TRUE, ignore installed CPAN packages when installing and\nupgrading.";
			$criterion = "TRUE or FALSE.";
			$flag = "P";
		}
		if ($option =~ /^DIALOGRC/) {
			$explanation = "If a PATH, use that file as dialogrc for sbotool.";
			$criterion = "A PATH or FALSE.";
			$flag = "D";
		}
		if ($option =~ /^DISTCLEAN/) {
			$explanation = "If TRUE, delete downloaded source and the package post-build.";
			$criterion = "TRUE or FALSE.";
			$flag = "d";
		}
		if ($option =~ /^ETC_PROFILE/) {
			$explanation = "If TRUE, source all executable *.sh scripts in /etc/profile.d\nbefore building.";
			$criterion = "TRUE or FALSE.";
			$flag = "e";
		}
		if ($option =~ /^GIT_BRANCH/) {
			$explanation = "If a BRANCH NAME, use a non-default git branch.";
			$criterion = "BRANCH NAME or FALSE.";
			$flag = "B";
		}
		if ($option =~ /^GPG_VERIFY/) {
			$explanation = "If TRUE, use GPG to verify the repository and the obsolete\nscript list.";
			$criterion = "TRUE or FALSE.";
			$flag = "g";
		}
		if ($option =~ /^JOBS/) {
			$explanation = "If a NUMBER, use that many jobs with \"make\".";
			$criterion = "A NUMBER or FALSE.";
			$flag = "j";
		}
		if ($option =~ /^LOCAL_OVERRIDES/) {
			$explanation = "If a PATH, use that directory for local overrides.\n\n/home/*, /root and / are not permitted.";
			$criterion = "A PATH or FALSE.";
			$flag = "o";
		}
		if ($option =~ /^LOG_DIR/) {
			$explanation = "If a PATH, save logs in that directory; logs are otherwise\nnot saved.\n\n/home/*, /root and / are not permitted.";
			$criterion = "A PATH or FALSE.";
			$flag = "L";
		}
		if ($option =~ /^NOCLEAN/) {
			$explanation = "If TRUE, do not delete the working directory after building.";
			$criterion = "TRUE or FALSE.";
			$flag = "c";
		}
		if ($option =~ /^NOWRAP/) {
			$explanation = "If TRUE, suppress word wrapping in sbotools output.";
			$criterion = "TRUE or FALSE.";
			$flag = "w";
		}
		if ($option =~ /^OBSOLETE_CHECK/) {
			$explanation = "If TRUE, download the obsolete script list and perl version\nhistory upon repo fetch.\n\nRelevant only on -current.";
			$criterion = "TRUE or FALSE.";
			$flag = "O";
		}
		if ($option =~ /^PKG_DIR/) {
			$explanation = "If a PATH, save built packages in that directory.\n\n/home/*, /root and / are not permitted.";
			$criterion = "A PATH or FALSE.";
			$flag = "p";
		}
		if ($option =~ /^REPO/) {
			$explanation = "If a git or rsync URL, use that as the upstream repository.\n\nThe default is otherwise the SBO GitLab mirror, or Ponce's repo on -current.";
			$criterion = "A git or rsync URL or FALSE.";
			$flag = "r";
		}
		if ($option =~ /^RSYNC_DEFAULT/) {
			$explanation = "If TRUE, use the SBO rsync server as the default.\n\nHas no effect on -current, or if REPO is set.";
			$criterion = "TRUE or FALSE.";
			$flag = "R";
		}
		if ($option =~ /^SBO_HOME/) {
			$explanation = "If a PATH, use that as the sbotools directory.\n\nThe default is /usr/sbo. /home/*, /root and / are not permitted.";
			$criterion = "A PATH or FALSE.";
			$flag = "s";
		}
		if ($option =~ /^SLACKWARE_VERSION/) {
			$explanation = "If a SLACKWARE VERSION number, use the repo for that version.";
			$criterion = "A SLACKWARE VERSION or FALSE.";
			$flag = "V";
		}
		if ($option =~ /^SO_CHECK/) {
			$explanation = "If TRUE, do a shared object check when checking for updates\nand after upgrades.\n\nFrom sbocheck, also search for incompatible perl, python and\nruby _SBo packages.";
			$criterion = "TRUE or FALSE.";
			$flag = "X";
		}
		if ($option =~ /^STRICT_UPGRADES/) {
			$explanation = "If TRUE, ignore apparent downgrades.";
			$criterion = "TRUE or FALSE.";
			$flag = "S";
		}
		edit_setting($option, $explanation, $criterion, $flag);
		display_settings();
	}
	unless ($config_menu) {
		return;
	} else {
		exit;
	}
}

# Act on one of the sbotool-internal lists, or clear them all.
sub display_user_lists {
	return unless @install_list or @template_list or @upgrade_list or @remove_list;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	my @lists_to_show;
	push @lists_to_show, "\"Install\" \"Install the packages in the install list.\" \\" if @install_list;
	push @lists_to_show, "\"Template\" \"Create a template from the template list.\" \\" if @template_list;
	push @lists_to_show, "\"Upgrade\" \"Upgrade the packages in the upgrade list.\" \\" if @upgrade_list;
	push @lists_to_show, "\"Remove\" \"Remove the packages in the remove list.\" \\" if @remove_list;
	push @lists_to_show, "\"\" \"\" \\", "\"Clear\" \"Clear list contents.\"";
	my $lists_menu = join "\n", @lists_to_show;
	($height, $width, $begin_y, $begin_x) = calculate_position("wide", $lists_menu, "Choose a list to implement\n");
	($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"List Operations\" --menu \"Choose a list to implement.\" $height $width $height $lists_menu", @help_lists);
	return if $res == $code_dialog_cancel or $res == $code_dialog_error;
	my $option = $output;
	unless ($option =~ /\w/) { display_user_lists(); return; }
	if ($option eq "Install") {
		my $installs = join " ", @install_list;
		run_command("/usr/sbin/sboinstall $installs");
	}

	# Save templates to /tmp/sbotool(|_\d+).tmp, first available.
	if ($option eq "Template") {
		my $templates = join " ", @template_list;
		my $out_file = "/tmp/sbotool.temp" unless -s "/tmp/sbotool.temp";
		unless ($out_file) {
			my $index = 0;
			NEXT_FILE: while (1) {
				$out_file = "/tmp/sbotool_$index.tmp" unless -s "/tmp/sbotool_$index.tmp";
				last if $out_file;
				$index++;
				next NEXT_FILE;
			}
		}
		run_command("/usr/sbin/sboinstall --template-only \"$out_file\" --reinstall $templates");
		($height, $width, $begin_y, $begin_x) = calculate_position("normal_cond", "Success or failure");
		if (-s $out_file) {
			system("$dialog_prefix --begin $begin_y $begin_x --title \"List Operations\" --msgbox \"Template saved to $out_file. Clearing the list.\" $height $width");
			splice @template_list;
		} else {
			system("$dialog_prefix --begin $begin_y $begin_x --title \"List Operations\" --msgbox \"Unable to save to $out_file.\" $height $width");
		}
	}

	if ($option eq "Upgrade") {
		my $upgrades = join " ", @upgrade_list;
		run_command("/usr/sbin/sboupgrade $upgrades");
	}

	if ($option eq "Remove") {
		my $removes = join " ", @remove_list;
		run_command("/usr/sbin/sboremove $removes");
	}

	if ($option eq "Clear") {
		splice @install_list;
		splice @upgrade_list;
		splice @remove_list;
		splice @template_list;
		($height, $width, $begin_y, $begin_x) = calculate_position("narrow", "Cleared");
		system("$dialog_prefix --begin $begin_y $begin_x --title \"List Operations\" --ok-label \"OK\" --msgbox \"List contents cleared.\" $height $width");
	}

	display_user_lists();
	return;
}

# Use do_dialog() in place of a system() call whenever
# output or an exit code is required from dialog, or
# if a Help menu should be displayed.
#
# The first variable (mandatory) is the dialog command;
# the second is an array imported from Help.pm.
#
# checklists have --separate-output applied; if a Help
# array is specified, add a Help button and (if necessary)
# --help-status.
sub do_dialog {
	script_error("do_dialog requires at least one argument.") unless @_ > 0;
	my ($command, @help_text) = @_;
	unlink $dialog_output;
	unlink $dialog_exit;

	$command =~ s/--begin/--help-button --begin/g if @help_text;
	$command =~ s/--begin/--help-status --begin/g if @help_text and $command =~ /--(inputbox|checklist)/;
	$command =~ s/--begin/--separate-output --begin/g if $command =~ /--checklist/;
	system("$command 2> $dialog_output ; echo \$? > $dialog_exit");
	chomp(my $res = slurp $dialog_exit);
	chomp(my $output = slurp $dialog_output);

	if (@help_text) {
		my $help_title = $help_text[0];
		my $help_text = $help_text[1];
		while ($res == $code_dialog_help or $res == $code_dialog_item_help) {
			# Preserve contents of inputbox and checklist after Help
			if ($command =~ /--inputbox/) {
				$output =~ s/^HELP//g;
				$output =~ s/"/\\\"/g;
				my @command = split " ", $command;
				while (@command > 1) {
					last if $command[-1] =~ /\d+/ and $command[-2] =~ /\d+/;
					pop @command;
				}
				$command = join " ", @command;
				$command .= " \"$output\"";
			}
			if ($command =~ /--checklist/) {
				$command =~ s/"on"/"off"/g;
				my @output = split "\n", $output;
				shift @output; # The first line is for HELP
				my @command = split "\n", $command;
				for my $on (@output) {
					for my $item (@command) {
						$item =~ s/"off"/"on"/g if $item =~ /\Q"$on"\E/;
					}
				}
				$command = join "\n", @command;
			}
			unlink $dialog_output;
			unlink $dialog_exit;
			my ($height, $width, $begin_y, $begin_x) = calculate_position("normal", $help_text);
			system("$dialog_prefix --no-collapse --begin $begin_y $begin_x --title \"$help_title\" --ok-label \"Back\" --msgbox \'$help_text\' $height $width");
			system("$command 2> $dialog_output ; echo \$? > $dialog_exit");
			chomp($res = slurp $dialog_exit);
			chomp($output = slurp $dialog_output);
		}
	}

	return($res, $output);
}

# An interface for sbohints. Accepts the name of a SlackBuild.
sub edit_hint {
	script_error("edit_hint requires an argument.") unless @_ == 1;
	my $sbo = shift;
	unlink $command_output;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	my @hint_menu;
	my $blacklisted = on_blacklist($sbo);
	my $is_auto = auto_reverse($sbo);
	my $optional = get_optional($sbo);

	if ($blacklisted) {
		push @hint_menu, "\"Blacklist (clear)\" \"Add to queues normally.\" \\";
	} else {
		push @hint_menu, "\"Blacklist\" \"Do not add to queues.\" \\";
	}

	if ($is_auto) {
		push @hint_menu, "\"No Auto-Rebuilds\" \"Do not rebuild reverse deps.\" \\";
	} else {
		push @hint_menu, "\"Do Auto-Rebuilds\" \"Auto-rebuild reverse deps.\" \\";
	}

	push @hint_menu, "\"Add Optional Deps\" \"Add optional dependencies.\" \\";
	if ($optional) {
		push @hint_menu, "\"Clear Optionals\" \"Clear optional dependencies.\" \\",
						"\"Clear all Optional\" \"Clear all optional dependencies.\" \\",
					    "\"New Optional List\" \"Replace the optional dependency list.\" \\";
	}
	if (($blacklisted and ($is_auto or $optional)) or ($is_auto and $optional)) {
		push @hint_menu, "\"\" \"\" \\",
						"\"Clear all Hints\" \"No hints for $sbo.\" \\" if $is_auto or $optional;
	}

	$hint_menu[-1] =~ s/\\$//;
	my $hint_menu = join "\n", @hint_menu;
	system("/usr/sbin/sbohints --wrap --nocolor --query $sbo > $command_output");
	chomp(my $msg = slurp $command_output);
	my $wanted = $optional ? "wide_cond" : "wide";
	($height, $width, $begin_y, $begin_x) = calculate_position($wanted, $hint_menu, $msg);
	($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --help-button --title \"$sbo: Hints\" --menu \"$msg\" $height $width $height $hint_menu", @help_hints);
	return if $res == $code_dialog_cancel or $res == $code_dialog_error;
	my $option = $output;
	return unless $option =~ /\w/;

	my @flags;

	if ($option eq "Clear all Hints") {
		push @flags, "cb" if $blacklisted;
		push @flags, "cr" if $is_auto;
		push @flags, "cO" if $optional;
	}

	push @flags, "cO" if $option eq "Clear all Optional";
	push @flags, "cb" if $option eq "Blacklist (clear)";
	push @flags, "co" if $option eq "Clear Optionals";
	push @flags, "cr" if $option eq "No Auto-Rebuilds";
	push @flags, "r" if $option eq "Do Auto-Rebuilds";
	push @flags, "o" if $option eq "Add Optional Deps";
	push @flags, "b" if $option eq "Blacklist";
	push @flags, "O" if $option eq "New Optional List";

	edit_hint($sbo) unless @flags;
	for (@flags) {
		system("clear");
		$res = system("/usr/sbin/sbohints -$_ $sbo") == 0;
		if ($res) {
			clear_info_store();
			new_package_information();
		}
	}

	edit_hint($sbo);
	return;
}

# Accessed only from display_settings(). Get user
# input, change the relevant setting and refresh
# sbotool.
#
# Accepts the existing setting, an explanation, a
# validity condition and the setting's sboconfig flag.
sub edit_setting {
	script_error("edit_setting requires four arguments.") unless @_ == 4;
	my ($existing_setting, $explanation, $criterion, $flag) = @_;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	my ($setting_name, $setting_content) = split "=", $existing_setting;
	my $original_setting = $setting_content;
	$setting_content =~ s/^FALSE$//;
	my $option;
	if ($criterion eq "TRUE or FALSE.") {
		$option = $setting_content eq "TRUE" ? "FALSE" : "TRUE";
	} else {
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $existing_setting, "$explanation\n\n$criterion\n\nInput here");
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --extra-button --extra-label \"False\" --title \"Settings Editor\" --inputbox \"$explanation\n\n$criterion\n\n$existing_setting\" $height $width \"$setting_content\"");
		$output = $output eq "" ? "FALSE" : $output;
		$option = $res == $code_dialog_extra ? "FALSE" : $output;
		return unless $option =~ /\w/;
		return if $option eq $original_setting or $res == $code_dialog_cancel or $res == $code_dialog_error;
	}
	$option = "/usr/sbo" if $flag eq "s" and $option eq "FALSE";
	system("clear");
	$res = system("/usr/sbin/sboconfig -$flag $option") == 0;
	if ($res) {
		clear_info_store();
		new_package_information();
		$save_dialogrc = $config{DIALOGRC} if $existing_setting =~ /^DIALOGRC=/;
		if ($setting_name eq "LOCAL_OVERRIDES" and $option ne "FALSE") {
			unless (-d $option) {
				($height, $width, $begin_y, $begin_x) = calculate_position("wide", "The specified local overrides directory does not exist:\n\n$option\n\ncreate?");
				($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"New Overrides Directory\" --yesno \"The specified local overrides directory does not exist:\n\n  $option\n\nCreate it?\" $height $width");
				if ($res == $code_dialog_ok) {
					make_path($option) unless -d $option;
				}
			}
		}
		$overrides_available = ($config{LOCAL_OVERRIDES} ne "FALSE" and -d -w $config{LOCAL_OVERRIDES});
		return;
	}
	edit_setting($existing_setting, $explanation, $criterion, $flag);
	return;
}

# Run this whenever there have been changes to settings, hints,
# local override contents or installed packages, and when the
# repository is fetched.
#
# Users can run with the Refresh option in Main Menu.
sub new_package_information {
	splice @update_names;
	splice @overrides;
	$inst_pkgs = "";
	%inst_names = ();
	%inst_names_std = ();
	$inst_vers = "";
	$inst_vers_std = "";

	%concluded = ();
	%warnings = ();
	splice @reverse_concluded;
	@listings = read_hints();
	$config{$_} = "FALSE" for (keys %config);
	read_config();
	lint_sbo_config($self, %config);
	unless (defined $dialogrc and -s $dialogrc) {
		$dialog_prefix = ($config{DIALOGRC} ne "FALSE" and -s $config{DIALOGRC}) ? "DIALOGRC=\"$config{DIALOGRC}\" $dialog_base" : $dialog_base;
		dialog_check();
	}
	$repo_path = "$config{SBO_HOME}/repo";
	$slackbuilds_txt = "$repo_path/SLACKBUILDS.TXT";
	return unless -s $slackbuilds_txt;
	renew_sbo_locations();
	@available = get_all_available();
	update_known_solibs();
	$overrides_available = ($config{LOCAL_OVERRIDES} ne "FALSE" and -d -w $config{LOCAL_OVERRIDES});

	@installed = @{ get_installed_packages('SBO', 1) };
	@installed_std = @{ get_installed_packages('STD') };
	for (@available) { push @overrides, $_ if is_local($_); }
	$inst_pkgs = +{ map {; $_->{name}, $_->{pkg} } @installed };
	$inst_pkgs_std = +{ map {; $_->{name}, $_->{pkg} } @installed_std };
	$inst_vers = +{ map {; $_->{name}, $_->{version} } @installed };
	$inst_names{$_->{name}} = $_ for @installed;
	$inst_names_std{$_->{name}} = $_ for @installed_std;
	$inst_vers_std = +{ map {; $_->{name}, $_->{version} } @installed_std };
	$fulldeps = get_reverse_reqs($inst_pkgs);
	$updates = $config{BUILD_IGNORE} eq "TRUE" ? get_available_updates("VERS") : get_available_updates("BOTH");
	if ($updates) {
		for (@$updates) { push @update_names, $_->{name} unless on_blacklist $_->{name}; }
	}
	@install_list = grep { !$inst_names{$_} && ! on_blacklist($_) } @install_list;
	@upgrade_list = grep { in $_, @update_names } @upgrade_list;
	@remove_list = grep { $inst_names{$_} && ! on_blacklist($_) } @remove_list;

	my @new_solibs_missing;
	for (@solibs_missing) {
		next unless $inst_names{$_} or $inst_names_std{$_};
		my $solib_check_pkg = $inst_names{$_} ? $inst_pkgs->{$_} : $inst_pkgs_std->{$_};
		push @new_solibs_missing, $_ unless solib_check($solib_check_pkg);
	}
	splice @solibs_missing;
	if (@new_solibs_missing) {
		@new_solibs_missing = uniq @new_solibs_missing;
		@solibs_missing = @new_solibs_missing;
	}
	my @new_perl;
	for (@incompatible_perl) {
		next unless $inst_names{$_} or $inst_names_std{$_};
		my $solib_check_pkg = $inst_names{$_} ? $inst_pkgs->{$_} : $inst_pkgs_std->{$_};
		push @new_perl, $_ unless (series_check($solib_check_pkg, "perl"))[0];
	}
	splice @incompatible_perl;
	if (@new_perl) {
		@new_perl = uniq @new_perl;
		@incompatible_perl = @new_perl;
	}
	my @new_python;
	for (@incompatible_python) {
		next unless $inst_names{$_} or $inst_names_std{$_};
		my $solib_check_pkg = $inst_names{$_} ? $inst_pkgs->{$_} : $inst_pkgs_std->{$_};
		push @new_python, $_ unless (series_check($solib_check_pkg, "python"))[1];
	}
	splice @incompatible_python;
	if (@new_python) {
		@new_python = uniq @new_python;
		@incompatible_python = @new_python;
	}
	my @new_ruby;
	for (@incompatible_ruby) {
		next unless $inst_names{$_} or $inst_names_std{$_};
		my $solib_check_pkg = $inst_names{$_} ? $inst_pkgs->{$_} : $inst_pkgs_std->{$_};
		push @new_ruby, $_ unless (series_check($solib_check_pkg, "ruby"))[2];
	}
	splice @incompatible_ruby;
	if (@new_ruby) {
		@new_ruby = uniq @new_ruby;
		@incompatible_ruby = @new_ruby;
	}
}

# Parse sbocheck log files and add relevant packages to the
# @solibs_missing and @incompatible_* lists.
sub parse_solib_log {
	script_error("parse_solib_log requires two arguments.") unless @_ == 2;
	my ($logfile, $command_time) = @_;
	return unless -s $logfile;
	return if (stat $logfile)[10] < $command_time;
	update_known_solibs() if $logfile =~ /solibs/;
	chomp(my $log = slurp $logfile);
	my @log = split "\n", $log;
	for (@log) {
		next unless $_ =~ /^\w/;
		my $pkg_name = (split " ", $_)[0];
		next unless get_sbo_location($pkg_name);
		my $solib_check_pkg = $inst_names{$pkg_name} ? $inst_pkgs->{$pkg_name} : $inst_pkgs_std->{$pkg_name};
		if ($logfile =~ /solibs/) {
			unless (in $pkg_name, @solibs_missing) {
				next unless $inst_names{$pkg_name} or $inst_names_std{$pkg_name};
				push @solibs_missing, $pkg_name unless solib_check($solib_check_pkg);
			}
		} elsif ($logfile =~ /perl/) {
			unless (in $pkg_name, @incompatible_perl) {
				next unless $inst_names{$pkg_name} or $inst_names_std{$pkg_name};
				push @incompatible_perl, $pkg_name unless (series_check($solib_check_pkg, "perl"))[0];
			}
		} elsif ($logfile =~ /python/) {
			unless (in $pkg_name, @incompatible_python) {
				next unless $inst_names{$pkg_name} or $inst_names_std{$pkg_name};
				push @incompatible_python, $pkg_name unless (series_check($solib_check_pkg, "python"))[1];
			}
		} elsif ($logfile =~ /ruby/) {
			unless (in $pkg_name, @incompatible_ruby) {
				next unless $inst_names{$pkg_name} or $inst_names_std{$pkg_name};
				push @incompatible_ruby, $pkg_name unless (series_check($solib_check_pkg, "ruby"))[2];
			}
		}
	}
	return;
}

# A wrapper for running commands requiring user input;
# pass any true value after the command to skip the
# batch processing check for sboinstall and sboupgrade.
sub run_command {
	script_error("run_command requires at least one argument.") unless @_;
	my ($cmd, $no_batch) = @_;
	my ($height, $width, $begin_y, $begin_x, $res, $output);
	my ($dry_run_output, $batch_res);
	my $overrides_ok = ($config{LOCAL_OVERRIDES} eq "FALSE" or -d $config{LOCAL_OVERRIDES});
	if ($cmd =~ /sbo(install|upgrade)/ and not defined $no_batch and $overrides_ok) {
		my $batch_cmd = "$cmd --batch";
		my $dry_run_cmd = "$batch_cmd --wrap --nocolor -D";
		unlink $command_output;
		system("$dry_run_cmd > $command_output");
		chomp($dry_run_output = slurp $command_output);
		unless ($dry_run_output =~ /with --batch.$/) {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide_cond", $dry_run_output, "$cmd\n\nUse batch mode?\n\nUse interactive mode?\n\nDry Run:\n");
			($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --help-button --yes-label \"Batch\" --extra-button --extra-label \"Interactive\" --no-label \"Back\" --title \"Building the Queue\" --yesno \"$cmd\n\nSelect \\\"Interactive\\\" to approve the queue manually.\n\nSelect \\\"Batch\\\" to build the queue automatically.\n\nDry Run:\n$dry_run_output\" $height $width", @help_batch);
			return if $res == $code_dialog_cancel or $res == $code_dialog_error;
			if ($res == $code_dialog_ok) { $cmd = $batch_cmd; $batch_res = 1; }
		} else {
			($height, $width, $begin_y, $begin_x) = calculate_position("wide_cond", "missing\n\nrun or continue\n\n$dry_run_output");
			($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"User or Group Missing\" --yesno \"Batch mode is unavailable due to missing users or groups.\n\nContinue for Interactive mode, which provides a prompt to add them?\n\n$dry_run_output\" $height $width");
			return unless $res == $code_dialog_ok;
		}
	} elsif ($cmd =~ /sbo(install|upgrade)/ and not defined $no_batch) {
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", "LOCAL_OVERRIDES\n\nLO\n\nunavailable");
		system("$dialog_prefix --begin $begin_y $begin_x --title \"Overrides Directory Missing\" --msgbox \"LOCAL_OVERRIDES is defined, but the directory does not exist:\n\n  $config{LOCAL_OVERRIDES}\n\nBatch mode is unavailable.\" $height $width");
	}
	if ($cmd =~ /sboclean/ and $cmd =~ /(-d|-w|-o ALL)/) {
		($height, $width, $begin_y, $begin_x) = calculate_position("normal_cond", "$cmd\n\nAsk to delete");
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Clean sbotools Files\" --yesno \"$cmd\n\nAsk to delete files interactively?\" $height $width");
		$cmd .= " -i" if $res == $code_dialog_ok;
	}
	if ($cmd =~ /sboremove/) {
		($height, $width, $begin_y, $begin_x) = calculate_position("normal_cond", "$cmd\n\nProceed");
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Interactive Package Removal\" --help-button --yesno \"$cmd\n\nProceed?\" $height $width", @help_sboremove);
	} elsif (defined $dry_run_output and $batch_res) {
		($height, $width, $begin_y, $begin_x) = calculate_position("wide", $dry_run_output, "Proceed?\n\n$cmd\n\nDry Run:\n");
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"Final Confirmation\" --yesno \"$cmd\n\nProceed?\n\nDry Run:\n$dry_run_output\" $height $width");
	} else {
		my $title = ($cmd =~ /sboclean/ and not $cmd =~ /-i/) ? "Final Confirmation" : "Confirmation";
		($height, $width, $begin_y, $begin_x) = calculate_position("normal_cond", "$cmd\n\nProceed");
		($res, $output) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --title \"$title\" --yesno \"$cmd\n\nProceed?\" $height $width");
	}
	return unless $res == $code_dialog_ok;
	my $command_time = time();
	system("clear");
	system($cmd);
	unless ($cmd =~ /sboclean/) {
		new_package_information();
		parse_solib_log("/var/log/sboupgrade-solibs.log", $command_time) if $cmd =~ /sboupgrade/;
		prompt $color_notice, "Press ENTER to continue.";
	}
	return;
}

# Optionally, pass a label and an array to filter results from
# that array.
sub run_search {
	unlink $command_output;
	my $passed_label = shift;
	my $new_label;
	if (defined $passed_label) {
		$new_label = $passed_label =~ /Filtered\)$/ ? $passed_label : "$passed_label (Filtered)";
	}
	my @to_search = @_;
	my $title = @to_search ? "Filter $passed_label" : "Package Search";
	my $result_title = @to_search ? $new_label : "Search Results";
	my $ok_label = -s "$config{SBO_HOME}/TAGS.txt" ? "Name and Tag" : "Search";
	my ($height, $width, $begin_y, $begin_x) = calculate_position("wide", "Search\nTerms");
	my ($normal_search, $option) = do_dialog("$dialog_prefix --begin $begin_y $begin_x --help-button --ok-label \"$ok_label\" --extra-button --extra-label \"Desc.\" --title \"$title\" --inputbox \"Enter any number of search terms, separated by spaces.\" $height $width", @help_search);
	return unless $option =~ /\w/;
	unlink $command_output;
	if ($normal_search == $code_dialog_ok) {
		system("/usr/sbin/sbofind --raw -- $option > $command_output");
	} else {
		system("/usr/sbin/sbofind --raw -d -- $option > $command_output");
	}
	my $search_result = slurp $command_output;
	if ($search_result =~ /\w/) {
		my @pre_search_results = split " ", $search_result;
		@pre_search_results = sort @pre_search_results;
		my @search_results;
		# If a search term matches an entire word, put that result
		# (or those results) on top.
		for my $result (@pre_search_results) { push @search_results, $result if $option =~ /(^|\W)\Q$result\E(\W|$)/i; }
		unless ($normal_search == $code_dialog_ok) {
			RESULTS: for my $result (@pre_search_results) {
				my @description = split " ", get_sbo_description($result);
				for my $in_desc (@description) {
					if ($option =~ /(^|\W)\Q$in_desc\E(\W|$)/i) {
						push @search_results, $result;
						next RESULTS;
					}
				}
			}
		}
		push @search_results, @pre_search_results;
		@search_results = uniq @search_results;
		if (@to_search) {
			@search_results = grep { in $_, @to_search } @search_results;
		}
		display_builds($result_title, @search_results) if @search_results;
	}
	if (@to_search) {
		run_search($passed_label, @to_search);
	} else {
		run_search();
	}
	return;
}

sub show_usage {
	print <<"EOF";
Usage: $self (-d FILE)

Options
  -h|--help:
    this screen.
  -v|--version:
    version information.
  --config
    access the sbotools settings menu only.
  -d|--dialogrc FILE:
    a dialogrc file to use; overrides the DIALOGRC setting.

$self is a dialog-based text user interface for sbotools.

The contents of the menus change based on available actions
and user permissions.

sbotool requires a terminal of at least 80 by 25 characters.

EOF
	return 1;
}

# Ensure that the terminal is at least 80x25, and adjust the
# height and width variables. Box widths, $terminal_height and
# $terminal_width are all set here; a sensible maximum height
# is returned.
sub terminal_check {
	chomp(my $lines = `tput lines`);
	chomp(my $columns = `tput cols`);
	if ($lines =~ /\D/ or $columns =~ /\D/) {
		system("clear");
		prompt($color_notice, "tput failure; check the value of \$TERM.\nPress ENTER to exit.\n");
		exit _ERR_USAGE;
	}

	if ($lines < 25 or $columns < 80) {
		system("clear");
		prompt($color_notice, "Please resize the terminal to at least 80x25 to run sbotool.\nPress ENTER to try again.");
		terminal_check();
	}

	$terminal_height = $lines;
	$terminal_width = $columns;
	my $max_height = $terminal_height - 6;
	$wide = $terminal_width - 14; # Effective minimum of 66.
	$wide = $wide < $max_width ? $wide : $max_width;
	$normal = 78; # Value for even number of columns; 77 is the README minimum with shadow.
	$narrow = 60;
	for ($normal, $narrow) { $_ -= $terminal_width % 2; }
	return $max_height;
}

END { unless ($help or $show_version) { $exit = $?; remove_tree($sbotool_tempdir) if -d $sbotool_tempdir; system("clear"); exit $exit } }
