#!/bin/sh
#
# pfetch - Simple POSIX sh fetch script.

log() {
    # The 'log()' function handles the printing of information.
    # In 'pfetch' (and 'neofetch'!) the printing of the ascii art and info
    # happen independently of each other.
    #
    # The size of the ascii art is stored and the ascii is printed first.
    # Once the ascii is printed, the cursor is located right below the art
    # (See marker $[1]).
    #
    # Using the stored ascii size, the cursor is then moved to marker $[2].
    # This is simply a cursor up escape sequence using the "height" of the
    # ascii art.
    #
    # 'log()' then moves the cursor to the right the "width" of the ascii art
    # with an additional amount of padding to add a gap between the art and
    # the information (See marker $[3]).
    #
    # When 'log()' has executed, the cursor is then located at marker $[4].
    # When 'log()' is run a second time, the next line of information is
    # printed, moving the cursor to marker $[5].
    #
    # Markers $[4] and $[5] repeat all the way down through the ascii art
    # until there is no more information left to print.
    #
    # Every time 'log()' is called the script keeps track of how many lines
    # were printed. When printing is complete the cursor is then manually
    # placed below the information and the art according to the "heights"
    # of both.
    #
    # The math is simple: move cursor down $((ascii_height - info_height)).
    # If the aim is to move the cursor from marker $[5] to marker $[6],
    # plus the ascii height is 8 while the info height is 2 it'd be a move
    # of 6 lines downwards.
    #
    # However, if the information printed is "taller" (takes up more lines)
    # than the ascii art, the cursor isn't moved at all!
    #
    # Once the cursor is at marker $[6], the script exits. This is the gist
    # of how this "dynamic" printing and layout works.
    #
    # This method allows ascii art to be stored without markers for info
    # and it allows for easy swapping of info order and amount.
    #
    # $[2] ___      $[3] goldie@KISS
    # $[4](.· |     $[5] os KISS Linux
    #     (<> |
    #    / __  \
    #   ( /  \ /|
    #  _/\ __)/_)
    #  \/-____\/
    # $[1]
    #
    # $[6] /home/goldie $

    # End here if no data was found.
    [ "$2" ] || return

    # Store the value of '$1' as we reset the argument list below.
    name=$1

    # Use 'set --' as a means of stripping all leading and trailing
    # white-space from the info string. This also normalizes all
    # whitespace inside of the string.
    #
    # Disable the shellcheck warning for word-splitting
    # as it's safe and intended ('set -f' disables globbing).
    # shellcheck disable=2046,2086
    {
        set -f
        set +f -- $2
        info=$*
    }

    # Move the cursor to the right, the width of the ascii art with an
    # additional gap for text spacing.
    printf '[%sC' "${ascii_width--1}"

    # Print the info name and color the text.
    printf '[3%s;1m%s[m' "${PF_COL1-4}" "$name"

    # Print the info name and info data separator.
    printf '%s' "$PF_SEP"

    # Move the cursor backward the length of the *current* info name and
    # then move it forwards the length of the *longest* info name. This
    # aligns each info data line.
    printf '[%sD[%sC' "${#name}" "${PF_ALIGN-$info_length}"

    # Print the info data, color it and strip all leading whitespace
    # from the string.
    printf '[3%sm%s[m\n' "${PF_COL2-7}" "$info"

    # Keep track of the number of times 'log()' has been run.
    : $((info_height+=1))
}

get_title() {
    # Username is retrieved by first checking '$USER' with a fallback
    # to the 'whoami' command.
    user=${USER:-$(whoami)}

    # Hostname is retrieved by first checking '$HOSTNAME' with a fallback
    # to the 'hostname' command.
    #
    # Disable the warning about '$HOSTNAME' being undefined in POSIX sh as
    # the intention for using it is allowing the user to overwrite the
    # value on invocation.
    # shellcheck disable=SC2039
    hostname=${HOSTNAME:-${hostname:-$(hostname)}}

    log "[3${PF_COL3:-1}m${user}${c7}@[3${PF_COL3:-1}m${hostname}" " " >&6
}

