#!/bin/sh
# shellcheck source=/dev/null
#
# pfetch - Simple POSIX sh fetch script.

die() {
    printf '\033[31;1merror\033[m: %s.\n' "$@" >&2
    exit 1
}

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.
    #
    # 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
    # information and it allows for easy swapping of information order and
    # amount.
    #
    # $[2] ___      $[3] goldie@KISS
    # $[4](.· |     $[5] os KISS Linux
    #     (<> |
    #    / __  \
    #   ( /  \ /|
    #  _/\ __)/_)
    #  \/-____\/
    # $[1]
    #
    # $[6] /home/goldie $
    #
    # PF_COL1: Control color of info name.
    # PF_SEP:  Control the separator between info name and info data.
    # PF_COL2: Control color of info data.
    #
    # '\033[14C': Move cursor 14 characters to the right.
    #             TODO: Base this on ASCII art width.
    #
    # '\033[3%s': Color formatting.
    # '\033[m':   Reset formatting.
    #
    # '\033[%sD': Move cursor '${#1}' characters to the left.
    #             This allows for aligned info names and data.
    #
    # '\033[6C':  Move cursor 6 characters to the right.
    #             This aligns the info.
    printf '\033[14C\033[3%s;1m%s\033[m%s\033[3%sm\033[%sD\033[6C%s\033[m\n' \
           "${PF_COL1:-5}" "$1" "${PF_SEP:- }" "${PF_COL2:-7}" "${#1}" "${2:-?}"

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

get_os() {
    case $kernel_name in
        Linux|GNU*) os=linux ;;

        *)
            die "Unknown OS detected '$kernel_name'" \
                "Open an issue on GitHub to add support for your OS"
        ;;
    esac
}

get_title() {
    case $os in
        linux)
            read -r hostname < /proc/sys/kernel/hostname
        ;;
    esac

    # Username is retrieved by first checking '$USER' with a fallback
    # to the 'whoami' command.
    #
    # Hostname is retrieved by first checking '$HOSTNAME' with a fallback
    # to the OS specific detection above and finally an additional fallback
    # to the 'hostname' command.
    #
    # PF_SEP and PF_COL{1,2} change printing options in the 'log()' function.
    #
    # 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
    PF_SEP=@ PF_COL1=1 PF_COL2='3;1' \
        log "${USER:-$(whoami)}" "${HOSTNAME:-${hostname:-$(hostname)}}"
}

get_distro() {
    case $os in
        linux)
            . /etc/os-release && distro=$PRETTY_NAME
        ;;
    esac

    log os "$distro"
}

get_kernel() {
    log kernel "$kernel_version"
}

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
        ;;
    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_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_total=$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_total=$((mem_total / 1024))
        ;;
    esac

    log memory "${mem_used}MiB / ${mem_total}MiB"
}

get_ascii() {
    case $os in
        linux)
            ascii="\
${c4}    ___
   (${c7}.· ${c4}|
   (${c5}<> ${c4}|
  / ${c7}__  ${c4}\\
 ( ${c7}/  \\ ${c4}/|
${c5}_${c4}/\\ ${c7}__)${c4}/${c5}_${c4})
${c5}\/${c4}-____${c5}\/
"
        ;;
    esac

    # Store the height of the ascii art for cursor 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.
    ascii_height=$(printf %s "$ascii" | wc -l)

    # Print the ascii art and position the cursor back where we
    # started prior to printing it.
    # '\e[?25l': Hide the cursor.
    # '\033[1m': Print the ascii in bold.
    # '\033[m':  Clear bold.
    # '\033[%sA: Move the cursor up '$ascii_height' amount of lines.
    printf '\e[?25l\033[1m%s\033[m\033[%sA' "$ascii" "$ascii_height"
}

main() {
    # 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 kernel_name kernel_version <<EOF
        $(uname -sr)
EOF

    get_os
    get_ascii
    get_title
    get_distro
    get_kernel
    get_uptime
    get_memory

    # Position the cursor below both the ascii art and information lines.
    # 'log' contains the amount of 'get_' info lines that were printed.
    # '\033[%sB': Move the cursor down N lines.
    # '\e[?25h' : Un-hide the cursor.
    printf '\033[%sB\n\e[?25h' "$((ascii_height - log))"
}

main "$@"
