#!/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 $

    # 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}" "$1"

    # 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' "${#1}" "${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}" "${2#${2%%[![:space:]]*}}"

    # 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
    # it is intended for allowing the user to overwrite the value on invocation.
    # shellcheck disable=SC2039
    host=${HOSTNAME:-${hostname:-$(hostname)}}

    log "[3${PF_COL3:-1}m${user}${c7}@[3${PF_COL3:-1}m${host}"
}

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"
        return
    }

    case $os in
        Linux*)
            # TODO: Some distributions don't support '/etc/os-release'.
            #       Bedrock support is something I'd like to add for example.

            # 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

            # CRUX does its own thing entirely, go CRUX!
            command -v crux >/dev/null && distro=$(crux)
        ;;

        Darwin*)
            # TODO: Parse '/System/Library/CoreServices/SystemVersion.plist'
            #       to grab the full distribution name, version and build.
            distro=macOS
        ;;

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

get_kernel() {
    # '$kernel' is the cached output of 'uname -r'.
    case $os in
        # Don't print kernel output on BSD systems as the
        # OS name includes it.
        *BSD*) ;;

        *)
            log kernel "$kernel"
        ;;
   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*)
            host=$(sysctl -n hw.model)
        ;;

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

    log host "$host"
}

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))
        ;;
    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}"
}

get_pkgs() {
    # Simple function to avoid '>/dev/null' spam.
    # This checks to see if a command is in '$PATH'.
    has() { command -v "$1" >/dev/null; }

    # This works by first checking for which package managers are
    # isntalled 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.
    packages=$(
        case $os in
            Linux*)
                # Commands which print packages one per line.
                has kiss       && kiss l
                has bonsai     && bonsai list
                has pacman-key && pacman -Qq
                has dpkg       && dpkg-query -f '.\n' -W
                has rpm        && rpm -qa
                has xbps-query && xbps-query -l
                has apk        && apk info

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

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

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

            FreeBSD*)
                # Commands which print packages one per line.
                has pkg        && pkg info
            ;;

            *BSD*)
                # Commands which print packages one per line.
                has pkginfo    && pkginfo -i
                has pkg        && pkg list
            ;;
        esac | wc -l
    )

    log pkgs "$packages"
}

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))
        ;;

        Darwin*)
            # If you run macOS and can send me the full output of
            # 'vm_stat' I'll be able to add full support here.
            mem_full=$(($(sysctl -n hw.memsize) / 1024 / 1024))
        ;;

        OpenBSD*)
            # If you run OpenBSD and can send me the full output of
            # 'vm_stat' I'll be able to add full support here.
            mem_full=$(($(sysctl -n hw.physmem) / 1024 / 1024))
        ;;

        FreeBSD*)
            # If you run FreeBSD and can help me get
            # the used memory amount, I'll be able to add support here.
            mem_full=$(($(sysctl -n hw.physmem) / 1024 / 1024))
        ;;
    esac

    log memory "${mem_used:-?}MiB / ${mem_full:-?}MiB"
}

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.
    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:-4}}
        PF_COL3=${PF_COL3:-$((${1:-4}%6+1))}

        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 6 <<-EOF
				${c6}      /\\
				     /^^\\
				    /\\   \\
				   /${c7}  __  \\
				  /  (  )  \\
				 / __|  |__\\\\
				///        \\\\\\
			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
        ;;

        [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
        ;;

        [Ll]inux*[Mm]int*|[Mm]int)
            read_ascii 2 <<-EOF
				${c2} ___________
				${c2}|_          \\
				${c2}  | ${c7}| _____ ${c2}|
				${c2}  | ${c7}| | | | ${c2}|
				${c2}  | ${c7}| | | | ${c2}|
				${c2}  | ${c7}\\__${c7}___/ ${c2}|
				${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]anjaro*)
            read_ascii 2 <<-EOF
				${c2}||||||||| ||||
				||||||||| ||||
				||||      ||||
				|||| |||| ||||
				|||| |||| ||||
				|||| |||| ||||
				|||| |||| ||||
			EOF
        ;;

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

        [Ss]lackware*)
            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
        ;;

        [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"
            printf 'error: Open an issue on GitHub for support to be added.\n'
            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.
    #
    # The "   " acts as the padding between the ascii art and the text as
    # it appends 3 spaces to the end of each line.
    while read -r line; do
        ascii_height=$((ascii_height + 1))
        ascii_width=$((${#line} > ascii_width ? ${#line} : ascii_width))
    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"
}

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' EXIT INT

    # Hide 'stderr' unless the first argument is '-v'. This saves
    # polluting the script with '2>/dev/null'.
    [ "$1" = -v ] || exec 2>/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 <<-EOF
		$(uname -sr)
	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 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. Using cursor down doesn't scroll
    # the screen correctly if this causes the cursor to hit the bottom of the
    # window. Using '0' gives us an extra iteration, adding a bottom line gap.
    printf '%0.s\n' $(seq 0 "$cursor_pos")
}

main "$@"