get_os() {
    # This function is called twice, once to detect the distribution name
    # for the purposes of picking an ascii art early and secondly to display
    # the distribution name in the info output (if enabled).
    #
    # On first run, this function displays _nothing_, only on the second
    # invocation is 'log()' called.
    [ "$distro" ] && {
        log os "$distro" >&6
        return
    }

    case $os in
        Linux*)
            # Some Linux disttributions (which are based on others)
            # fail to identify as they **do not** change the upstream
            # distributions identification packages or files.
            #
            # It is senseless to add a special case in the code for
            # each and every distribution (which _is_ technically no
            # different from what it is based on) as they're either too
            # lazy to modify upstream's identification files or they
            # don't have the know-how (or means) to ship their own
            # lsb-release package.
            #
            # This causes users to think there's a bug in system detection
            # tools like neofetch or pfetch when they technically *do*
            # function correctly.
            #
            # Exceptions are made for distributions which are independent,
            # not based on another distribution or follow different
            # standards.
            #
            # This applies only to distributions which follow the standard
            # by shipping unmodified identification files and packages
            # from their respective upstreams.
            if command -v lsb_release; then
                distro=$(lsb_release -sd)

            else
                # Disable warning about shellcheck not being able
                # to read '/etc/os-release'. This is fine.
                # shellcheck source=/dev/null
                . /etc/os-release && distro=$PRETTY_NAME
            fi

            # Special cases for (independent) distributions which
            # don't follow any os-release/lsb standards whatsoever.
            command -v crux        && distro=$(crux)
            command -v guix        && distro='Guix System'
        ;;

        Darwin*)
            # Parse the SystemVersion.plist file to grab the macOS
            # version. The file is in the following format:
            #
            # <key>ProductVersion</key>
            # <string>10.14.6</string>
            #
            # 'IFS' is set to '<>' to enable splitting between the
            # keys and a second 'read' is used to operate on the
            # next line directly after a match.
            #
            # '_' is used to nullify a field. '_ _ line _' basically
            # says "populate $line with the third field's contents".
            while IFS='<>' read -r _ _ line _; do
                case $line in
                    # Match 'ProductVersion' and read the next line
                    # directly as it contains the key's value.
                    ProductVersion)
                        IFS='<>' read -r _ _ mac_version _
                        break
                    ;;
                esac
            done < /System/Library/CoreServices/SystemVersion.plist

            # Use the ProductVersion to determine which macOS/OS X codename
            # the system has. As far as I'm aware there's no "dynamic" way
            # of grabbing this information.
            case $mac_version in
                10.4*)  distro='Mac OS X Tiger' ;;
                10.5*)  distro='Mac OS X Leopard' ;;
                10.6*)  distro='Mac OS X Snow Leopard' ;;
                10.7*)  distro='Mac OS X Lion' ;;
                10.8*)  distro='OS X Mountain Lion' ;;
                10.9*)  distro='OS X Mavericks' ;;
                10.10*) distro='OS X Yosemite' ;;
                10.11*) distro='OS X El Capitan' ;;
                10.12*) distro='macOS Sierra' ;;
                10.13*) distro='macOS High Sierra' ;;
                10.14*) distro='macOS Mojave' ;;
                10.15*) distro='macOS Catalina' ;;
                *)      distro='macOS' ;;
            esac

            distro="$distro $mac_version"
        ;;

        Haiku)
            # Haiku uses 'uname -v' for version information
            # instead of 'uname -r'.
            distro="Haiku $(uname -v)"
        ;;

        *)
            # Catch all to ensure '$distro' is never blank.
            # This also handles the BSDs.
            distro="$os $kernel"
        ;;
    esac
}

get_kernel() {
    case $os in
        # Don't print kernel output on some systems as the
        # OS name includes it.
        *BSD*|Haiku) ;;

        *)
            # '$kernel' is the cached output of 'uname -r'.
            log kernel "$kernel" >&6
        ;;
   esac
}

get_host() {
    case $os in
        Linux*)
            # Despite what these files are called, version doesn't
            # always contain the version nor does name always contain
            # the name.
            read -r name    < /sys/devices/virtual/dmi/id/product_name
            read -r version < /sys/devices/virtual/dmi/id/product_version
            read -r model   < /sys/firmware/devicetree/base/model

            host="$name $version $model"
        ;;

        Darwin*|FreeBSD*)
            host=$(sysctl -n hw.model)
        ;;

        NetBSD*)
            host=$(sysctl -n machdep.dmi.system-vendor \
                             machdep.dmi.system-product)
        ;;

        *BSD*)
            host=$(sysctl -n hw.vendor hw.product)
        ;;
    esac

    # Turn the host string into an argument list so we can iterate
    # over it and remove OEM strings and other information which
    # shouldn't be displayed.
    #
    # Disable the shellcheck warning for word-splitting
    # as it's safe and intended ('set -f' disables globbing).
    # shellcheck disable=2046,2086
    {
        set -f
        set +f -- $host
        host=
    }

    # Iterate over the host string word by word as a means of stripping
    # unwanted and OEM information from the string as a whole.
    #
    # This could have been implemented using a long 'sed' command with
    # a list of word replacements, however I want to show that something
    # like this is possible in pure sh.
    #
    # This string reconstruction is needed as some OEMs either leave the
    # identification information as "To be filled by OEM", "Default",
    # "undefined" etc and we shouldn't print this to the screen.
    for word; do
        # This works by reconstructing the string by excluding words
        # found in the "blacklist" below. Only non-matches are appended
        # to the final host string.
        case $word in
            To      | [Bb]e      | [Ff]illed | by     | O.E.M.  | OEM  |\
            Not     | Applicable | Specified | System | Product | Name |\
            Version | Undefined  | Default   | string | INVALID | �    )
                continue
            ;;
        esac

        host="$host$word "
    done

    # '$arch' is the cached output from 'uname -m'.
    log host "${host:-$arch}" >&6
}

