#!/usr/bin/perl -w
# This program is distributed under the terms of the GNU General Public License
# 
#    This file is part of sysv-rc-conf.
#
#    sysv-rc-conf is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    sysv-rc-conf is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with sysv-rc-conf; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Copyright 2004 Joe Oppegaard <joe@pidone.org>
#           2025 Andrew Bower <andrew@bowerg.uk>
#
use strict;
use utf8;
use Getopt::Long qw(:config no_ignore_case);
use Pod::Usage;
use File::Basename;
use Curses;
use Curses::UI;
use List::Util qw ( first any );
use List::UtilsBy qw ( max_by min_by uniq_by );
use App::SysVRcConf::LSB;
use IPC::Open3;
use Storable qw(dclone);

use constant {
    BOTTOM_LAB_HEIGHT   => 2,
    BOTTOM_WIN_HEIGHT   => 4,
    DEFAULT_K_PRI       => 80,
    DEFAULT_S_PRI       => 20,
    DEFAULT_LABEL_WIDTH => 10,
    DEFAULT_LIST_SN_LENGTH
                        => 12,
    LIST_SN_PAD         => 1,
    MAX_ROWS            => 8,
    TOP_LABEL_HEIGHT    => 2,
};

my $OURNAME = "sysv-rc-conf";
my $VERSION = "1.4.1";
my $called_as_chkconfig = $0 =~ /chkconfig$/;

# Runlevel patterns: [ state regex, inverse video, colour, summary ]
my $rl_patterns = [
    [qr(^01111002),           0, "green",   "Multiuser"],
    [qr(^12222222),           0, "yellow",  "Single user"],
    [qr(^02222222),           0, "red",     "Off (single user)"],
    [qr(^10000002),           0, "yellow",  "Single user"],
    [qr(^12222222),           0, "yellow",  "Single user oneshot"],
    [qr(^11111222),           0, "yellow",  "Single and multiuser linger"],
    [qr(^11111002),           0, "yellow",  "Single and multiuser"],
    [qr(^22222221),           0, "cyan",    "Startup"],
    [qr(^22222220),           0, "red",     "Off (startup/shutdown)"],
    [qr(^21111002),           0, "cyan",    "Multiuser startup"],
    [qr(^22222222),           1, "red",     "No symlinks"],
    [qr(^[02]1111222),        0, "green",   "Multiuser linger"],
    [qr(^[02]0000[02][02]2),  0, "red",     "Off"],
    [qr(^[02][01]*1[01]*[02][02]2$),
                              1, "green",   "Custom multiuser"],
    [qr(^[02]2222001),        0, "cyan",    "Startup and shutdown"],
    [qr(^22222[02]*0[02]*2$), 0, "magenta", "Shutdown"],
    [qr(0$),                  1, "red",     "Likely error"],
    [qr(^......*1.*.$),       1, "red",     "Likely error"],
    [qr(.*),                  0, "",        "Custom"]
];

my %def_alt_init_handler = (
    is_managed => sub { return 0; },
);

my %runit_handler = (
    manager => "runit",
    is_managed => sub {
        my ($sn) = shift;
        return ( -f "/etc/runit/override-sysv.d/$sn.block" ||
                 -f "/etc/runit/override-sysv.d/$sn.pkgblock" ||
                  ( ! -f "/etc/runit/override-sysv.d/$sn.sysv" &&
                    ( -l "/etc/service/$sn" ||
                      -d "/etc/sv/$sn" ||
                      -d "/usr/share/runit/sv.current/$sn" )));
    },
);

my %systemd_handler = (
    manager => "systemd",
    is_managed => sub {
        my ($sn) = shift;
        # Do not include generator paths
        my @dirs = ( "/etc/systemd/system",
                     "/run/systemd/system",
                     "/usr/local/lib/systemd/system",
                     "/usr/lib/systemd/system",
                   );
        return any { -e "$_/$sn.service" || -e "$_/$sn.target" } @dirs;
    },
);

my %opts = (
    cache_dir   => undef,
    order       => 'a',
    priority    => '',
    purge_cache => 0,
    root        => '/',
    show        => '',
    verbose     => '',
    force       => 0,
    chkcfg_levels => '2345', # default runlevels to affect if not specified
    chkcfg_info   => undef,
    chkcfg_list   => undef,
    chkcfg_add    => undef,
    chkcfg_del    => undef,
    chkcfg_sn     => '',
    chkcfg_state  => '',
    chkcfg_type   => 'sysv',
    label_width   => undef,
    read_only   => 0,
    ui          => undef,
    insserv     => 0,
    interpret   => 1,
    defdiff     => 1,
    pictograms  => 0,
    warninit    => 1,
    colour      => (defined $ENV{NO_COLOR}) ? 0 : 1,
    classic     => 0,
);
my @mutually_exclusive_options = qw(chkcfg_list chkcfg_add chkcfg_del);

