#!/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' "${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\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 # the intention for using it is 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}" >&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*) command -v lsb_release && distro=$(lsb_release -sd) # 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 # Special cases for distributions which don't follow. # the '/etc/os-release' "standard". command -v crux && distro=$(crux) command -v guix && distro='Guix System' ;; 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" >&6 ;; esac } get_shell() { log shell "${SHELL##*/}" >&6 } 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" >&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)) ;; 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 # 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. 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/*/*/ # GUIX requires two commands. command -v guix && { guix package -p /run/current-system/profile -I guix package -I } ;; 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*) # Commands which print packages one per line. command -v pkg && pkg info ;; *BSD*) # Commands which print packages one per line. command -v pkginfo && pkginfo -i command -v pkg && pkg list command -v pkg_info && pkg_info ;; 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 ('|') or subshell. # This ensures that any variables defined in the while loop # are still accessible in the script. done <<-EOF $(vmstat) EOF mem_used=$((mem_used * 4 / 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" >&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. 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))} 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]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} ___________ ${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]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 'm' 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)) # Using '<<-EOF' is the only way to loop over a command's # output without the use of a pipe ('|') or subshell. # 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%s[%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=''; c2='' c3=''; c4='' c5=''; c6='' c7=''; c8='' } # Store the output of 'uname' to avoid calling it multiple times # throughout the script. 'read </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") >&6 } main "$@"