get_uptime() {
    # Uptime works by retrieving the data in total seconds and then
    # converting that data into days, hours and minutes using simple
    # math.
    case $os in
        Linux*)
            IFS=. read -r s _ < /proc/uptime
        ;;

        Darwin*|*BSD*)
            s=$(sysctl -n kern.boottime)

            # Extract the uptime in seconds from the following output:
            # [...] { sec = 1271934886, usec = 667779 } Thu Apr 22 12:14:46 2010
            s=${s#*=}
            s=${s%,*}

            # The uptime format from 'sysctl' needs to be subtracted from
            # the current time in seconds.
            s=$(($(date +%s) - s))
        ;;

        Haiku)
            # The boot time is returned in microseconds, convert it to
            # regular seconds.
            s=$(($(system_time) / 1000000))
        ;;
    esac

    # Convert the uptime from seconds into days, hours and minutes.
    d=$((s / 60 / 60 / 24))
    h=$((s / 60 / 60 % 24))
    m=$((s / 60 % 60))

    # Only append days, hours and minutes if they're non-zero.
    [ "$d" = 0 ] || uptime="${uptime}${d}d "
    [ "$h" = 0 ] || uptime="${uptime}${h}h "
    [ "$m" = 0 ] || uptime="${uptime}${m}m "

    log uptime "${uptime:-0m}" >&6
}

get_pkgs() {
    # This works by first checking for which package managers are
    # installed and finally by printing each package manager's
    # package list with each package one per line.
    #
    # The output from this is then piped to 'wc -l' to count each
    # line, giving us the total package count of whatever package
    # managers are installed.
    #
    # Backticks are *required* here as '/bin/sh' on macOS is
    # 'bash 3.2' and it can't handle the following:
    #
    # var=$(
    #    code here
    # )
    #
    # shellcheck disable=2006
    packages=`
        case $os in
            Linux*)
                # Commands which print packages one per line.
                command -v kiss       && kiss l
                command -v bonsai     && bonsai list
                command -v pacman-key && pacman -Qq
                command -v dpkg       && dpkg-query -f '.\n' -W
                command -v rpm        && rpm -qa
                command -v xbps-query && xbps-query -l
                command -v apk        && apk info

                # Directories containing packages.
                command -v brew       && printf '%s\n' "$(brew --cellar)/"*
                command -v emerge     && printf '%s\n' /var/db/pkg/*/*/
                command -v pkgtool    && printf '%s\n' /var/log/packages/*

                # GUIX requires two commands.
                command -v guix       && {
                    guix package -p /run/current-system/profile -I
                    guix package -I
                }

                # NIX requires two commands.
                command -v nix-store  && {
                    nix-store -q --requisites /run/current-system/sw
                    nix-store -q --requisites ~.nix-profile
                }
            ;;

            Darwin*)
                # Commands which print packages one per line.
                command -v pkgin      && pkgin list
                command -v port       && port installed

                # Directories containing packages.
                command -v brew       && printf '%s\n' /usr/local/Cellar/*
            ;;

            FreeBSD*)
                pkg info
            ;;

            OpenBSD*)
                printf '%s\n' /var/db/pkg/*/
            ;;

            NetBSD*)
                pkg_info
            ;;

            Haiku)
                printf '%s\n' /boot/system/package-links/*
            ;;
        esac | wc -l
    `

    log pkgs "${packages:-?}" >&6
}