foreach my $conffile (grep { -s } (
    "/usr/share/sysv-rc-conf/config",
    "/etc/sysv-rc-conf.conf",
    ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/sysv-rc-conf.conf",
)) {
    if (open (my $conf, "<", $conffile)) {
        while (<$conf>) {
            next if /^\s*#/ or /^\s*$/;
            my ($k, $v) = /^\s*(\S+)(?:\s+([^#]*?))?\s*(?:#.*)?$/ or die "malformed config line: $_";
            if ($k =~ /^no-(.*)$/) {
                $k = $1;
                $v = 0;
            }
            $opts{$k} = $v ne '' ? $v : 1;
        }
        close $conf;
    }
}

GetOptions("cache=s"	=> \$opts{cache_dir},
           "help"       => sub { pod2usage(-verbose => 1); },
           "level=s"    => \$opts{chkcfg_levels},
           "type=s"     => \$opts{chkcfg_type},
           "list:s"     => \$opts{chkcfg_list},
           "info=s"     => \$opts{chkcfg_info},
           "add=s"      => \$opts{chkcfg_add},
           "del=s"      => \$opts{chkcfg_del},
	   "order=s"	=> \$opts{order},
	   "priority|p"	=> \$opts{priority},
           "Purge"      => \$opts{purge_cache},
	   "root=s"	=> \$opts{root},
	   "show=s"	=> \$opts{show},
	   "verbose=s"	=> \$opts{verbose},
           "force"      => \$opts{force},
           "Version"    => sub { print STDERR "$0 $VERSION\n"; exit; },
           "width=i"    => \$opts{label_width},
           "RO"         => \$opts{read_only},
           "UI"         => \$opts{ui},
           "batch"      => sub { $opts{ui} = 0; },
           "insserv"    => \$opts{insserv},
           "interpret"      =>      \$opts{interpret},
           "no-interpret"   => sub { $opts{interpret} = 0; },
           "pictograms"     =>      \$opts{pictograms},
           "no-pictograms"  => sub { $opts{pictograms} = 0; },
           "colour"         =>      \$opts{colour},
           "no-colour"      => sub { $opts{colour} = 0; },
           "defdiff"        =>      \$opts{defdiff},
           "no-defdiff"     => sub { $opts{defdiff} = 0; },
           "classic"        =>      \$opts{classic},
	  ) or exit(1);

$opts{cache_dir} //= "$opts{root}var/lib/sysv-rc-conf";
$opts{verbose} ||= "/dev/null";
open VERBOSE, "> $opts{verbose}" or die "Can't open $opts{verbose} : $!";

if ($opts{purge_cache} && !$opts{ui}) {
    printf "NOTE: The --Purge option now inhibits the user interface; add --UI to override.\n";
}
if (scalar (grep { defined $opts{$_} } @mutually_exclusive_options) > 1) {
    pod2usage(-msg => "Mutually exclusive options specified\n");
}
$opts{chkcfg_type} eq "sysv" or die "Only 'sysv' type services supported";
$opts{pictograms} = ($ENV{LANG} =~ /\.UTF-8$/ ? 1 : 0) if $opts{pictograms};
$opts{ui} //= 0 if $opts{purge_cache};
$opts{ui} //= 1;
if ($opts{classic}) {
    $opts{interpret} = $opts{pictograms} = $opts{defdiff} = $opts{colour} = 0;
}

my $etc_rc = $opts{root} . "etc/rc";
my $initd = $opts{root} . "etc/init.d";
my @rls = qw/ 1 2 3 4 5 7 8 9 0 6 S /;
my @all_lsb_rls = qw/ 1 2 3 4 5 0 6 S /;
my @valid_rls = grep { -d "$etc_rc$_.d" } @rls;
my $retcode = undef;
my $first_write_attempted = 0;
my $passive_ops_only = 0;
my $delta = $opts{pictograms} ? "\x{0394}" : '#';

my $LSB = App::SysVRcConf::LSB->new(
    root => $opts{root},
    initd => "$initd/",
    ignore_errors => 1,
);
my $runlevel_cmd = '/sbin/runlevel';
my $alt_init_handler = \%def_alt_init_handler;
my $unsuitable_init = check_suitable_init();
check_root_access();
check_args();
setup_cache_env();

my @show_rls = split //, $opts{show};

# Page index
my $current_screen = 0;
my $_cached_max_services = undef;
my $max_service_digits = undef;

# Page screens
my @s = ();

# All the runlevel information
my @status_data = runlevel_status();
my %runlevels = %{$status_data[0]};
my %runlevels_ui = %{dclone($status_data[0])};
my %services = %{$status_data[1]};
my $longest_service = length (max_by { length($_) } (keys %runlevels));

if (defined $opts{chkcfg_info}) {
    $retcode = infotext($opts{chkcfg_info});
    $opts{ui} = 0;
    $passive_ops_only = 1;
} elsif (defined $opts{chkcfg_list} or
         $called_as_chkconfig and not defined $ARGV[0]) {
    $opts{label_width} //= max_by { $_ } (
        DEFAULT_LIST_SN_LENGTH,
        $longest_service
    );
    list_output(%runlevels);
    $opts{ui} = 0;
    $passive_ops_only = 1;
} else {
    $retcode = chkconfig_emulation();
    if ($retcode < 2) {
        $opts{ui} = 0;
    } else {
        $retcode = 0;
    }
}

# CUI Globals
my $mode_text = $opts{read_only} ? "RO" : "";
my $title_help    = "SysV Runlevel Config   +/-: start/stop   i: info   h: help   q: quit  $mode_text";
my $title_helpscr = "SysV Runlevel Config                                h/q: quit help    $mode_text";
my $cui;
my @snames_per_screen;
my %box_pos;
my %service_controls = ();
my %extra_controls = ();
my $last_service = undef;
my $last_page = undef;
my @found = ();
my $found_index;

if ($opts{ui}) {
    $cui = new Curses::UI( -clear_on_exit    => 0,
			      -color_support    => $opts{colour},
			      -default_colors   => 1,
			    ) or die "Can't create base Curses::UI object";
    # Get the service names for each screen
    @snames_per_screen = split_services();
    %box_pos = (row => 0, col => 0);

    create_bottom_box();
    create_main_window( \%service_controls );
    $cui->set_binding( \&revert_changes, "r" );
    $cui->set_binding( sub { $cui->leave_curses(); kill 'STOP', 0; }, "\cZ" );
    $cui->set_binding( sub { $cui->mainloopExit(); }, "\cC" );
    first_service();
}

ui_warning("$unsuitable_init") if defined $unsuitable_init and $opts{warninit};

if ($opts{ui}) {
    $cui->mainloop();
}

if ($opts{insserv} and not $opts{priority} and not $opts{read_only} and not $passive_ops_only) {
    my $insserv = "/usr/sbin/insserv";

    if ( -x $insserv ) {
        my $pid = open3(undef, '>&STDOUT', $opts{verbose} ? '>&VERBOSE' : undef,
			$insserv,
			$opts{root} ne "/" ? (
				"--path", $initd,
				"--override", "$opts{root}etc/insserv/overrides/",
				"--insserv-dir", $initd,
				"--config", "$opts{root}etc/insserv.conf",
			) : (),
			$opts{force} ? ("--force") : (),
			$opts{read_only} ? ("--dryrun") : (),
			$opts{verbose} ? ("--verbose") : ());
        waitpid($pid, 0) or $retcode //= ($? >> 8);
    } elsif (!defined $unsuitable_init) {
	verbose("insserv requested but not available");
    }
}

exit $retcode if defined $retcode;

#--- rc access and modification subs ---#
sub update_link
{
    my ($sn, $rl, $sk, $pri) = @_;

    if (defined $sn && defined $rl && defined $sk && defined $pri) {
        if (-e "$etc_rc$rl.d/$sk$pri$sn") {
            # The symlink we are trying to make already exists
            $runlevels_ui{$sn}{$rl} = "$sk$pri";
            return 'exists';
        }
    }

    opendir (RL, "$etc_rc$rl.d") or die "$0: opendir $etc_rc$rl.d : $!";

    foreach (grep { /[SK]\d\d$sn$/i } readdir(RL)) {
	verbose("rm $etc_rc$rl.d/$_");
        next unless write_ok();
	unlink "$etc_rc$rl.d/$_"
	    or die "Can't unlink $etc_rc$rl.d/$_ : $!";
    }

    # If completely deleting the link, $sk will
    # be empty.
    if (!(defined $sk) || $sk eq '') {
        $runlevels_ui{$sn}{$rl} = undef;
        return 1;
    }

    $pri = get_pri_cache($sn, $rl, $sk) unless $pri;

    unless ($pri =~ /^\d\d$/) { die "Priority isn't two numbers: $pri" }
    unless ($sk  =~ /^[SK]$/) { die "You have to use S or K to start a link" }

    verbose("symlink $initd/$sn $etc_rc$rl.d/$sk$pri$sn");
    # unlike ln relative symlinks are relative to the target file, not the cwd
    $runlevels_ui{$sn}{$rl} = "$sk$pri";
    return unless write_ok();
    symlink "../init.d/$sn", "$etc_rc$rl.d/$sk$pri$sn"
        or die "Can't symlink $etc_rc$rl.d/$sk$pri$sn to ../init.d/$sn : $!\n";
}

sub write_ok
{
    my $ok = !$opts{read_only};
    if (!$first_write_attempted) {
        if (!$ok) {
            ui_warning("Service symlinks will not be changed:\n" .
                       "insufficient rights or running in read-only mode.");
        }
        $first_write_attempted = 1;
    }
    return $ok;
}

sub chkconfig_emulation
{
    $opts{chkcfg_sn}    = $ARGV[0] if defined $ARGV[0];
    $opts{chkcfg_state} = $ARGV[1] if defined $ARGV[1];
    
    if ($opts{chkcfg_sn} && not $opts{chkcfg_state}) {
        infotext($opts{chkcfg_sn}) if not $called_as_chkconfig;
        # Check to see if service is configured to run in current rl
        # exit true if it is, false if not.
        # See chkconfig(8) 
        my $current_rl = current_runlevel();
        if (exists $runlevels{$opts{chkcfg_sn}}{$current_rl}) {
            if ($runlevels{$opts{chkcfg_sn}}{$current_rl} =~ /^S/) {
                # Service is configured to start, exit true
                return 0;
            }
        }
        return 1;
    }

    if ($opts{chkcfg_add}) {
	my $sn = $opts{chkcfg_add};
	if (! -f "$initd/$sn") {
	    die "no initscript for $sn";
	}
	if (! -e "$initd/$sn") {
	    printf STDERR "warning: $sn initscript is not executable\n";
	}
	my $lsb = $services{$sn}->{'lsb_info'} or die "No LSB headers defined for service\n";
	foreach (@{($lsb->{'Default-Start'})}) {
            update_link($sn, $_, 'S', undef) if not defined $runlevels{$sn}{$_};
	}
	foreach (@{($lsb->{'Default-Stop'})}) {
            update_link($sn, $_, 'K', undef) if not defined $runlevels{$sn}{$_};
	}
	return 0;
    }

    if ($opts{chkcfg_del}) {
	my $sn = $opts{chkcfg_del};
	foreach (@valid_rls) {
            update_link($sn, $_, '', undef);
	}
	return 0;
    }

    if ($opts{chkcfg_sn} && $opts{chkcfg_state}) {
        my @set_rls = split //, $opts{chkcfg_levels};
	my %sk;
        if ($opts{chkcfg_state} =~ /^on$/i) {
            %sk = map { $_ => 'S' } @set_rls;
        }
        elsif ($opts{chkcfg_state} =~ /^off$/i) {
            %sk = map { $_ => 'K' } @set_rls;
        }
	elsif ($opts{chkcfg_state} =~ /^reset$/i) {
	    my $lsb = $services{$opts{chkcfg_sn}}->{'lsb_info'} or die "No LSB headers defined for service\n";
	    @set_rls = @valid_rls;
	    %sk = ((map { $_ => 'S' } @{($lsb->{'Default-Start'} // [])}),
	           (map { $_ => 'K' } @{($lsb->{'Default-Stop'} // [])}));
        }
	else {
            pod2usage();
        }

        if (! -e "$initd/$opts{chkcfg_sn}") {
            my $warning = "no such initscript $opts{chkcfg_sn}";
            if (defined ($runlevels{$opts{chkcfg_sn}})) {
                print STDERR "warning: $warning but symlinks exist so proceeding anyway.\n";
            } elsif ($opts{force}) {
                print STDERR "warning: $warning but --force requested so proceeding anyway.\n";
            } else {
                die "error: $warning. Use --force to proceed anyway.\n";
            }
        }

        foreach (@set_rls) {
            update_link($opts{chkcfg_sn}, $_, $sk{$_}, undef);
        }

        return 0;
    }

    # Program isn't being called like chkconfig, so return to normal
    # operation.
    return 2;
}

sub list_output
{
    my (%runlevels) = @_;

    # There was an argument to --list
    my $opt_sn = $opts{chkcfg_list};

    # Find out which runlevels are used
    my @ss = grep { !$opt_sn or $_ eq $opt_sn } (sort keys %runlevels);
    my @rr = uniq_by { $_ } (map { keys %$_ } (map { $runlevels{$_} } @ss));

    # Do the output
    foreach my $sn (@ss) {
        my $output = substr $sn, 0, $opts{label_width};
        $output .= " " until length $output >= $opts{label_width} + LIST_SN_PAD;

        foreach my $rl (sort @rr) {
	    if (defined($runlevels{$sn}{$rl})) {
                $output .= "$rl:";
                if ($runlevels{$sn}{$rl} =~ /^[Ss]/) {
                    $output .= "on ";
                }
                else {
                    $output .= "off";
                }
                $output .= "   ";
	    } else {
		$output .= "        ";
	    }
        }
        chop($output);
        $output .= "\n";
        print $output;
    }
}
            
sub revert_service
{
    my $sn = shift;
    my %cache = %{service_controls};

    foreach my $rl (keys %{$runlevels{$sn}}) {
        $runlevels{$sn}{$rl} =~ /^([SK])(\d\d)$/;
        my ($sk, $pri) = ($1, $2);

        # In RW mode, trying to revert the link will tell us if UI needs reverting
        next if update_link($sn, $rl, $sk, $pri) eq 'exists' and write_ok();

        if (defined($cache{$sn}{$rl})) {
            my $box = $cache{$sn}{$rl};
            if ($opts{priority}) {
                # Reset the text
                $box->text($sk.$pri);
                $box->draw(1);
            }
            else {
                # Simple layout, just toggle the box.
                $box->set($sk eq 'K' ? 0 : 1);
                $box->draw(1);
            }
        }
    }
    unless ($opts{priority}) {
        foreach my $rl (keys %{$cache{$sn}}) {
            if (!defined($runlevels{$sn}{$rl})) {
                my $box = $cache{$sn}{$rl};
                    update_link($sn, $rl, '', undef);
                $box->set(2);
                $box->draw(1);
            }
        }
    }
    update_note($sn);
}

sub revert_changes
{
    foreach my $sn (keys %runlevels) {
        revert_service($sn);
    }

    $cui->dialog("All symlinks restored to original state!");
}

sub revert_one
{
    my ($widget) = @_;
    my $ud = $widget->userdata();

    revert_service($ud->{sn});
    $widget->focus();
}

sub find_dialog
{
    my $search_string = $cui->question(-question =>
        'Find service. (Then press n/p for next/previous.)');
    if (($search_string // "") =~ /.+/) {
        @found = sort grep { index($_, $search_string) != -1 } (keys %services);
        undef $found_index;
    }
    goto_next();
}

sub goto_service
{
    my ($sn) = @_;
    my $box = $service_controls{$sn}->{$show_rls[0]};
    my $scr = $box->parent();
    my $id = $box->userdata()->{id};
    $current_screen = first { $s[$_] == $scr } 0..$#s;
    $id =~ /^(\d+),(\d+)$/;
    _move_focus(int($1), $box_pos{col});
}

sub goto_prev
{
    return if (scalar @found) == 0;
    $found_index = ($found_index // 0) -1;
    $found_index = (scalar @found) -1 if $found_index < 0;
    goto_service($found[$found_index]);
}

sub goto_next
{
    return if (scalar @found) == 0;
    $found_index = ($found_index // -1) + 1;
    $found_index = 0 if $found_index == (scalar @found);
    goto_service($found[$found_index]);
}

sub start_service
{
    my ($widget) = @_;
    my $ud = $widget->userdata();

    st_service($ud->{sn}, 'start');
}

sub stop_service
{
    my ($widget) = @_;
    my $ud = $widget->userdata();

    st_service($ud->{sn}, 'stop');
}

sub st_service
{
    my ($sn, $st) = @_;

    my $output = `/usr/sbin/service $sn $st 2>&1`;
    my $rc = $? >> 8;

    verbose("$initd/$sn $st (return code $rc): $output");
    $cui->dialog("Results of /usr/sbin/service $sn $st (return code $rc):\n" . $output);
}

sub update_note
{
    my ($sn) = shift;

    my $infobox = $extra_controls{$sn}{"info"};
    my $defdiff = $extra_controls{$sn}{"defdiff"};
    return if !defined $infobox and !defined $defdiff;

    my $rl_state = rl_str($sn);

    if (defined $infobox) {
        my ($invert, $colour, $note) = interp($sn, $rl_state);
        $infobox->text($note);
        $infobox->set_color_fg($colour);
        $infobox->reverse($invert);
        $infobox->draw(1);
    }

    if (defined $defdiff) {
        my $lsb_rls = $services{$sn}->{lsb_rls};
        $defdiff->text(default_status($lsb_rls, $rl_state));
    }
}

sub toggle_multiuser
{
    my ($widget) = @_;
    my $ud = $widget->userdata();
    my $sn = $ud->{sn};
    my $multiuser = qr/[2345]/;
    my @multirls = grep { $_ =~ $multiuser } @rls;
    my @multishowrls = grep { $_ =~ $multiuser } @show_rls;
    my %controls = %{$service_controls{$sn}};
    my $w = $widget;
    my $next;

    # Decide current state based on where cursor is if possible
    if ($ud->{runlevel} !~ $multiuser) {
        # else if any of the multiuser services is ON, switch all to OFF
        $w = $controls{max_by { $controls{$_}->get() == 1 } (@multishowrls)};
    }
    $next = $w->get() == 1 ? 0 : 1;

    foreach my $rl (@multirls) {
        my $box = $controls{$rl};

        update_link($sn, $rl, $next ? 'S' : 'K', undef);
        if (defined $box) {
            $box->set($next);
            $box->draw(1);
        }
    }
    update_note($sn);
    $widget->focus();
}

sub set_lsb_defaults
{
    my ($widget) = @_;
    my $ud = $widget->userdata();
    my $sn = $ud->{sn};
    my %controls = %{$service_controls{$sn}};
    my $next;

    my $lsb = $services{$sn}->{'lsb_info'};
    if (!defined $lsb) {
	$cui->error("No LSB headers defined for service: $sn\n");
	return;
    }
    my %def = ((map { $_ => 'S' } @{($lsb->{'Default-Start'} // [])}),
	       (map { $_ => 'K' } @{($lsb->{'Default-Stop'} // [])}));

    foreach my $rl (@valid_rls) {
        update_link($sn, $rl, $def{$rl}, undef);
    }
    foreach my $rl (@show_rls) {
	my $box = $controls{$rl};
	$box->set((defined $def{$rl}) ? ($def{$rl} eq 'S' ? 1 : 0) : 2);
	$box->draw(1);
    }
    update_note($sn);
    $widget->focus();
}

sub infobox
{
    my ($widget) = @_;
    my $ud = $widget->userdata();
    my $sn = $ud->{sn};
    my $rl_state = rl_str($sn);
    my ($invert, $colour, $note) = interp($sn, $rl_state);

    my $lsb = $services{$sn}->{'lsb_info'};
    if (!defined $lsb) {
	$cui->error("No LSB headers defined for service: $sn\n");
	return;
    }
    my $lsb_rls = $services{$sn}->{lsb_rls};

    my $box = $cui->add(
        "infobox", 'Window',
        -title        => "$sn: $lsb->{'Short-Description'}",
        -border       => 1,
        -padtop       => 3,
        -padbottom    => 1,
        -padleft      => 12,
        -padright     => 12,
        -ipad         => 1,
        -ipadbottom   => 0,
        -modal        => 1,
    );
    $box->set_color_tfg($colour);
    my $desc = $box->add(
        undef, 'TextViewer',
        -text         => $lsb->{'Description'} // $lsb->{'Short-Description'} // $sn,
        -wrapping     => 1,
        -height       => 4,
        -vscrollbar   => 1,
        -border       => 0,
    );
    my $grid = $box->add(
        undef, "Container",
        -releasefocus => 1,
        -y            => 5,
    );
    my $label1 = $grid->add(
        undef, "Label",
        -text   => "Settings  :\nRunlevels :\nChanged?  :"
    );
    my $settings = $grid->add(
        undef, 'Label',
        -x       => 12,
        -width   => 28,
        -text    => $note,
        -reverse => $invert,
    );
    $grid->add(
        undef, 'Label',
        -x      => 12,
        -y      => 1,
        -height => 2,
        -text   => (join "\n",
          rl_prettystr($sn),
          ($lsb_rls eq rl_str($sn) ? "Using LSB defaults" : "Changed from LSB defaults"),
        ),
    );
    $settings->set_color_fg($colour);
    my $code = "";
    if (open (my $SCRIPT, "<:encoding(utf8)", "$initd/$sn")) {
        chomp (my @lines = <$SCRIPT>);
        close $SCRIPT;
        my $width = length (sprintf "%d", $#lines);
        my $ln = 1;
        $code = join "\n", (map { sprintf "%0*d: %s", ($width, $ln++, $_); } @lines);
    }
    my $script = $grid->add(
        undef, 'TextViewer',
        -y      => 3,
        -height => $grid->canvasheight() - 4,
        -vscrollbar => 1,
        -border     => 1,
        -text       => $code,
    );
    $desc->set_color_sfg($colour);
    $script->set_color_sfg($colour);
    $grid->layout();

    my $buttons = $box->add(
        undef, 'Buttonbox',
        -buttons      => [ { -label => 'OK', -shortcut => 'o',
                             -onpress => sub { $cui->delete("infobox"); $cui->draw(); } } ],
        -buttonalignment => 'middle',
        -y      => $box->canvasheight() - 1,
    );
    $buttons->set_color_fg($colour);
    $box->focus();
    $script->focus();
    sub exitbox {
        $cui->delete("infobox");
        $cui->draw();
    };
    $box->set_binding( \&exitbox,  "q" );
    $box->set_binding( \&exitbox,  "o" );
    $box->set_binding( \&exitbox,  "i" );
    $box->set_binding( \&exitbox,  KEY_ENTER );
    $buttons->draw();
}

sub infotext
{
    my ($sn) = @_;
    my $rl_state = rl_str($sn);
    my $svc = $services{$sn};

    if (!defined $svc) {
        printf("Service %s not present\n", $sn);
        return 1;
    }

    my ($invert, $colour, $note) = interp($sn, $rl_state);
    my $lsb = $svc->{lsb_info} // {};
    my $lsb_states = join "", (map { $svc->{lsb_defaults}->{$_} // 2 } @all_lsb_rls)
        if defined $svc->{lsb_defaults};
    my $lsb_note;
    ($_, $_, $lsb_note) = interp($sn, $lsb_states);
    my $lsb_rls = (join " ", (map { $_.(($svc->{lsb_defaults}->{$_} // 2) =~ tr/012/KS~/r) } @all_lsb_rls))
        . "  ($lsb_note)"
        if defined $svc->{lsb_defaults};

    my @infos = (
        Service => $sn,
        Summary => $lsb->{'Short-Description'},
        Runlevels => rl_prettystr($sn) . "  ($note)",
        LSB => $lsb_rls,
        Description => $lsb->{Description},
    );

    while (my ($k, $v) = splice(@infos, 0, 2)) {
        printf "%12s: %s\n", ($k, $v) if $v;
    }

    return 0;
}

sub get_pri_cache
{
    my ($sn, $rl, $sk) = @_;
    # See if we can get an exact match from the cache, if not try to match
    # the S or K except in a different run level, if there still is not a match
    # get the opposite of S or K on another runlevel, if still no match return
    # the default.

    open CACHE, "< $opts{cache_dir}/services" 
        or die "Can't open $opts{cache_dir}/services : $!";

    chomp (my @cache = <CACHE>);
    close CACHE;

    # Try an exact cache match
    foreach (@cache) {
        # $arg_rl $arg_sk $arg_pri $arg_sn
        next unless /^$rl\s+$sk\s+(\d\d)\s+$sn$/;
        verbose("Got exact cache match for priority: $_");
	    return $1;
    }

    # Try an $sk match, except on any runlevel
    foreach (@cache) {
        next unless /^[\dsS]\s+$sk\s+(\d\d)\s+$sn$/;
        verbose("Got differing runlevel cache for priority: $_");
        return $1;
    }

    # Ok, try to match on any runlevel with either S or K
    foreach (@cache) {
        next unless /^[\dsS]\s+([SK])\s+(\d\d)\s+$sn$/;
	my $p = int($2);
        $p = 1 if $p == 0;
        verbose("Returning difference of 100 and $p: $_");
        # Sequence numbers are usually defined as stop = 100 - start
        # So that means that start = 100 - stop
        # Above we would have returned if $sk eq $1 so we know that $1 is
        # the opposite of $sk. So return 100 - $2.
        return prio_pad(100 - $p);
    }

    verbose("No cache found, returning default");
    return DEFAULT_S_PRI if $sk eq 'S';
    return DEFAULT_K_PRI if $sk eq 'K';
}

sub pri_box_changed
{
    my ($widget) = @_;

    my $ud = $widget->userdata();
    my $new_link = $widget->get();

    if ($new_link eq $ud->{last_good_text}) {
	# Text didn't actually change, just moved out of the box
	return 1;
    }

    if ($new_link =~ /^([KS])(\d\d)$/ or $new_link =~ /^$/) {
	my ($sk, $pri) = ('', '');
	if (defined $1 and defined $2) {
	    $sk  = $1;
	    $pri = $2;
	}

	update_link($ud->{sn}, $ud->{runlevel}, $sk, $pri);

	$ud->{last_good_text} = $new_link;
    }
    else {
	$cui->error("Incorrect format: $new_link\n" .
		    "The correct format is a K or S followed by two digits!\n" .
		    "Returning field back to original state."
		   );

	# Set the text in the box back to whatever the last good text was.
	$widget->{-text} = $ud->{last_good_text};
    }
}

sub simple_box_changed
{
    my ($box) = @_;
    my $userdata = $box->userdata();
    my $new_state = $box->get();
    my $sn = $userdata->{sn};

    $userdata->{changed}++;

    if ($new_state == 1) {
	update_link($sn, $userdata->{runlevel}, 'S', undef);
    } 
    else {
        if ($new_state == 0) {
            update_link($sn, $userdata->{runlevel}, 'K', undef);
        }
        else {
            update_link($sn, $userdata->{runlevel}, '', undef);
        }
    }
    update_note($userdata->{sn});
}

sub runlevel_status 
{
    my %runlevels = ();
    my %services = ();

    opendir (INITD, $initd) or die "$0: opendir $initd : $!";
    while ( defined(my $service = readdir INITD) ) {
	my %info = ();
	next if $service =~ /\.sh$/; # see the debian policy manual
        next if $service =~ /\.dpkg-/; # see the debian policy manual
	next if $service =~ /^\./; # ignore ., .., and any dot (hidden) files
        next if $service =~ /^rc$/; # ignore rc
        next if $service =~ /^rcS$/; # ignore rcS
        next if $service =~ /^skeleton$/; # ignore the sample init.d file
        next unless -f "$initd/$service"; # ignore non-files
	# ignore stuff like README
	$runlevels{$service} = { } if -x "$initd/$service";
	my $lsb = $LSB->get_headers($service);
        if (defined $lsb) {
            $info{lsb_headers} = $lsb;
            $info{lsb_info} = $LSB->parse_headers($lsb);
            my %defs = ((map { $_ => 1 } @{($info{lsb_info}->{'Default-Start'} // [])}),
	                (map { $_ => 0 } @{($info{lsb_info}->{'Default-Stop'} // [])})) if defined $info{lsb_info};
            $info{lsb_defaults} = \%defs;
            $info{lsb_rls} = join "", (map { $info{lsb_defaults}->{$_} // 2 } @all_lsb_rls) if defined $info{lsb_defaults};
        }
	$services{$service} = \%info;
    }
    closedir INITD or die "$0: closedir $initd : $!";

    # While 7-9 usually aren't used, init supports it.
    foreach my $rl (@rls) {
	unless (opendir(DIR, "$etc_rc$rl.d")) {
	    next if $rl =~ /^[789S]$/;
	    die "$0: opendir $etc_rc$rl.d : $!";
	}
	while ( defined(my $file = readdir DIR) ) {
	    $file = "$etc_rc$rl.d/$file"; # Add the pathname to the file
	    next unless -l $file;
	    next if $file =~ /\.sh$/;
	    next unless $file =~ /([SK])(\d\d)(.+)$/;
	    my ($sk, $pri, $sn) = ($1, $2, $3);
	    $runlevels{$sn}{$rl} = $sk.$pri;
	}
	closedir DIR or die "$0: closedir $etc_rc$rl.d : $!";
    }

    update_cache(\%runlevels) if ! defined $opts{chkcfg_list} and !$opts{read_only};
    return (\%runlevels, \%services);
}

sub setup_cache_env
{
    unless (-e $opts{cache_dir}) {
        verbose("Creating non-existant cache directory: $opts{cache_dir}");
        mkdir $opts{cache_dir} or $opts{read_only} or die "Can't create $opts{cache_dir} : $!";
    }

    if ($opts{purge_cache}) {
        verbose("Purging the services cache");
        if (-e "$opts{cache_dir}/services") {
            unlink "$opts{cache_dir}/services"
                or die "Can't unlink $opts{cache_dir}/services : $!";
        }
    }

    unless (-e "$opts{cache_dir}/services" or $opts{read_only}) {
        # Later we need to open the file with +< which can't create a new file
        # so we'll emulate touch.
        verbose("Touching $opts{cache_dir}/services");
        open CACHE, "> $opts{cache_dir}/services"
            or die "Can't touch $opts{cache_dir}/services : $!";
        close CACHE;
    }
}

sub update_cache
{
    my ($runlevels) = @_;

    open CACHE, "+< $opts{cache_dir}/services"
        or die "Can't open $opts{cache_dir}/services for rw access : $!";

    # Check to see if this service & rl already exists somewhere in this file
    # and update the line if so.
    my %touched = ();
    my $eol = 0;
    while (<CACHE>) {
	my $sol = $eol;
        chomp;
        $eol = tell(CACHE);
        next unless /^([\dSs])\s+([SK])\s+(\d\d)\s+([^\s]+)$/;

        my ($rl, $sk, $pri, $sn) = ($1, $2, $3, $4);

        if (defined $runlevels->{$sn} &&
            defined $runlevels->{$sn}{$rl}) {
            $runlevels->{$sn}{$rl} =~ /^([SK])(\d\d)$/;
            $touched{$sn}{$rl} = 1;

            my ($n_sk, $n_pri) = ($1, $2);
            next if $sk eq $n_sk && $pri eq $n_pri;

            my $old = $_;
            s/^.+$/$rl $n_sk $n_pri $sn/;

            verbose("Updating service cache line\n from: $old\n   to: $_\n");
            seek CACHE, $sol, 0;
            if ($eol - $sol - length("\n") != length($_)) {
                # This defensive path is unnecessary with well-formed content.
                # Cause the updated version to be written at end of file.
                print CACHE "#" x ($eol - $sol - length("\n"));
                undef $touched{$sn}{$rl};
            } else {
                print CACHE $_;
            }
            seek CACHE, $eol, 0;
        }
    }

    foreach my $sn (sort keys %{$runlevels}) {
        foreach my $rl (sort keys %{$runlevels->{$sn}}) {
            unless (defined $touched{$sn}{$rl}) {
                $runlevels->{$sn}{$rl} =~ /^([SK])(\d\d)$/;
                print CACHE "$rl $1 $2 $sn\n";
            }
        }
    }

    close CACHE or die "Can't close $opts{cache_dir}/services : $!";
}

#--- UI subs ---#
sub ui_message
{
    my $level = shift;
    my $message = shift;

    if ($cui) {
        $message = uc($level) . ": " . $message;
        if ($level =~ /^(warning|error)$/) {
	    $cui->error($message);
        } else {
	    $cui->dialog($message);
        }
    } else {
        $message =~ s/\n/ /g;
        print STDERR uc($level) . ": $message\n";
    }
}

sub ui_warning { ui_message("warning", @_) }
sub ui_error { ui_message("error", @_) }

#--- Misc subs ---#
sub check_args
{
    $opts{show} ||= get_default_show();

    unless ($opts{show} =~ /^[S0-9]*$/) {
	die "$0: --show must match [S0-9]\n";
    }

    if (length($opts{show}) > MAX_ROWS) {
	die "$0: can only show ". MAX_ROWS . "rows at a time\n";
    }

    return 1;
}

sub check_suitable_init
{
    if ($opts{root} ne '/') {
        return undef;
    } elsif ( -d "/run/systemd/system" ) {
        $alt_init_handler = \%systemd_handler;
        return "systemd is init;\n" .
               "${OURNAME}'s controls will be ineffective.\n" .
               "Use systemctl(1).";
    } elsif ( -f "/run/runit.stopit" ) {
        $alt_init_handler = \%runit_handler;
        return "runit is init;\n" .
               "${OURNAME}'s controls will be ineffective for services with " .
               "service directories managed by runit.\n" .
	       "Use update-service(8).";
    } elsif ( -d "/run/openrc" ) {
        return "openrc is runlevel manager;\n" .
               "${OURNAME}'s controls will be ineffective.\n" .
               "Use rc-update(8).";
    } else {
        return undef;
    }
}

sub check_root_access
{
    my @dirs = ( (grep { -d $_ } (map { "$etc_rc$_.d" } @rls)),
		 (-d $opts{cache_dir}) ? $opts{cache_dir} : dirname($opts{cache_dir}));
    my @fails = grep { not -w $_ } @dirs;
    if (@fails and not $opts{read_only}) {
        verbose("Switching to read-mode because no write access to: ".join(' ', @fails));
        $opts{read_only} = 1;
    }
}

sub current_runlevel
{
    if (-e $runlevel_cmd) {
        my $rl_out = `$runlevel_cmd`;
        return 1 if $rl_out =~ /unknown/;
        $rl_out =~ /^\S\s?([Ss\d])?$/ or
            die "Unknown return from $runlevel_cmd : $rl_out";
        return $1;
    }
    else {
        return 1;
    }
}

sub default_runlevel
{
    my $rl = 2;
    if (open (my $inittab, "<", "$opts{root}etc/inittab")) {
        while (<$inittab>) {
            $rl = $1 if /^\s*[^#:]+:([^:]+):initdefault:/;
        }
        close($inittab);
    }
    return $rl;
}

sub probably_oneshot
{
    my $service = shift;
    my $rls = join '', keys %{$runlevels{$service}};
    return $rls =~ /[S016]/ && $rls !~ /[2345]/;
}


sub split_services
{
    # Figure out how many services can fit on the screen, then make
    # as many screens as needed to fit all the services.
    my @screens = ();

    my @services = ();

    my %o_opts = ();
    $o_opts{p} = 1 if $opts{order} =~ /p/;
    $o_opts{n} = 1 if $opts{order} =~ /n/;
    $o_opts{s} = 1 if $opts{order} =~ /s/;
    $o_opts{a} = 1 unless exists $o_opts{p};

    if ($opts{order} =~ /([Ss\d])/) {
        $o_opts{rl} = $1;
    }
    else {
        # If the --order option didn't set a runlevel to sort by, then
        # use the current runlevel (from the output of /sbin/runlevel) or
        # sort by runlevel 1 if the runlevel command doesn't exsist on this
        # system.
        $o_opts{rl} = current_runlevel();
    }
    
    # Process the opts we just set.
    if (exists $o_opts{a}) {
        if (exists $o_opts{n}) {
            # Include the priority num on an alpha sort
            foreach my $sn (sort keys %runlevels) {
                next unless exists $runlevels{$sn}{$o_opts{rl}};
                next unless $runlevels{$sn}{$o_opts{rl}} =~ /^[SK](\d\d)$/;
                my $render_as = "$1 $sn";
                push @services, $render_as;
                if (length $render_as > $longest_service) {
		    $longest_service = length $render_as;
		}
            }
        }
        elsif (exists $o_opts{s}) {
            @services = sort {
                my $oneshot = probably_oneshot($a) <=> probably_oneshot($b);
                return $oneshot == 0 ? $a cmp $b : $oneshot;
            } keys %runlevels;
        }
        else {
            # Standard alpha sort
            @services = sort keys %runlevels;
        }
    }
    elsif (exists $o_opts{p}) {
        # Sort by priority at runlevel specified or current runlevel

        my @tmp_order = ( [ ], [ ] ); # S is 0 and K is 1
        foreach my $sn (keys %runlevels) {
            next unless exists $runlevels{$sn}{$o_opts{rl}};
            next unless $runlevels{$sn}{$o_opts{rl}} =~ /^([SK])(\d\d)$/;
            
            if    ($1 eq 'S') { push @{$tmp_order[0]}, $2.$sn }
            elsif ($1 eq 'K') { push @{$tmp_order[1]}, $2.$sn }
        }

        foreach (0, 1) {
            foreach my $ddsn (sort @{$tmp_order[$_]}) {
                $ddsn =~ /^(\d\d)(.+)$/;
                if (exists $o_opts{n}) {
                    # Include the priority num on a pri sort
                    push @services, "$1 $2";
                }
                else {
                    push @services, $2;
                }
            }
        }
    }

    {
        # We could be missing some services if they didn't have a link in the
        # runlevel we were sorting by. This happens in all circumstances except
        # the default of just 'a' being set.
        my %seen = ();
        foreach (@services) {
            next unless $_ =~ /^(\d\d )?(.+)$/;
            $seen{$2} = 1;
        }

        foreach (sort keys %runlevels) {
            unless (exists $seen{$_}) {
                push(@services, $_);
            }
        }
    }
    
    my $per_screen = max_services();

    my $i = 0;
    do {
        $screens[$i] = [ splice(@services, 0, $per_screen) ];
        $i++;
    } while @services;

    return @screens;
}

sub max_services
{
    if (!defined $_cached_max_services) {
	my $tmp_screen = $cui->add(
	    undef, 'Window',
	    -title	    => "N/A",
	    -border	    => 1,
	    -padtop	    => 1,
	    -padbottom	    => 4,
	    -ipad	    => 1,
	);
	$_cached_max_services = $tmp_screen->canvasheight() - TOP_LABEL_HEIGHT;
	$max_service_digits = length(sprintf("%.2u", $_cached_max_services));
	undef $tmp_screen; # Make sure the memory is cleaned up.
    }
    return $_cached_max_services
}

sub get_default_show
{
    my $show = '';
    foreach (@rls) {
	if (-e "$etc_rc$_.d") {
	    $show .= $_;
	}
    }
    return $show;
}

sub prio_pad { sprintf('%.2u', $_[0]); }
sub rlid_pad { sprintf('%.2u', $_[0]); }
sub rowid_pad { sprintf('%.*u', $max_service_digits, $_[0]); }
sub cell_id { return rowid_pad($_[0]).','.rlid_pad($_[1]); }

sub verbose { print VERBOSE $_[0]."\n" if $opts{verbose}; }


#--- Screen layout subs ---#
sub create_main_window
{
    my $service_controls = shift;
    my $max_width;

    create_help_window();
    
    my $i = 0;
    foreach my $services (@snames_per_screen) {

	# First create the main window all of this page of services goes in
	my $id = "window_$i";
	my $screen = $cui->add(
	    $id, 'Window',
	    -title	    => $title_help,
	    -border	    => 1,
	    -padtop	    => 1,
	    -padbottom      => 4,
	    -ipad	    => 1,
	);

        $max_width = $screen->width();
        $opts{label_width} //= max_by { $_ } (
            DEFAULT_LABEL_WIDTH,
            min_by { $_ } (
                $longest_service,
                $max_width - scalar(@show_rls) * 8 - 6
            )
        );

        # Can't set these globally (on $cui) or else it overrides the
        # keybindings on all other objects
        $screen->set_binding( \&move_up,    KEY_UP(),    );
        $screen->set_binding( \&move_down,  KEY_DOWN(),  );
        $screen->set_binding( \&move_left,  KEY_LEFT(),  );
        $screen->set_binding( \&move_right, KEY_RIGHT(), );
        $screen->set_binding( \&next_page, "\cN" );
        $screen->set_binding( \&prev_page, "\cP" );
        $screen->set_binding( \&next_page, KEY_NPAGE );
        $screen->set_binding( \&prev_page, KEY_PPAGE );
        $screen->set_binding( \&first_service, KEY_HOME );
        $screen->set_binding( \&last_service, KEY_END );
	$screen->set_binding( sub { $cui->mainloopExit },  "q" );
        $screen->set_binding( \&toggle_help,  "h" );
        $screen->set_binding( \&find_dialog,  "/" );
        $screen->set_binding( \&goto_next,    "n" );
        $screen->set_binding( \&goto_prev,    "p" );

	create_top_label($screen, $i, $#snames_per_screen);

	my $left_label = '';
	for (my $i = 0; $i < scalar(@$services); $i++) {
            my $sn = $services->[$i];
            $sn =~ s/^\d\d (.+)$/$1/;
            $screen->add(
                "label:$sn", 'Label',
                -text   => $services->[$i],
                -y      => TOP_LABEL_HEIGHT + $i,
                -width  => $opts{label_width},
                -height => 1,
	    );
            # If the labels had numbers, we don't need them anymore.
            $services->[$i] = $sn;
	}

	my $row = TOP_LABEL_HEIGHT;

	foreach my $sn (@$services) {
	    my $controls;
	    if   ($opts{priority}) {
		$controls = draw_priority_layout($screen, $sn, $row);
	    } else {
		$controls = draw_simple_layout($screen, $sn, $row, $max_width - 4);
	    }
	    $service_controls->{$sn} = $controls;
	    $row++;
	}	

	$s[$i] = $screen;

	$i++;
    }
}

sub create_help_window
{
    my $help_text = <<EOF;
Quick key reference:

  Arrow keys: Move around
  PgDn or ^n: Next Page
  PgUp or ^p: Previous Page
  Home:       Go to first service
  End:        Go to last service
  Backspace:  Restore service's symlinks back to state when started
  r:          Restore all symlinks back to state when sysv-rc-conf started
  -:          Stop service now
  +:          Start service now
  i:          Show information about service
  /:          Find service
  n:           - go to next found service
  p:           - go to previous found service
  h:          Toggle help screen on / off
  q:          Back/quit
  CTRL-Z:     Suspend
  CTRL-C:     Quit

  Checkbox Layout:
    Space       Toggle service between most likely alternative states
    m           Toggle service on / off in all multiuser runlevels
    d           Set LSB default runlevels for service
    <           Cycle backwards through states
    >           Cycle forwards through states
    0/o         Set service to stop (off)
    1/x         Set service to start (on)
    2/~         Set service to floating

  Priority Layout:
    Backspace:  Delete text behind cursor
    CTRL-d:     Delete text in front of cursor
    CTRL-f:     Move cursor forward through text
    CTRL-b:     Move cursor backwards through text

Key to symbols:

   $delta    The current runlevel symlinks differ from LSB defaults
  [X]   A start ('S') symlink is present for the service at this runlevel
  [ ]   A stop ('K') symlink is present for the service at this runlevel
  [~]   No symlink is present for the service at this runlevel

        The current runlevel is shown in bold face.
        The default runlevel is underlined.

  NOTE: When using either GUI layout (checkbox or priority), all
  configuration changes to the symlinks will happen IMMEDIATELY, not
  when the program exits.

  The text "RO" appears in the top right hand corner if the tool is started
  in a read-only mode, perhaps because it is not running as root.

  Floating services are shown with a tilde ('~') in the checkbox layout
  and no content in the priority layout. These service have no symlink for
  the given runlevel and stay in their current state on transition to that
  runlevel. When the normal multiuser runlevels 2-5 are floating, this
  typically represents a startup or shutdown service and such services are
  sorted to the end of the list when 's' is included in the order string.

  See the man page for much more detailed documentation on how to use
  sysv-rc-conf and it's various options, also it documents how the system
  uses the init script links, how to get new version of the program, how to
  submit bug reports, etc.

  sysv-rc-conf is released under the GNU GPL.
  Version: $VERSION

  (c) 2004 Joe Oppegaard <joe\@pidone.org>
  (c) 2025 Andrew Bower <andrew\@bower.uk>
EOF

    my $hw = $cui->add(
        'help_window', 'Window',
        -title	    => $title_helpscr,
        -border	    => 1,
        -padtop	    => 1,
        -padbottom  => 4,
        -ipad	    => 1,
        -userdata   => 0,
    );

    $hw->add(
        'help_tv', 'TextViewer',
        -text       => $help_text,
        -title      => 'sysv-rc-conf help',
        -vscrollbar => 1,
    );

    $hw->set_binding( \&toggle_help,  "q" );
    $hw->set_binding( \&toggle_help,  "h" );

}

sub rl_state
{
    my ($sn, $rl) = @_;

    if (defined $runlevels_ui{$sn}{$rl}) {
        if ($runlevels_ui{$sn}{$rl} =~ /^S\d\d$/) {
            return 1;
        }
        return 0;
    }
    return 2;
}

sub rl_str {
    my $sn = shift;
    return join "", (map { rl_state($sn, $_) } @all_lsb_rls);
}

sub rl_prettystr {
    my $sn = shift;
    return join ' ', (map { "$_".(rl_state($sn, $_) =~ tr/012/KS~/r) } @all_lsb_rls);
}

sub interp
{
    my ($sn, $str) = @_;
    return (undef, undef, undef) if not defined $str;

    if ($alt_init_handler->{is_managed}->($sn)) {
        return (1, "blue", "$alt_init_handler->{manager}-managed");
    }

    return @{(first { $str =~ /$_->[0]/ } @{$rl_patterns})}[1..3];
}

sub default_status
{
    my ($lsb_rls, $rlstates) = @_;
    return $lsb_rls eq $rlstates ? '' : $delta,
}

sub draw_simple_layout
{
    my ($screen, $sn, $row, $max_width) = @_;
    my %controls;
    my $right_n = $opts{label_width} + 2;

    for (my $i = 0; $i <= $#show_rls; $i++, $right_n += 8) {
	my $rl = $show_rls[$i];
	my $on_or_off = 0;
        my $state = rl_state($sn, $rl);
	# We only want to show S\d\d services as selected. 
	my $id = cell_id($row - 2, $i);
        my $box = $screen->add(
            $id, 'App::SysVRcConf::Multistate',
            -label      => '',
            -stateind   => $state,
            -border     => 0,
            -x          => $right_n,
            -y          => $row,
            -width      => 5,
            #-height     => 1,
	    -userdata	=> { id		=> $id,
			     sn		=> $sn,
			     changed	=> 0,
			     runlevel	=> $rl,
                             initial_state => $state != 2 ? 1 : 0,
			   },
            -onchange   => \&simple_box_changed,
            -onfocus    => \&got_focus,
	    -states     => [ ' ', 'X', '~' ],
	    -togglemap  => { 0 => $rl =~ /[06]/ ? 2 : 1,
			     1 => $rl =~ /S/ ? 2 : ($state != 2 ? 0 : 2),
			     2 => $rl =~ /[06]/ ? 0 : 1},
	    -shortcuts  => { '0' => 0,
			     'o' => 0,
			     'O' => 0,
			     '1' => 1,
			     'x' => 1,
			     'X' => 1,
			     '2' => 2,
			     '~' => 2},
        );
	$box->set_binding( \&start_service, "+" );
	$box->set_binding( \&stop_service, "-" );
	$box->set_binding( \&toggle_multiuser, "m" );
	$box->set_binding( \&set_lsb_defaults, "d" );
        $box->set_binding( \&revert_one,       KEY_BACKSPACE);
        $box->set_binding( \&infobox, "i" );
	$controls{$rl} = $box;
    }
    my $rlstates = rl_str($sn);
    my $lsb_rls = $services{$sn}->{lsb_rls};
    if ($opts{defdiff} and defined $lsb_rls) {
        my $box = $screen->add(
            "status:$sn", 'Label',
            -text => default_status($lsb_rls, $rlstates),
            -x    => $right_n + ($right_n == $max_width ? -2 : 0),
            -y    => $row,
            -width => 1,
        );
        $extra_controls{$sn}{"defdiff"} = $box;
    }
    if ($opts{interpret}) {
        my ($inv, $col, $note) = interp($sn, $rlstates);
        my $box = $screen->add(
            "info:$sn", 'Label',
            -text => $note,
            -x    => $right_n + ($right_n == $max_width ? -1 : 2),
            -y    => $row,
            -reverse => $inv,
            -fg   => $col,
            -width => $right_n == $max_width ? 28 : 28,
        );
        $extra_controls{$sn}{"info"} = $box;
    }
    return \%controls;
}

sub draw_priority_layout
{
    my ($screen, $sn, $row) = @_;
    my %controls = ();

    for (my $i = 0, my $right_n = $opts{label_width} + 1; $i <= $#show_rls; $i++, $right_n += 8) {
	my $rl = $show_rls[$i];
	my $text = exists $runlevels{$sn}{$rl}
		    ? $runlevels{$sn}{$rl}
		    : '';
	my $id = cell_id($row - 2, $i);
	my $box = $screen->add(
	    $id, 'TextEntry',
	    -sbborder   => 1,
	    -x		=> $right_n,
	    -y		=> $row,
	    -width	=> 6,
	    -maxlength  => 3,
	    -regexp	=> '/^[skSK\d]*$/',
	    -toupper	=> 1,
	    -showoverflow => 0,
	    -text	=> $text,
	    -userdata	=> { id		=> $id,
			     sn		=> $sn,
			     changed	=> 0,
			     runlevel	=> $rl,
			     last_good_text => $text,
			   },
	    -onblur	=> \&pri_box_changed,
	    -onfocus	=> \&got_focus,
	);

	$box->set_binding( \&start_service, "=", "+" );
	$box->set_binding( \&stop_service,  "-" );
        $box->set_binding( \&infobox, "i" );
	$controls{$rl} = $box;
    }
    return \%controls;
}

sub rl_colour {
    my ($rl) = @_;
    if ($rl =~ /[06]/) {
      return 'magenta';
    } elsif ($rl =~ /[2345]/) {
      return 'green';
    } elsif ($rl =~ /1/) {
      return 'yellow';
    } elsif ($rl =~ /S/) {
      return 'cyan';
    } else {
      return undef;
    }
}

sub create_top_label
{
    my ($window, $page, $of) = @_;
    my @label_rls = @show_rls;
    my $space = $opts{label_width} - 4;
    my $pages = sprintf(" %s%d/%d%s  ", $space > 13 ? "page " : "", $page + 1, $of + 1, $space > 10 ? "  " : "");

    $window->add(
	undef, 'Label',
	-text	=> 'service' . sprintf("%*.*s", $space, $space, $pages),
	-y	=> 0,
	-x	=> 0,
	-height	=> 1,
	-textalignment	=> 'left',
    );
    $window->add(
	undef, 'Label',
	-text	=> "-" x ($opts{label_width} + 2 + scalar(@show_rls) * 8),
	-y	=> 1,
	-x	=> 0,
	-height	=> TOP_LABEL_HEIGHT - 1,
	-textalignment	=> 'left',
    );

    my $rl = current_runlevel();
    my $drl = default_runlevel();
    my $x = $opts{label_width} + 3;
    foreach (@label_rls) {
        $window->add(
            undef, 'Label',
            -text       => $_,
            -bold       => $_ eq $rl,
            -underline  => $_ eq $drl,
            -x          => $x,
            -y          => 0,
            #-fg         => $_ !~ /uncoloured/ ? rl_colour($_) : undef,
        );
        $x += 8;
    }
}

sub create_bottom_box
{
    my $cmd_text = '';
    my $extra_controls = "m: toggle multiuser   /:find   d: defaults";
    my $cursor = $opts{pictograms} ?
	"\x{2190}\x{2191}\x{2193}\x{2192}\x{21de}\x{21df}\x{21f1}\x{21f2}\x{1f5b1}" :
	"<arrows>";
    if ($opts{priority}) {
	$cmd_text = 
"Editing:     Backspace: bs     ^d: delete      ^b: backward      ^f: forward";
	$extra_controls = " " x (length($extra_controls) + 1);
    }
    else {
	$cmd_text = 
"0: stop [ ]  1: start [X]  2: floating [~]  space: toggle  </>: cycle state";
    }
    
    my $exp_window = $cui->add(
	'exp_window', 'Window',
	-border    => 1,
	-y	   => -1,
	-height	   => BOTTOM_WIN_HEIGHT,
        -ipadleft  => 1,
        -ipadright => 1,
    );
    $exp_window->add(undef, 'Label', 
	-y	=> 0,
	-width	=> -1,
	-height => BOTTOM_LAB_HEIGHT,
	-text   => $opts{pictograms} ?
"$cursor  $extra_controls  r/\x{232b}: restore all/one\n$cmd_text" :
"$cursor  $extra_controls  r/BS: restore all/one\n$cmd_text",
    );
}

sub toggle_help
{
    my $hw = $cui->getobj('help_window');
    my $hw_data = $hw->userdata();

    if ($hw_data == 0) {
        $hw->userdata($cui->getfocusobj);
        $hw->focus();
    }
    else {
        # The help window is up, so turn it off by focusing the last
        # object that was focused on before we pulled up the help window
        $hw_data->focus();
        $hw->userdata(0);
    }
}

#--- Movement subs ---#
sub next_page
{
    $current_screen++;
    $current_screen = 0 if $current_screen > last_screen();
    verbose("Going to screen $current_screen");
    _move_focus(0, $box_pos{col});
}

sub prev_page
{
    $current_screen--;
    $current_screen = last_screen() if $current_screen < 0;
    verbose("Going to screen $current_screen");
    $box_pos{row} = last_row();
    _move_focus($box_pos{row}, $box_pos{col});
}

sub first_service
{
    $current_screen = first_screen();
    verbose("Going to first service");
    $box_pos{row} = (0);
    _move_focus($box_pos{row}, $box_pos{col});
}

sub last_service
{
    $current_screen = last_screen();
    verbose("Going to last service");
    $box_pos{row} = last_row();
    _move_focus($box_pos{row}, $box_pos{col});
}

sub got_focus
{
    my $widget = shift;
    # Is there a better way to figure out my own id besides putting it
    # in userdata on creation and fetching it?
    my $userdata = $widget->userdata();
    my $id = $userdata->{id};
    $last_service = $userdata->{sn};
    $last_page = $widget->parent();

    $id =~ /^(\d+),(\d+)$/;

    $box_pos{row} = int($1);
    $box_pos{col} = int($2);
}

sub move_left
{
    return if $box_pos{col} == 0;
    _move_focus($box_pos{row}, $box_pos{col} - 1);
}

sub move_right
{
    return if $box_pos{col} == scalar(@show_rls)-1;
    _move_focus($box_pos{row}, $box_pos{col} + 1);
}

sub move_up
{
    #return if $box_pos{row} == 0;
    return prev_page() if $box_pos{row} == 0;
    _move_focus($box_pos{row} - 1, $box_pos{col});
}

sub move_down
{
    # Index starts at 00, so we need one less then the max services that
    # are on the screen.
    return next_page() if $box_pos{row} == last_row();
    _move_focus($box_pos{row} + 1, $box_pos{col});
}

sub _move_focus
{
    $box_pos{row} = $_[0];
    $box_pos{col} = $_[1];

    # Underline current service
    my $box = $s[$current_screen]->getobj(cell_id($_[0], $_[1]));
    my $sn = $box->userdata()->{sn};
    my $label = $box->parent()->getobj("label:$sn");
    if (defined $last_service) {
        my $oldlabel = $last_page->getobj("label:$last_service");
        $oldlabel->underline(0) if defined $oldlabel;
    }
    $label->underline(1) if defined $label;

    $box->focus();
}

sub last_row { return scalar(@{$snames_per_screen[$current_screen]})-1; }

sub first_screen { return 0 }

sub last_screen  { return scalar(@s)-1 }

=pod 

=head1 NAME

B<sysv-rc-conf> - Run-level configuration for SysV like init script links

=head1 SYNOPSIS

B<sysv-rc-conf> [ I<OPTIONS> ]

B<sysv-rc-conf>  --list  [ I<service> ]

B<sysv-rc-conf>  --add|--del  [ I<service> ]

B<sysv-rc-conf> [ --level I<levels> ] I<service> E<lt>I<on|off|reset>E<gt>

B<sysv-rc-conf> [ --level I<levels> ] [ --info ] I<service>

=head1 DESCRIPTION

B<sysv-rc-conf> gives an easy-to-use interface for managing
C</etc/rc?.d/> symlinks. The interface comes in two different
flavors, one that simply allows turning services on or off and another that
allows for more fine-tuned management of the symlinks.

B<sysv-rc-conf> can also be used at the command line when the desired changes
to the symlinks are already known. The syntax is borrowed from B<chkconfig(8)>,
although it does not follow it exactly.

=head1 OPTIONS

=head2 General Options

=over

=item B<-c> DIRECTORY, B<--cache=>DIRECTORY

The directory where the priority numbers, old runlevel configuration, etc. 
should be stored. This defaults to C</var/lib/sysv-rc-conf>. See the FILES
section below and the --Purge option.

=item B<-r> DIRECTORY, B<--root=>DIRECTORY

The root directory to use. This defaults to C</>. This comes in handy if the
root file system is mounted somewhere else, such as when using a rescue disk.

=item B<-P>, B<--Purge>

Purge the information stored in the cache file. See the FILES section below
and the --cache option. The user interface is inhibited unless B<--UI> is
also specified.

=item B<-f>, B<--force>

Force an action that would otherwise be aborted on sanity check failure.
This applies to creating a symlink on the command line for a non-existent
initscript in case of a typo.

=item B<--insserv>

Run insserv(8) to recalculate dependency-based boot sequence and priorities
on exit.

=item B<-v> FILE, B<--verbose=>FILE

Print verbose information to C<FILE>

=item B<-V>, B<--Version>

Print version information to STDOUT and exit

=item B<-w>, B<--width=>COLUMNS

Set width in characters to use for the service label column in lists and the
configuration screen.

=item B<--RO>

Read only: never change any symlinks or the service cache.

=item B<--UI>

Enter the curses user interface even if performing a batch operation which
would not otherwise show the UI. (Does not apply to B<chkconfig>
compatibility mode, including setting levels, or to B<--list>.)

=item B<--batch>

Avoid the curses user interface, reversing a setting of B<--UI>.

=back

=head2 GUI-Related Options

=over

=item B<-o> [ see description ], B<--order=>[ see description ]

Allows various sorting orders and ways to display the rows. The argument can be
made up of any of the following:

=over

=item B<a>

Sort the rows B<a>lphabetically.

=item B<n>

Show the priority numbers along with the name of the service.

=item B<p>

Sorts by the B<p>riority numbers.

=item B<s>

Sorts by whether the service is probably a B<s>tartup/shutdown one-shot
service.

=item B<level>

I<level> can be any runlevel, 0-9 or S. This controls which runlevel the
priority numbers are sorted at. It only makes sense to use this in conjuntion
with B<p>. If omitted the priority numbers are sorted by the current runlevel
the system is in.

=back

The default setting is B<as>, which orders alphabetically with startup/shutdown
one-shot services appearing underneath regular services used in the multi-user
runlevels.

=item B<-p>, B<--priority>

Alternate layout. Instead of just showing a checkbox, the priority of the
service and the S or K are allowed to be edited. This is for more fine-tuned
control then the default layout allows.

On modern systems, it is more useful to enable the B<--insserv> option with the
default layout, so that priority levels will be managed automatically based on
dependencies encoded in LSB headers.

=item B<-s> I<levels>, B<--show=>I<levels>

Which runlevels to show. This defaults to up to 8 of the runlevels available
on the system. Usually this means it will show 1, 2, 3, 4, 5, 0, 6, and S.
The syntax calls for the runlevels to be allruntogether. For instance, to
show runlevels 3, 4, and 5 the syntax would be C<--show=345>. Also see 
B<--order>.

=item B<--interpret>, B<--no-interpret>

Show (default), or do not show, an interpretation of the current runlevel
symlinks on the right hand side of the simple view. This note is truncated
if the terminal is not wide enough.

=item B<--defdiff>, B<--no-defdiff>

Show (default), or do not show, a delta symbol on the right hand side when
current runlevel settings differ from LSB defaults.

=item B<--pictograms>, B<--no-pictograms>

Show, or do not show, unicode pictogram characters in the user interface.

=item B<--colour>, B<--no-colour>

Enable (default) or disable colour in the user interface.

=item B<--classic>

Equivalent to B<--no-colour --no-pictograms --no-interpret --no-defdiff>.

=back

=head2 CLI-Related Options

These options provide emulation for B<chkconfig>.

=over

=item B<--level> I<levels>

The runlevels this operation will affect. I<levels> can be any number from
0-9 or S. For example, B<--level 135> will affect runlevels 1, 3, and 5.
If B<--level> is not set, the default is to affect runlevels 2, 3, 4, and 5.
This option is only used for the command line interface, see the section
below labled USING THE CLI for more information. 

=item B<--list> [I<name>]

This option will list all of the services and if they are stopped or started
when entering each runlevel. If I<name> is specified, only the information
for that service is displayed. If launched as B<chkconfig> with no arguments,
the B<--list> option is implied.

=item B<--add> I<name>

Add the named service, setting any missing runlevel symlinks according to the
LSB header in the initscript, which must be defined. Legacy priority-based
chkconfig settings like C<chkconfig: 2345 20 80> in init scripts are not
supported.

=item B<--del> I<name>

Delete all runlevel symlinks for the named service.

=item B<--info> [I<name>]

In an extension to B<chkconfig>, this option will show some information about
the service, its current configuration, and information from the LSB header
if available.

This information is also presented if the only argument on the command line
is the service name and the tool is not invoked as B<chkconfig>.

=back

=head1 USING THE GUI

=head2 Note

When using either GUI layout described below, all configuration 
changes to the symlinks will happen immediately, not when the program exits.

=head2 Using the Default layout

The default (simple) layout shows in a grid fashion all of the services that
are in C<init.d> and which runlevels they are turned on at. For example, where
the C<ssh> row and C<3> column intersect, if there is an 'X' in the box there
that means the ssh service will be turned on when entering runlevel 3. If there
is no checkbox it means that the service is turned off when entering that
runlevel. A tilde '~' in the box means that there are no links to the service
in that specific runlevel. If more configuration detail is needed, see the next
paragraph and the B<--priority> option.

=head2 Using the Priority layout

The priority (advanced) layout also uses a grid fashion, but instead of
checkboxes there are text boxes that can have a few different values. If the
text box is blank, that means there isn't a symlink in that runlevel for that
service. This means that when changing into that runlevel that the service
will not be started or stopped, which is significant. If the text box starts
with the letter K that means that the service will be stopped when entering
that runlevel. If the text box starts with the letter S that means the service
will be started when entering that runlevel. The two digits following is the 
order in which the services are started. That means that C<S08iptables> would
start before C<S20ssh>. For more information see your system documentation.

If the B<--insserv> option is in use then any tuning to the priority level will
be lost on exit. It is unlikely that tuning the priority level manually will be
useful on a modern system.

=head2 Controls

To move around use the arrow keys, or if the terminal support it, the mouse.
Typically there is more then one page of services (unless the terminal screen
is large), to move between the pages use Page Up or Page Down, or arrow key
down or up at the bottom or top of the screen, respectively. The Home key
will jump to the first service on the first page. The bottom of the
screen also shows these movement commands for quick reference. To restore the
symlinks back to their original state before the B<sysv-rc-conf> was run,
press the B<r> key. To restore just the current service's symlinks, press
the B<backspace> key. The B<h> key will display a quick reference help screen.

=head2 Default layout

When using the default layout use the space bar to toggle the service with the
most likely alternate setting in the context. The C<E<lt>> and C<E<gt>> keys
cycle through the available states. The current runlevel can be set directly at
the required state with C<0> or C<o> for stopping the service (a 'K' symlink),
C<1> or C<x> to start the service (a 'S' symlink) and C<2> or C<~> to leave the
service floating (no symlink).

=head2 Priority layout

The priority layout uses the default movement keys. In order to edit the fields
you can use CTRL-d to delete the character in front of the cursor or backspace
to backspace. Use CTRL-b or CTRL-f to move the cursor backwards or forwards 
within the field. Note that only S, K, or any digit is allowed to be entered
into the field.

=head2 Starting / Stopping Services

To start a service now, press the C<+> key.
To stop  a service now, press the C<-> key.

This will call C</etc/init.d/service start> or C</etc/init.d/service stop>.

=head1 USING THE CLI

If the desired modifications to the symlinks are known and only one quick
change is needed, then you can use a CLI interface to B<sysv-rc-conf>.
Examples:

  # sysv-rc-conf --level 35 ssh off
  # sysv-rc-conf atd on
  # sysv-rc-conf acct reset
  # sysv-rc-conf cron || echo cron is not started

The first example will turn ssh off on levels 3 and 5. The second example
turns atd on for runlevels 2, 3, 4, and 5. The third example sets the on/off
state for each runlevel based on the defaults specified in the LSB header.

If invoked with only a service name, the return code is set to 0 if the
service is enabled in the current (or defined-by C<--level>) runlevel. If
not invoked as B<chkconfig> then the information displayed by C<--info> is
also printed.

=head1 FILES

=over

=item F</etc/sysv-rc-conf.conf>

Configuration file for B<sysv-rc-conf> that populates some configuration
options before command line options are processed. Options are expressed
one per line, with comments introduced by C<#>. The following options
are supported; other options may also be effective but are not guaranteed
and may have different names from the corresponding command line options
and may not be meaningful expressed in the config file.

=over

=item B<insserv>

Call the B<insserv(8)> tool to recompute dependency-based init ordering
on exit.

=item B<order> I<order>

Set display order as per B<--order>.

=item B<verbose> I<file>

As per B<--verbose>.

=item B<read_only>

Always operate in read-only mode as per B<--RO>.

=item B<pictograms> or B<no-pictograms>

Whether to use unicode symbols.

=item B<interpret> or B<no-interpret>

Whether to show symlink status analysis on right hand side.

=item B<no-warninit>

Disable warnings about alternative init systems.

=item B<colour> or B<no-colour>

Whether or not to use colour in the user interface.

=item B<classic>

Whether to turn off all fancy additions to the UI.

=back

=item F</usr/share/sysv-rc-conf/config>

If present, processed before F</etc/sysv-rc-conf.conf>. This is a good place
for downstream packagers to put distribution-specific defaults that may differ
from built-in defaults.

=item F<.config/sysv-rc-conf.conf> in user's home directory

If present, processed after F</etc/sysv-rc-conf.conf>.

=item F</var/lib/sysv-rc-conf/services>

B<sysv-rc-conf> stores a cache of all the symlink information from 
C</etc/rc{runlevel}.d/> in C</var/lib/sysv-rc-conf/services> (See the --cache
option to change the location of this file). It uses this cache to make an
intelligent decision on what priority number to give the K or S link when they
are changed in the simple layout. This cache is updated/created every time the
program is launched. The program needs to run with root privileges in
order to update the cache. The format of the file is as follows:

  RUNLEVEL S|K PRIORITY SERVICE

Here's a few examples:

  2 K 74 ntpd
  2 K 50 xinetd
  3 S 08 iptables
  3 S 80 sendmail

B<sysv-rc-conf> will first see if it can get an exact match from the cache.
For example, if the symlink for C<cron> in runlevel 3 is S89cron and you 
uncheck it, B<sysv-rc-conf> will first see if there is an entry in the cache
that looks like C<3 K nn cron>, if so it will use nn for the priority number.

If there wasn't a match, B<sysv-rc-conf> will then see if there is another S or
K (whichever you're switching to, so in this example, K) entry on a different
runlevel - so an entry like C<i K nn cron>, where i is any runlevel. If found,
the link will use nn. 

If there still wasn't a match, B<sysv-rc-conf> will look for the opposite of S
or K in any run level, and use 100 - that priority. So in our example,
C<i S nn cron>. If nn is 20, then it will use 80 (100 - 20), since that is
typically the way that the priority numbers are used.

If there still isn't a match, the default priority of 20 for S links is used,
and the default priority of 80 for K links is used.

=back

=head1 COMPATIBILITY

B<sysv-rc-conf> should work on any Unix like system that manages services when
changing runlevels by using symlinks in C</etc/rc?.d/>. Refer to your system
documentation to see if that's the case (usually there's a
C</etc/init.d/README>).

=head1 CAVEATS

B<sysv-rc-conf> does not understand dependency-based booting. It is
recommend to run B<insserv(8)> after adjusting service symlinks with
B<sysv-rc-conf>. This can be done automatically using the B<--insserv> option,
which may be included in the system config file.

B<sysv-rc-conf> only manages the symlinks in the C<rc?.d>
directories. It is possible that packages may have other ways of being
disabled or enabled. 

Because Curses takes over the screen sometimes error messages won't be
seen on the terminal. If you run across any weird problems try redirecting
STDERR to a file when you execute the program. 

For example:
  # sysv-rc-conf 2E<gt> err.out

=head1 REPORTING BUGS

Report bugs at https://gitlab.com/init-tools/sysv-rc-conf/-/issues

=head1 SEE ALSO

B<insserv(8)>, B<init(8)>, B<runlevel(8)>, B<chkconfig(8)>, C</etc/init.d/README>

  www: https://gitlab.com/init-tools/sysv-rc-conf

=head1 AUTHOR

Joe Oppegaard E<lt>joe@pidone.orgE<gt>,
Andrew Bower E<lt>andrew@bower.ukE<gt>