get_memory() {
    case $os in
        # Used memory is calculated using the following "formula" (Linux):
        # MemUsed = MemTotal + Shmem - MemFree - Buffers - Cached - SReclaimable
        # Source: https://github.com/KittyKatt/screenFetch/issues/386
        Linux*)
            # Parse the '/proc/meminfo' file splitting on ':' and 'k'.
            # The format of the file is 'key:   000kB' and an additional
            # split is used on 'k' to filter out 'kB'.
            while IFS=:k read -r key val _; do
                case $key in
                    MemTotal)
                        mem_used=$((mem_used + val))
                        mem_full=$val
                    ;;

                    Shmem)
                        mem_used=$((mem_used + val))
                    ;;

                    MemFree|Buffers|Cached|SReclaimable)
                        mem_used=$((mem_used - val))
                    ;;
                esac
            done < /proc/meminfo

            mem_used=$((mem_used / 1024))
            mem_full=$((mem_full / 1024))
        ;;

        # Used memory is calculated using the following "formula" (MacOS):
        # (wired + active + occupied) * 4 / 1024
        Darwin*)
            mem_full=$(($(sysctl -n hw.memsize) / 1024 / 1024))

            # Parse the 'vmstat' file splitting on ':' and '.'.
            # The format of the file is 'key:   000.' and an additional
            # split is used on '.' to filter it out.
            while IFS=:. read -r key val; do
                case $key in
                    *wired*|*active*|*occupied*)
                        mem_used=$((mem_used + ${val:-0}))
                    ;;
                esac

            # Using '<<-EOF' is the only way to loop over a command's
            # output without the use of a pipe ('|').
            # This ensures that any variables defined in the while loop
            # are still accessible in the script.
            done <<-EOF
                $(vm_stat)
			EOF

            mem_used=$((mem_used * 4 / 1024))
        ;;

        OpenBSD*)
            mem_full=$(($(sysctl -n hw.physmem) / 1024 / 1024))

            # This is a really simpler parser for 'vmstat' which grabs
            # the used memory amount in a lazy way. 'vmstat' prints 3
            # lines of output with the needed value being stored in the
            # final line.
            #
            # This loop simply grabs the 3rd element of each line until
            # the EOF is reached. Each line overwrites the value of the
            # previous one so we're left with what we wanted. This isn't
            # slow as only 3 lines are parsed.
            while read -r _ _ line _; do
                mem_used=${line%%M}

            # Using '<<-EOF' is the only way to loop over a command's
            # output without the use of a pipe ('|').
            # This ensures that any variables defined in the while loop
            # are still accessible in the script.
            done <<-EOF
                $(vmstat)
			EOF
        ;;

        # Used memory is calculated using the following "formula" (FreeBSD):
        # (inactive_count + free_count + cache_count) * page_size / 1024
        FreeBSD*)
            mem_full=$(($(sysctl -n hw.physmem) / 1024 / 1024))

            # Use 'set --' to store the output of the command in the
            # argument list. POSIX sh has no arrays but this is close enough.
            #
            # Disable the shellcheck warning for word-splitting
            # as it's safe and intended ('set -f' disables globbing).
            # shellcheck disable=2046
            {
                set -f
                set +f -- $(sysctl -n hw.pagesize \
                                      vm.stats.vm.v_inactive_count \
                                      vm.stats.vm.v_free_count \
                                      vm.stats.vm.v_cache_count)
            }

            # Calculate the amount of used memory.
            # $1: hw.pagesize
            # $2: vm.stats.vm.v_inactive_count
            # $3: vm.stats.vm.v_free_count
            # $4: vm.stats.vm.v_cache_count
            mem_used=$((($2 + $3 + $4) * $1 / 1024 / 1024))
        ;;

        NetBSD*)
            mem_full=$(($(sysctl -n hw.physmem64) / 1024 / 1024))

            # NetBSD implements a lot of the Linux '/proc' filesystem,
            # this uses the same parser as the Linux memory detection.
            while IFS=:k read -r key val _; do
                case $key in
                    MemFree)
                        mem_free=$((val / 1024))
                        break
                    ;;
                esac
            done < /proc/meminfo

            mem_used=$((mem_full - mem_free))
        ;;

        Haiku)
            # Read the first line of 'sysinfo -mem' splitting on
            # '(', ' ', and ')'. The needed information is then
            # stored in the 5th and 7th elements. Using '_' "consumes"
            # an element allowing us to proceed to the next one.
            #
            # The parsed format is as follows:
            # 3501142016 bytes free      (used/max  792645632 / 4293787648)
            IFS='( )' read -r _ _ _ _ mem_used _ mem_full <<-EOF
                $(sysinfo -mem)
			EOF

            mem_used=$((mem_used / 1024 / 1024))
            mem_full=$((mem_full / 1024 / 1024))
        ;;
    esac

    log memory "${mem_used:-?}M / ${mem_full:-?}M" >&6
}

get_term() {
    # Workaround for macOS systems that don't support the
    # "algorithm" of obtaining the terminal program name.
    #
    # This also doubles as a means of allowing the user to
    # set whatever value they like here through the
    # '$TERM_PROGRAM' environment variable.
    case $TERM_PROGRAM in
        iTerm.app)    term=iTerm2 ;;
        Terminal.app) term='Apple Terminal' ;;
        Hyper)        term=HyperTerm ;;
        *)            term=${TERM_PROGRAM%%.app} ;;
    esac

    # Special case for TosWin2 (FreeMiNT) which doesn't
    # support the "algorithm" of obtaining the terminal
    # program name.
    [ "$TERM" = tw52 ] || [ "$TERM" = tw100 ] &&
        term=TosWin2

    # Special case for when 'pfetch' is run over SSH.
    [ "$SSH_CONNECTION" ] &&
        term=$SSH_TTY

    # This surprisingly reliable method of detecting the current
    # terminal emulator is kinda neat.
    #
    # It works by looping through each parent of each process
    # starting with '$PPID' (the parent process ID) until we
    # find a match or hit PID 1 (init).
    #
    # On each iteration the name of the current parent process
    # is checked against a list of good values and bad values.
    # If no match is found we check the parent of the parent
    # and so on.
    #
    # Using this method *no* terminal emulator names are
    # hardcoded and the list remains small and general. In short
    # it's basically a list of what *isn't* a terminal emulator
    # and a list of places we should *stop*.
    while [ -z "$term" ]; do
        # This block is OS-specific and handles the fetching of
        # the parent process (of the parent) and the fetching of
        # said process' name.
        case $os in
            Linux*)
                # On Linux some implementation of 'ps' aren't POSIX
                # compliant, thankfully Linux provides this information
                # though the '/proc' filesystem.
                #
                # This loops line by line over the '/proc/PID/status'
                # file splitting at ':' and '<TAB>', we then look for
                # the key containing 'PPid' and grab the value.
                while IFS=':	' read -r key val; do
                    case $key in
                        PPid)
                            ppid=$val
                            break
                        ;;
                    esac
                done < "/proc/${ppid:-$PPID}/status"

                # Get the name of the parent process.
                read -r name < "/proc/$ppid/comm"
            ;;

            Windows*)
                # I need some assistance to add Windows support
                # as the 'ps' command used in MINGW, MSYS and CYGWIN
                # isn't POSIX compliant(?).
                return
            ;;

            *)
                # POSIX compliant 'ps' makes this really easy,
                # just two simple commands to grab the parent
                # process ID and the ID's name.
                ppid=$(ps -p "${ppid:-$PPID}" -o ppid=)
                name=$(ps -p "$ppid"          -o comm=)
            ;;
        esac

        # Check the parent process name against a list of good and bad
        # values. On a bad value we either keep iterating up the parent
        # process list or we stop altogether (PID 1 for example).
        case $name in
            # If the parent process name matches the user's shell (or
            # anything that looks like a shell), do another iteration.
            #
            # This also includes 'screen' and anything that looks like
            # 'su' or 'sudo'.
            ${SHELL##*/} | *sh | screen | su* ) ;;

            # If the parent process name matches 'login', 'init' or
            # '*Login*' we're most likely in the TTY and not a graphical
            # session. In this case 'term' is set to the current TTY and
            # we end here.
            login* | *Login* | init)
                term=$(tty)
            ;;

            # If the parent process name matches anything in this list
            # we can no longer continue. We've either hit PID 1 or a parent
            # which *won't* lead to the terminal emulator's PID.
            ruby      | systemd  | python*  | 1  | sshd* | tmux* |\
            USER*PID* | kdeinit* | launchd* | '' )
                break
            ;;

            # If none of the above have matched we've reached the terminal
            # emulator's PID and we can end here.
            *)
                term=${name##*/}
            ;;
        esac
    done

    [ "$term" ] && log term "$term" >&6
}

get_shell() {
    log shell "${SHELL##*/}" >&6
}

get_ascii() {
    # This is a simple function to read the contents of
    # an ascii file from 'stdin'. It allows for the use
    # of '<<-EOF' to prevent the break in indentation in
    # this source code.
    #
    # This function also sets the text colors according
    # to the ascii color.
    read_ascii() {
        # 'PF_COL1': Set the info name color according to ascii color.
        # 'PF_COL3': Set the title color to some other color. ¯\_(ツ)_/¯
        PF_COL1=${PF_COL1:-${1:-7}}
        PF_COL3=${PF_COL3:-$((${1:-7}%8+1))}

        # POSIX sh has no 'var+=' so 'var=${var}append' is used. What's
        # interesting is that 'var+=' _is_ supported inside '$(())'
        # (arithmetic) though there's no support for 'var++/var--'.
        #
        # There is also no $'\n' to add a "literal"(?) newline to the
        # string. The simplest workaround being to break the line inside
        # the string (though this has the caveat of breaking indentation).
        while IFS= read -r line; do
            ascii="$ascii$line
"
        done
    }

    # This checks for ascii art in the following order:
    # '$1':        Argument given to 'get_ascii()' directly.
    # '$PF_ASCII': Environment variable set by user.
    # '$distro':   The detected distribution name.
    # '$os':       The name of the operating system/kernel.
    #
    # NOTE: Each ascii art below is indented using tabs, this
    #       allows indentation to continue naturally despite
    #       the use of '<<-EOF'.
    case ${1:-${PF_ASCII:-${distro:-$os}}} in
        [Aa]lpine*)
            read_ascii 4 <<-EOF
				${c4}   /\\ /\\
				  /${c7}/ ${c4}\\  \\
				 /${c7}/   ${c4}\\  \\
				/${c7}//    ${c4}\\  \\
				${c7}//      ${c4}\\  \\
				         \\
			EOF
        ;;

        [Aa]rch*)
            read_ascii 4 <<-EOF
				${c6}      /\\
				     /^^\\
				    /\\   \\
				   /${c7}  __  \\
				  /  (  )  \\
				 / __|  |__\\\\
				///        \\\\\\
			EOF
        ;;

        [Aa]rco*)
            read_ascii 4 <<-EOF
				${c4}      /\\
				     /  \\
				    / /\\ \\
				   / /  \\ \\
				  / /    \\ \\
				 / / _____\\ \\
				/_/  \`----.\\_\\
			EOF
        ;;

        [Aa]rtix*)
            read_ascii 6 <<-EOF
				${c4}      /\\
				     /  \\
				    /\`'.,\\
				   /     ',
				  /      ,\`\\
				 /   ,.'\`.  \\
				/.,'\`     \`'.\\
			EOF
        ;;

        [Cc]ent[Oo][Ss]*)
            read_ascii 5 <<-EOF
				${c2} ____${c3}^${c5}____
				${c2} |\\  ${c3}|${c5}  /|
				${c2} | \\ ${c3}|${c5} / |
				${c5}<---- ${c4}---->
				${c4} | / ${c2}|${c3} \\ |
				${c4} |/__${c2}|${c3}__\\|
				${c2}     v
			EOF
        ;;

        [Dd]ebian*)
            read_ascii 1 <<-EOF
				${c1}  _____
				 /  __ \\
				|  /    |
				|  \\___-
				-_
				  --_
			EOF
        ;;

        [Ee]lementary*)
            read_ascii <<-EOF
				${c7}  _______
				 / ____  \\
				/  |  /  /\\
				|__\\ /  / |
				\\   /__/  /
				 \\_______/
			EOF
        ;;

        [Ff]edora*)
            read_ascii 4 <<-EOF
				${c7}      _____
				     /   __)${c4}\\${c7}
				     |  /  ${c4}\\ \\${c7}
				  ${c4}__${c7}_|  |_${c4}_/ /${c7}
				 ${c4}/ ${c7}(_    _)${c4}_/${c7}
				${c4}/ /${c7}  |  |
				${c4}\\ \\${c7}__/  |
				 ${c4}\\${c7}(_____/
			EOF
        ;;

        [Ff]ree[Bb][Ss][Dd]*)
            read_ascii 1 <<-EOF
				${c1} /\\ _____ /\\
				 \\_)     (_/
				 /         \\
				|           |
				|           |
				 \         /
				  --_____--
			EOF
        ;;

        [Gg]entoo*)
            read_ascii 5 <<-EOF
				${c5} _-----_
				(       \\
				\\    0   \\
				${c7} \\        )
				 /      _/
				(     _-
				\\____-
			EOF
        ;;

        [Gg]uix[Ss][Dd]*|guix*)
            read_ascii 3 <<-EOF
				${c3}|.__          __.|
				|__ \\        / __|
				   \\ \\      / /
				    \\ \\    / /
				     \\ \\  / /
				      \\ \\/ /
				       \\__/
			EOF
        ;;

        [Hh]aiku*)
            read_ascii 3 <<-EOF
				${c3}       ,^,
				      /   \\
				*--_ ;     ; _--*
				\\   '"     "'   /
				 '.           .'
				.-'"         "'-.
				 '-.__.   .__.-'
				       |_|
			EOF
        ;;

        [Hh]yperbola*)
            read_ascii <<-EOF
				${c7}    |\`__.\`/
				    \____/
				    .--.
				   /    \\
				  /  ___ \\
				 / .\`   \`.\\
				/.\`      \`.\\
			EOF
        ;;

        [Ll]inux*[Ll]ite*)
            read_ascii 3 <<-EOF
				${c3}   /\\
				  /  \\
				 / ${c7}/ ${c3}/
				> ${c7}/ ${c3}/
				\\ ${c7}\\ ${c3}\\
				 \\_${c7}\\${c3}_\\
				${c7}    \\
			EOF
        ;;

        [Ll]inux*[Mm]int*|[Mm]int)
            read_ascii 2 <<-EOF
				${c2} ___________
				|_          \\
				  | ${c7}| _____ ${c2}|
				  | ${c7}| | | | ${c2}|
				  | ${c7}| | | | ${c2}|
				  | ${c7}\\__${c7}___/ ${c2}|
				  \\_________/
			EOF
        ;;


        [Ll]inux*)
            read_ascii 4 <<-EOF
				${c4}    ___
				   (${c7}.· ${c4}|
				   (${c5}<> ${c4}|
				  / ${c7}__  ${c4}\\
				 ( ${c7}/  \\ ${c4}/|
				${c5}_${c4}/\\ ${c7}__)${c4}/${c5}_${c4})
				${c5}\/${c4}-____${c5}\/
			EOF
        ;;

        [Mm]ac[Oo][Ss]*|[Dd]arwin*)
            read_ascii 1 <<-EOF
				${c1}       .:'
				    _ :'_
				${c2} .'\`_\`-'_\`\`.
				:________.-'
				${c3}:_______:
				${c4} :_______\`-;
				${c5}  \`._.-._.'
			EOF
        ;;

        [Mm]ageia*)
            read_ascii 2 <<-EOF
				${c6}   *
				    *
				   **
				${c7} /\\__/\\
				/      \\
				\\      /
				 \\____/
			EOF
        ;;

        [Mm]anjaro*)
            read_ascii 2 <<-EOF
				${c2}||||||||| ||||
				||||||||| ||||
				||||      ||||
				|||| |||| ||||
				|||| |||| ||||
				|||| |||| ||||
				|||| |||| ||||
			EOF
        ;;

        [Mm][Xx]*)
            read_ascii <<-EOF
				${c7}    \\\\  /
				     \\\\/
				      \\\\
				   /\\/ \\\\
				  /  \\  /\\
				 /    \\/  \\
				/__________\\
			EOF
        ;;

        [Nn]et[Bb][Ss][Dd]*)
            read_ascii 3 <<-EOF
				${c7}\\\\${c3}\`-______,----__
				${c7} \\\\        ${c3}__,---\`_
				${c7}  \\\\       ${c3}\`.____
				${c7}   \\\\${c3}-______,----\`-
				${c7}    \\\\
				     \\\\
				      \\\\
			EOF
        ;;

        [Nn]ix[Oo][Ss]*)
            read_ascii 4 <<-EOF
				${c4}  \\\\  \\\\ //
				 ==\\\\__\\\\/ //
				   //   \\\\//
				==//     //==
				 //\\\\___//
				// /\\\\  \\\\==
				  // \\\\  \\\\
			EOF
        ;;

        [Oo]pen[Bb][Ss][Dd]*)
            read_ascii 3 <<-EOF
				${c3}      _____
				    \\-     -/
				 \\_/         \\
				 |        ${c7}O O${c3} |
				 |_  <   )  3 )
				 / \\         /
				    /-_____-\\
			EOF
        ;;

        openSUSE*|open*SUSE*|SUSE*|suse*)
            read_ascii 2 <<-EOF
				${c2}  _______
				__|   __ \\
				     / .\\ \\
				     \\__/ |
				   _______|
				   \\_______
				__________/
			EOF
        ;;

        [Pp]arabola*)
            read_ascii 5 <<-EOF
				${c5}  __ __ __  _
				.\`_//_//_/ / \`.
				          /  .\`
				         / .\`
				        /.\`
				       /\`
			EOF
        ;;

        [Pp]op!_[Oo][Ss]*)
            read_ascii 6 <<-EOF
				${c6}______
				\\   _ \\        __
				 \\ \\ \\ \\      / /
				  \\ \\_\\ \\    / /
				   \\  ___\\  /_/
				    \\ \\    _
				   __\\_\\__(_)_
				  (___________)
			EOF
        ;;

        [Pp]ure[Oo][Ss]*)
            read_ascii <<-EOF
				${c7} _____________
				|  _________  |
				| |         | |
				| |         | |
				| |_________| |
				|_____________|
			EOF
        ;;

        [Ss]lackware*)
            read_ascii 4 <<-EOF
				${c4}   ________
				  /  ______|
				  | |______
				  \\______  \\
				   ______| |
				| |________/
				|____________
			EOF
        ;;

        [Vv]oid*)
            read_ascii 2 <<-EOF
				${c2}    _______
				 _ \\______ -
				| \\  ___  \\ |
				| | /   \ | |
				| | \___/ | |
				| \\______ \\_|
				 -_______\\
			EOF
        ;;

        *)
            # On no match of a distribution ascii art, this function calls
            # itself again, this time to look for a more generic OS related
            # ascii art (KISS Linux -> Linux).
            [ "$1" ] || {
                get_ascii "$os"
                return
            }

            printf 'error: %s is not currently supported.\n' "$os" >&6
            printf 'error: Open an issue for support to be added.\n' >&6
            exit 1
        ;;
    esac

    # Store the "width" (longest line) and "height" (number of lines)
    # of the ascii art for positioning. This script prints to the screen
    # *almost* like a TUI does. It uses escape sequences to allow dynamic
    # printing of the information through user configuration.
    #
    # Iterate over each line of the ascii art to retrieve the above
    # information. The 'sed' is used to strip '[3Xm' color codes from
    # the ascii art so they don't affect the width variable.
    while read -r line; do
        : $((ascii_height+=1))
        ascii_width=$((${#line} > ascii_width ? ${#line} : ascii_width))

    # Using '<<-EOF' is the only way to loop over a command's
    # output without the use of a pipe ('|').
    # This ensures that any variables defined in the while loop
    # are still accessible in the script.
    done <<-EOF
		$(printf %s "$ascii" | sed 's/\[3.m//g')
	EOF

    # Add a gap between the ascii art and the information.
    : $((ascii_width+=4))

    # Print the ascii art and position the cursor back where we
    # started prior to printing it.
    # '[?7l':  Disable line-wrapping.
    # '[?25l': Hide the cursor.
    # '[1m':   Print the ascii in bold.
    # '[m':    Clear bold.
    # '[%sA':  Move the cursor up '$ascii_height' amount of lines.
    printf '[?7l[?25l[1m%s[m[%sA' "$ascii" "$ascii_height" >&6
}

main() {
    # Leave the terminal how we found it on exit or Ctrl+C.
    # '[?7h':  Enable line-wrapping.
    # '[?25h': Un-hide the cursor.
    trap 'printf [?7h[?25h >&6' EXIT

    # Hide 'stderr' unless the first argument is '-v'. This saves
    # polluting the script with '2>/dev/null'.
    [ "$1" = -v ] || exec 2>/dev/null

    # Hide 'stdout' and selectively print to it using '>&6'.
    # This gives full control over what it displayed on the screen.
    exec 6>&1 >/dev/null

    # Generic color list.
    # Disable warning about unused variables.
    # shellcheck disable=2034
    {
        c1='[31m'; c2='[32m'
        c3='[33m'; c4='[34m'
        c5='[35m'; c6='[36m'
        c7='[37m'; c8='[38m'
    }

    # Store the output of 'uname' to avoid calling it multiple times
    # throughout the script. 'read <<EOF' is the simplest way of reading
    # a command into a list of variables.
    read -r os kernel arch <<-EOF
		$(uname -srm)
	EOF

    # Always run 'get_os' for the purposes of detecting which ascii
    # art to display.
    get_os

    # Allow the user to specify the order and inclusion of information
    # functions through the 'PF_INFO' environment variable.
    # shellcheck disable=2086
    {
        # Disable globbing and set the positional parameters to the
        # contents of 'PF_INFO'.
        set -f
        set +f ${PF_INFO-ascii title os host kernel term uptime pkgs memory}

        # Iterate over the info functions to determine the lengths of the
        # "info names" for output alignment. The option names and subtitles
        # match 1:1 so this is thankfully simple.
        for info; do
            command -v "get_$info" >/dev/null &&
                info_length=$((${#info} > info_length ? ${#info} : info_length))
        done

        # Add an additional space of length to act as a gap.
        : $((info_length+=1))

        # Iterate over the above list and run any existing "get_" functions.
        for info; do "get_$info"; done
    }

    # Position the cursor below both the ascii art and information lines
    # according to the height of both. If the information exceeds the ascii
    # art in height, don't touch the cursor, else move it down N lines.
    cursor_pos=$((info_height > ascii_height ? 0 : ascii_height - info_height))

    # Print '$cursor_pos' amount of newlines to correctly position the
    # cursor. This used to be a 'printf $(seq X X)' however 'seq' is only
    # typically available (by default) on GNU based systems!
    while [ "${i:-0}" -le "$cursor_pos" ]; do
        printf '\n'
        : $((i+=1))
    done >&6
}

main "$@"
