commit 568a10253ec221f4585a193bb257330bf2fc63bf Author: Theri Date: Mon Dec 16 08:21:58 2024 +0000 ⭐ Furtop v1.0 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..23f69dc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*.{sh,md,cfg,sample}] +indent_style = tab +indent_size = 4 + +[furtop] +indent_style = tab +indent_size = 4 \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..45d2a03 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: aristocratos +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..80f8e27 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,41 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: aristocratos + +--- + +**Describe the bug** + +[A clear and concise description of what the bug is.] + +**To Reproduce** + +[Steps to reproduce the behavior:] + +**Expected behavior** + +[A clear and concise description of what you expected to happen.] + +**Screenshots** + +[If applicable, add screenshots to help explain your problem.] + +**Info (please complete the following information):** + - furtop version: + - (Linux) Linux distribution and version: + - (Linux) Data collection type (/proc or psutil): + - Psutil version: `python3 -c "import psutil; print(psutil.version_info)"` (version 5.7.0 or above is required): + - (OSX/FreeBSD) Os release version: + - Terminal used: + - Font used: + - Bash version, `bash --version` (version 4.4 or above is required): + - Locales: output of `locale -v` + +**Additional context** + +contents of `$HOME/.config/furtop/error.log` + +(enable error-logging in "$HOME/.config/furtop/furtop.cfg" if missing) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..93fb3ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[REQUEST]" +labels: enhancement +assignees: aristocratos + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c0704d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +DEB/furtop_* +DEB/usr/* \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dedb624 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "test/libs/bats"] + path = test/libs/bats + url = https://github.com/sstephenson/bats +[submodule "test/libs/bats-assert"] + path = test/libs/bats-assert + url = https://github.com/ztombol/bats-assert +[submodule "test/libs/bats-support"] + path = test/libs/bats-support + url = https://github.com/ztombol/bats-support diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b589ccf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +# travis does not offer python container for OSX or Windows, not sure if we can get automated testing? +os: linux +language: python +python: + - "3.6" +dist: bionic +install: + - pip install -r requirements.txt +script: ./test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..af65b95 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,292 @@ +# Changelog + +## v0.9.25 + +* Fixed: Crash when using "/proc" data collection and filesystem type is 9p, by @bolapara + +## v0.9.24 + +* Fixed: Psutil script crash on OSX +* Fixed: Error handling for malformed osx-cpu-temp output + +## v0.9.23 + +* Fixed: kill/terminate/interrupt process not working in OsX and FreeBSD + +## v0.9.22 + +* Added: Added handler for mktemp failure for psutil script +* Removed: Secondary mktemp command for psutil script +* Fixed: Insecure test import of psutil changed + +## v0.9.21 + +* Changed: Config file comments for theme locations +* Added: Check for correct theme file path prefix +* Added: Support for application cursor mode input +* Fixed: Incorrect value calculation for reversed proc gradient + +## v0.9.20 + +* Fixed: Psutil script security issue when placed directly in temp folder + +## v0.9.19 + +* Added: Option for timestamps with python on bash < 5 +* Changed: Reverted "date" command timestamps to not using fifo +* Added missing # from hex value in monokai theme + +## v0.9.18 + +* Fixed: Errors caused by process scroll change +* Fixed: Process graph creation ignored for process below 0.5% + +## v0.9.17 + +* Changed: Process list now scrolls instead of "page jump" and shows number of processes instead of number of pages +* Fixed: Inverted gradient on dark text in processes box + +## v0.9.16 + +* Fixed: Errors in v0.9.15 psutil disk collection fix +* Added: Additional graph creation error checks + +## v0.9.15 + +* Fixed: Psutil error on disk collection now fallback to df and iostat + +## v0.9.14 + +* Added: Additional processes error checking +* Added: Additional sensors error checking +* Added: Additional psutil error checking + +## v0.9.13 + +* Added: More robust psutil error handling + +## v0.9.12 + +* Changed: Psutil data collection now runs a python script in a coprocess taking commands and sending output over coproc pipes +* Added: Psutil data collection now replaces most external calls including sensors, cpu info, disks info and io collection +* Changed: Tree view is now a toggle instead of sorting option +* Fixed: Cpu temp check not using vcgencmd when sensors is available + +## v0.9.11 + +* Fixed: Processes text color now sets RGB instead of RBB... + +## v0.9.10 + +* Fixed: Humanizer function now round values 1000-1023 up to 1024 to fit size constraints. +* Added: More error checks for psutil +* Changed: Terminal title now includes original title if $TERMINAL_TITLE is set, suggested by @theytaz + +## v0.9.9 + +* Fixed: Fixed theme downloader not reporting new themes and corrected comment in config + +## v0.9.8 + +* Added: Nord theme by Justin Zobel +* Changed: Theme downloader now overwrites default themes, folder user_themes (safe from overwrites) added +* Changed: Cleaned up monokai theme variants +* Added: Base for testing with BATS by Maciek Swiech + +## v0.9.7 + +* Changed: UTF-8 locale check, try to find UTF-8 for current language if LANG is set but not with "UTF-8" suffix + +## v0.9.6 + +* Fixed: UTF-8 locale check + +## v0.9.5 + +* Added: UTF-8 locale check and automatic LANG variable set if not UTF-8 +* Fixed: Filter out zero sized disks and added some psutil error checks + +## v0.9.4 + +* Fixed: Missing path for OSX df and correct swap usage reporting for OSX + +## v0.9.3 + +* Fixed: Resizing problems in iTerm2 +* Changed: Removed redundant error checking in print function for lower cpu usage +* Fixed: Memory in OSX now shows active memory usage and /private/var/vm as swap memory +* Fixed: Disks in OSX changed from using "GNU df" to "BSD df" for better compatibility + +## v0.9.2 + +* Fixed: Correct prefixes for some missed GNU tools +* Added: Startup progress screen +* Changed: replaced tput commands with escape sequence commands + +## v0.9.1 + +* Added: FreeBSD support with python3 psutil data collection +* Added: Check for gnu tools on non Linux platforms +* Fixed: Increased graph history to avoid cut off on high resolution graphs + +## v0.9.0 + +* Added: Mac OS X support with python3 psutil data collection +* Added: Ability to switch between all available network devices + +## v0.8.32 + +* Fixed: Error in theme error checking corrupting default theme + +## v0.8.31 + +* Fixed: Theme 2-color gradient generation +* Fixed: Theme file error checking + +## v0.8.30 + +* Fixed: Crash on missing net device + +## v0.8.29 + +* Fixed: Cpu temperature colors not working when above high temp value +* Fixed: Unescaped "\" in process list and indent fixes +* Changed: Changes to net graph rescaling parameters + +## v0.8.28 + +* Fixed: Ctrl-C and Ctrl-Z not registering after change to "dd" +* Added: Option to switch to high resolution graphs +* Added: Current peak value for download/upload graphs + +## v0.8.27 + +* Fixed: Use value for "Inactive"+"MemFree" if "MemAvailable" is missing in /proc/meminfo +* Added: Option to toggle update check at start + +## v0.8.26 + +* Fixed: Escaped delimiter for sed to fix config not saving "/" character +* Fixed: Detailed process view missing info and slowdown in certain cases +* Optimization: Fork cleanup + +## v0.8.25 + +* Fixed: Backspace not registering when not set to send ascii delete +* Fixed: Broken cpu temperature graph when is value over cpu high temp +* Added: Possibility to run date through background fifo for bash <5 + +## v0.8.24 + +* Fixed: Input error freezes, by changing from using "read" command to using "dd" for reading keyboard input. + +## v0.8.23 + +* Added: Support for Raspberry Pi cpu temperature reporting +* Fixed: Decreased chance of read command stalling on lower spec systems +* Added: Failover to nproc if lscpu are reporting 0 cpu cores +* Changed: Moved page display for options and help to bottom and changed to Page Up/Down for changing page + +## v0.8.22 + +* Added: Sorting option "tree", shows processes in a tree structure +* Added: Option to toggle process cpu usage per core instead of total available cpu power +* Fixed: Possible fix for stalling read command +* Added: Multiple while loop fail safes + +## v0.8.21 + +* Fixed: iostat flag compatibility for older iostat versions +* Fixed: possible fix for script stall on bash 4 + +## v0.8.20 + +* Fixed: Update slowdown when not sorting by cpu +* Added: New version desktop notification + +## v0.8.19 + +* Added: Disks read and write stats, requires new optional dependency "iostat (part of sysstat)" +* Fixed: Ctrl-C not working when showing resize error message +* Fixed: Network download/upload offset auto switched off if /proc/net/dev resets +* Fixed: Removed trailing whitespace in script + +## v0.8.18 + +* Added: Pagination for help and options windows if items don't fit +* Added: Option to turn off color gradient in process list +* Changed: bash version check to use $BASH_VERSINFO array +* Added: Filter for shown disks +* Added: Option to reset network totals in options menu + +## v0.8.17 + +* Fixed: Not showing CPU temperatures when "Package" temp is missing +* Added: CPU temperature support for AMD Ryzen +* Changed: Minimum size changed from 80x25 to 80x24 +* Fixed: High cpu usage on systems with a lot of mounted disks + +## v0.8.16 + +* Added: Bash version check, by Calinou +* Added: OS check, by kpucynski +* Fixed: number of themes reported in options when theme folder is empty, by deluxe +* Fixed: README.md typos, by lucaskim1233 +* Added: CHANGELOG.md + +## v0.8.15 + +* Added: deb build script by Jukoo +* Fixed: load average and uptime not showing +* Fixed: freeze on reverse process order when showing detailed information +* Fixed: single quotes on associative arrays + +## v0.8.14 + +* Fixed: disks usage runaway array +* Fixed: disks used not reporting new values +* Changed: memory and disks update frequency increased + +## v0.8.13 + +* Fixed: get_value() regex +* Added: 2 new themes, flat-remix and flat-remix-light, by Daniel Ruiz de Alegría +* Other: general cleanup and formatting + +## v0.8.12 + +* Fixed: changed remaining ps thcount flags to nlwp + +## v0.8.11 + +* Fixed: ps flag thcount changed to nlwp for greater compability +* Fixed: regex and float to int rounding in get_value() + +## v0.8.10 + +* Fixed: erroneous regular expressions + +## v0.8.9 + +* Added: functions is_int, is_float, is_hex +* Fixes: error checking on internal functions + +## v0.8.8 + +* Fixed: load average max length + +## v0.8.7 + +* Fixed: load average clipping +* Fixed: cpu box calculations error + +## v0.8.6 + +* Added: load average and uptime +* Fixed: cohesive window size representation +* Added: unset LC_ALL to not override wanted locale +* Fixed: cpu box calculation errors + +## v0.8.5 + +* Fixed: cpu frequency and /proc/stat error checks diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4ef2c68 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at admin@qvantnet.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cac272e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +# Contributing guidelines + +## When submitting pull requests + +* Explain your thinking in why a change or addition is needed. + * Is it a requested change or feature? + * If not, open a feature request to get feedback before making a pull request. + +* If it's a fix for a unreported bug, make a bug report and link the pull request. + +* Split up multiple unrelated changes in multiple pull requests. + +* [Shellcheck](https://github.com/koalaman/shellcheck) your work. Current shellsheck exceptions at the beginning of [furtop](furtop). + +* Purely cosmetic changes won't be accepted without a very good explanation of its value. + * (Some design choices are for better configurability of syntax highlighting.) + +## Formatting + +### Follow the current syntax design + +* Indent type: Tabs + +* Tab size: 4 + +* Use the longer "if, elif, then, else, fi" statements and indent conditionals, loops etc. + +* Use "[[ ]]", "(( ))" for conditions and "$( ), <( )" for command substitution. + +* Create functions instead of repeating blocks of code. + +* Don't stack unrelated blocks of code, leave blank lines for better readability. + +* Comment new code that isn't very obvious in it's function. + +* Name new variables and functions in lower-case and after what purpose they serve. + * (Exception arithmetic with many variables, make sure to comment what's happening instead.) + +## Optimization + +* Avoid forks if possible. + +* Avoid writing to disk if possible. + +* Make sure variables/arrays are cleaned up if not reused. + +* Compare cpu and memory usage with and without your code and look for alternatives if they cause a noticeable negative impact. + +For questions contact Aristocratos at admin@qvantnet.com + +For proposing changes to this document create a [new issue](https://github.com/aristocratos/furtop/issues/new/choose). diff --git a/DEB/DEBIAN/control b/DEB/DEBIAN/control new file mode 100644 index 0000000..9a898ea --- /dev/null +++ b/DEB/DEBIAN/control @@ -0,0 +1,10 @@ +Package: furtop +Version: 0.0.0 +Section: base +Priority: optional +Architecture: all +Depends: bash (>= 4.4),curl (>= 7.16.2), coreutils, sed, awk, grep +Maintainer: your_name +Description: Resource monitor that shows usage + and stats for processor, memory, disks, network and processes +Homepage: https://github.com/aristocratos/furtop diff --git a/DEB/DEBIAN/postinst b/DEB/DEBIAN/postinst new file mode 100755 index 0000000..5c79ea1 --- /dev/null +++ b/DEB/DEBIAN/postinst @@ -0,0 +1,4 @@ +#!/bin/bash + + +echo -e "[\033[1;32m done \033[0m]" diff --git a/DEB/DEBIAN/postrm b/DEB/DEBIAN/postrm new file mode 100755 index 0000000..018758e --- /dev/null +++ b/DEB/DEBIAN/postrm @@ -0,0 +1,4 @@ +#!/bin/bash + + +echo -e "[\033[1;32m successfully removed \033[0m]" diff --git a/DEB/DEBIAN/preinst b/DEB/DEBIAN/preinst new file mode 100755 index 0000000..91750c3 --- /dev/null +++ b/DEB/DEBIAN/preinst @@ -0,0 +1,4 @@ +#!/bin/bash + + +echo -e "[\033[1;32m starting installation \033[0m]" diff --git a/DEB/DEBIAN/prerm b/DEB/DEBIAN/prerm new file mode 100755 index 0000000..d586fe2 --- /dev/null +++ b/DEB/DEBIAN/prerm @@ -0,0 +1,4 @@ +#!/bin/bash + + +echo -e "[\033[1;33m removing packet from the system \033[0m]" diff --git a/DEB/build b/DEB/build new file mode 100755 index 0000000..b206506 --- /dev/null +++ b/DEB/build @@ -0,0 +1,115 @@ +#!/bin/bash + + +# this little script is just to automate the creation of the .deb package under debian and also the automatic installation of the furtop program in the system. +# How does it work? +# ---- +# It parses the furtop file to retrieve the last version specified in the script to allow a fresh creation of the .deb package +# +by also retrieving the most recent version of the script then it proceeds to the installation ... + +set -o errexit +#set -x # just for debuging + +readonly file_src_location=../furtop # furtop location ^ +readonly ubin=usr/bin/ +readonly file_name=${file_src_location##*/} +readonly ctrl_file=DEBIAN/control +readonly architecture=`dpkg --print-architecture` # for all architectures +readonly root_uid=0 +declare version build_version + +[[ ROOT::PERMISSION ]] +{ + [[ $UID -ne ${root_uid} ]] && { + echo -e "require root user" + exit $UID + } +} + +[[ ARGUMENTS::HANDLER ]] +{ + if [[ -n $1 ]] ; then + case $1 in + "--remove") + [[ -x /${ubin}/${file_name} ]] && { + dpkg --remove ${file_name} + test $? -eq 0 && exit 0 + }||{ + echo -e "~ nothing todo: furtop is removed " + exit 0 + } + ;; + esac + fi +} + +echo -e "+ building package ..." +sleep 1 +[[ FILECHECK ]] +{ + #+ require furtop file to read inside + [[ ! -f ${file_src_location} ]] && { + echo -e "undefine ${file_name}" + exit 3 # just a basic exit + }|| { + echo -e "+ populate DEB folder " + [[ -d $ubin ]] || mkdir -p $ubin + `cp $file_src_location $ubin` + } + + #+ require control file to write inside + [[ ! -f ${ctrl_file} ]] && { + echo -e "undefined ${ctrl_file##*/}" + exit 3 + } +} +[[ IO::SEMVERS ]] +{ + echo -e "+ fetching the lastest version of ${file_name}" + + get_current_version () { + local watch_version=`grep -i "declare version" ${file_src_location}` + local semvers=${watch_version##*=} + echo ${semvers:1:-1} + } + + set_new_version_ctrl() { + local catch_package_version=`grep -i version ${ctrl_file}` + version=${catch_package_version%%:*} + build_version=${catch_package_version##*:} + [[ -n $1 ]] && build_version=$1 + version+=": ${build_version}" + + echo -e "+ set new version control" + `sed -i "s/$catch_package_version/${version}/g" ${ctrl_file}` + } + +set_new_version_ctrl $(get_current_version) +} + +[[ PACKAGER_BUILD::DEB ]] +{ + build_for_debian_base (){ + local debian_package_name=${file_name}_${build_version}-${architecture}.deb + #echo ${debian_package_name} + dpkg-deb --build ../DEB ${debian_package_name} + test $? -eq 0 && { + if [[ -f ${debian_package_name} ]] ;then + dpkg -i ${debian_package_name} + [[ $? -eq 0 ]] && { + exit $? + }||{ + echo -e "Installation failed" + exit $? + } + fi + }||{ + echo -e "build failed" + exit 5 + } + } + +build_for_debian_base +} + + diff --git a/Imgs/bpytop.png b/Imgs/bpytop.png new file mode 100755 index 0000000..1539938 Binary files /dev/null and b/Imgs/bpytop.png differ diff --git a/Imgs/logo-t.png b/Imgs/logo-t.png new file mode 100644 index 0000000..5767721 Binary files /dev/null and b/Imgs/logo-t.png differ diff --git a/Imgs/main.png b/Imgs/main.png new file mode 100644 index 0000000..69fc6ec Binary files /dev/null and b/Imgs/main.png differ diff --git a/Imgs/menu.png b/Imgs/menu.png new file mode 100644 index 0000000..72a5b3f Binary files /dev/null and b/Imgs/menu.png differ diff --git a/Imgs/options.png b/Imgs/options.png new file mode 100644 index 0000000..105025c Binary files /dev/null and b/Imgs/options.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..837c187 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +PREFIX ?= /usr/local +DOCDIR ?= $(PREFIX)/share/doc/furtop + +all: + @echo Run \'make install\' to install furtop. + +install: + @mkdir -p $(DESTDIR)$(PREFIX)/bin + @cp -p furtop $(DESTDIR)$(PREFIX)/bin/furtop + @mkdir -p $(DESTDIR)$(DOCDIR) + @cp -p README.md $(DESTDIR)$(DOCDIR) + @chmod 755 $(DESTDIR)$(PREFIX)/bin/furtop + +uninstall: + @rm -rf $(DESTDIR)$(PREFIX)/bin/furtop + @rm -rf $(DESTDIR)$(DOCDIR) diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa1b7f9 --- /dev/null +++ b/README.md @@ -0,0 +1,403 @@ +# ![furtop](Imgs/logo-t.png) + +![Linux](https://img.shields.io/badge/-Linux-grey?logo=linux) +![OSX](https://img.shields.io/badge/-OSX-black?logo=apple) +![FreeBSD](https://img.shields.io/badge/-FreeBSD-red?logo=freebsd) +![Usage](https://img.shields.io/badge/Usage-System%20resource%20monitor-blue) +![Bash](https://img.shields.io/badge/Bash-v4.4%5E-green?logo=GNU%20bash) +![Python](https://img.shields.io/badge/Python-v3.6%5E-orange?logo=python) + + +# + +## Index + +* [Documents](#documents) +* [Description](#description) +* [Features](#features) +* [Themes](#themes) +* [Support and funding](#support-and-funding) +* [Prerequisites](#prerequisites) +* [Dependencies](#dependencies) +* [Screenshots](#screenshots) +* [Installation](#installation) +* [Configurability](#configurability) +* [TODO](#todo) +* [License](#license) + + +## Documents + +#### [CHANGELOG.md](CHANGELOG.md) + +#### [CONTRIBUTING.md](CONTRIBUTING.md) + +#### [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) + +## Description + +Resource monitor that shows usage and stats for processor, memory, disks, network and processes. + +## Features + +* Easy to use, with a game inspired menu system. +* Fast and "mostly" responsive UI with UP, DOWN keys process selection. +* Function for showing detailed stats for selected process. +* Ability to filter processes. +* Easy switching between sorting options. +* Send SIGTERM, SIGKILL, SIGINT to selected process. +* UI menu for changing all config file options. +* Auto scaling graph for network usage. +* Shows message in menu if new version is available +* Shows current read and write speeds for disks +* Multiple data collection methods which can be switched if running on Linux + +## Themes + +furtop now has theme support and a function to download missing local themes from repository. + +See [themes](themes) folder for available themes. + +The builtin theme downloader places the default themes in `$HOME/.config/furtop/themes`. +User created themes should be placed in `$HOME/.config/furtop/user_themes` to be safe from overwrites. + +Let me know if you want to contribute with new themes. + +## Support and funding + +Bug fixes and updates might be slow during normal workdays since I work full time as an industrial worker and don't have much time or energy left during the week. +I'm looking into ways of funding this project that would allow me to take off time from my day job to work on this. + +Any advice on how to get funding for open source projects is very welcome! + +#### Update + +You can now sponsor this project through github, see [my sponsors page](https://github.com/sponsors/aristocratos) for options. + +Also added donation links for [paypal](https://paypal.me/aristocratos) and [ko-fi](https://ko-fi.com/aristocratos). + +Any support is greatly appreciated! + +## Prerequisites + +#### Mac Os X + +Will not display correctly in the standard terminal! +Recommended alternative [iTerm2](https://www.iterm2.com/) + +Will also need to be run as superuser to display stats for processes not owned by user. + +#### Linux, Mac Os X and FreeBSD + +For correct display, a terminal with support for: + +* 24-bit truecolor ([See list of terminals with truecolor support](https://gist.github.com/XVilka/8346728)) +* Wide characters (Are sometimes problematic in web-based terminals) + +Also needs a UTF8 locale and a font that covers: + +* Unicode Block “Braille Patterns” U+2800 - U+28FF +* Unicode Block “Geometric Shapes” U+25A0 - U+25FF +* Unicode Block "Box Drawing" and "Block Elements" U+2500 - U+259F + +#### Notice + +Dropbear seems to not be able to set correct locale. So if accessing furtop over ssh, OpenSSH is recommended. + +## Dependencies + +## Linux, OSX and FreeBSD + +**[bash](https://www.gnu.org/software/bash/)** (v4.4 or later) Script functionality will most probably break with earlier versions. +Bash version 5 is highly recommended to make use of $EPOCHREALTIME variable instead of a lot of external date command calls. + +**[GNU coreutils](https://www.gnu.org/software/coreutils/)** + +**[GNU sed](https://www.gnu.org/software/sed/)** + +## Linux using /proc for data collection + +**[GNU grep](https://www.gnu.org/software/grep/)** + +**[ps from procps-ng](https://gitlab.com/procps-ng/procps)** (v3.1.15 or later) + +**[GNU awk](https://www.gnu.org/software/gawk/)** + +## OSX and FreeBSD or Linux using psutil for data collection + +**[Python3](https://www.python.org/downloads/)** (v3.6 or later) + +**[psutil python module](https://github.com/giampaolo/psutil)** (v5.7.0 or later) + +## Optionals for additional stats + +(Optional OSX) **[osx-cpu-temp](https://github.com/lavoiesl/osx-cpu-temp)** Needed to show CPU temperatures. + +(Optional Linux) **[lm-sensors](https://github.com/lm-sensors/lm-sensors)** Needed to show CPU temperatures. + +(Optional Linux) **[iostat (part of sysstat)](https://github.com/sysstat/sysstat)** Needed if you want disk read/write stats and are not using psutil data collection. + +(Optional OSX/Linux/FreeBSD) **[curl](https://curl.haxx.se/download.html)** (v7.16.2 or later) Needed if you want messages about updates and the ability to download themes. + +## Screenshots + +Main UI showing details for a selected process. +![Screenshot 1](Imgs/main.png) + +Main menu. +![Screenshot 2](Imgs/menu.png) + +Options menu. +![Screenshot 3](Imgs/options.png) + +## Installation + +#### Dependencies installation OSX + +>Install homebrew if not already installed + +``` bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" +``` + + + +>If you got python 3.6 or later installed outside of brew: + +``` bash +sudo python3 -m ensurepip +sudo python3 -m pip install psutil +``` + +>If you haven't got python3 installed: + +``` +brew install python3 +python3 -m pip install psutil +``` + +>Install dependencies + + +``` bash +brew install bash coreutils gnu-sed git +``` + +>Install optional dependency osx-cpu-temp + +``` bash +brew install osx-cpu-temp +``` + +#### Dependencies installation FreeBSD + +>Install with pkg and pip + +``` bash +sudo pkg install coreutils gsed git py37-psutil +``` + +#### Manual installation Linux, OSX and FreeBSD + +>Clone and install + +``` bash +git clone https://github.com/aristocratos/furtop.git +cd furtop +sudo make install +``` + +>to uninstall it + +``` bash +sudo make uninstall +``` + +#### FreeBSD package + +Available in [FreeBSD ports](https://www.freshports.org/sysutils/furtop/) + +Install pre-built pacakge + +``` bash +sudo pkg install furtop +``` + +#### Arch based + +Available in the AUR as [furtop-git](https://aur.archlinux.org/packages/furtop-git/) + +Available in the Arch Linux repository as [furtop](https://www.archlinux.org/packages/community/any/furtop/) + +#### Debian based + +Available in [official Debian repository](https://tracker.debian.org/pkg/furtop) since Debian 11 + +Available for debian/ubuntu from [Azlux's repository](http://packages.azlux.fr/) + +Or use quick installation: + +>Quick install go to DEB folder and type + +``` bash + sudo ./build +``` + +>to uninstall it go to DEB folder and type + +``` bash + sudo ./build --remove +``` + +#### Guix based + +Available in [official Guix repository](https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/packages/admin.scm) since 6bbd0fd2 + +>Installation + +``` bash +guix install furtop +``` + +#### Ubuntu based + +Available in [official Ubuntu repository](https://launchpad.net/ubuntu/+source/furtop) since Ubuntu 20.10 + +Available for Ubuntu from [PPA repository](https://code.launchpad.net/~furtop-monitor/+archive/ubuntu/furtop) + +>Add PPA repository and install furtop + +``` bash + sudo add-apt-repository ppa:furtop-monitor/furtop + sudo apt update + sudo apt install furtop +``` + +#### Fedora + +Available in the Fedora repository. + +>Installation + +``` bash +sudo dnf install furtop +``` + +#### CentOS 8 + +>Installation + +``` bash +dnf config-manager --set-enabled PowerTools +dnf install epel-release +dnf install furtop +``` + +#### RHEL 8 + +>Installation + +``` bash +ARCH=$( /bin/arch ) +subscription-manager repos --enable +"codeready-builder-for-rhel-8-${ARCH}-rpms" +dnf install epel-release +dnf install furtop +``` + +## Configurability + +All options changeable from within UI. +Config files stored in "$HOME/.config/furtop" folder + +#### furtop.cfg: (auto generated if not found) + +```bash +#? Config file for furtop v. 0.9.21 + +#* Color theme, looks for a .theme file in "$HOME/.config/furtop/themes" and "$HOME/.config/furtop/user_themes" +#* Should be prefixed with either "themes/" or "user_themes/" depending on location, "Default" for builtin default theme +color_theme="Default" + +#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs +update_ms="2500" + +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive" +#* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly +proc_sorting="cpu lazy" + +#* Reverse sorting order, "true" or "false" +proc_reversed="false" + +#* Show processes as a tree +proc_tree="false" + +#* Check cpu temperature, only works if "sensors", "vcgencmd" or "osx-cpu-temp" commands is available +check_temp="true" + +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable +draw_clock="%X" + +#* Update main ui when menus are showing, set this to false if the menus is flickering too much for comfort +background_update="true" + +#* Custom cpu model name, empty string to disable +custom_cpu_name="" + +#* Enable error logging to "$HOME/.config/furtop/error.log", "true" or "false" +error_logging="true" + +#* Show color gradient in process list, "true" or "false" +proc_gradient="true" + +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power +proc_per_core="false" + +#* Optional filter for shown disks, should be names of mountpoints, "root" replaces "/", separate multiple values with space +disks_filter="" + +#* Enable check for new version from github.com/aristocratos/furtop at start +update_check="true" + +#* Enable graphs with double the horizontal resolution, increases cpu usage +hires_graphs="false" + +#* Enable the use of psutil python3 module for data collection, default on OSX +use_psutil="true" +``` + +#### Command line options: (not yet implemented) + +``` bash +USAGE: furtop + +``` + +## TODO + +Might finish off items out of order since I usually work on multiple at a time. + +- [x] Add options to change colors for text, graphs and meters. +- [x] Fix cross platform compatibility for Mac OSX and *BSD: Working on OSX, and FreeBSD. +- [x] Add support for showing AMD cpu temperatures. +- [x] Add option to show tree view of processes. +- [x] Add option to reset network download/upload totals. +- [x] Add option to turn of gradient in processes list. +- [ ] Add gpu temp and usage. (If feasible) +- [x] Add io stats for disks. +- [ ] Add cpu and mem stats for docker containers. (If feasible) +- [x] Change process list to line scroll instead of page change. +- [ ] Add optional window for tailing log files. +- [ ] Add options for resizing all boxes. +- [ ] Add command line argument parsing. +- [ ] Builtin updater. Relevant PR #96 by Jukoo +- [ ] Add support for zram in memory box. Relevant PR #122 by perkinslr + +- [ ] Miscellaneous optimizations and code cleanup. +- [ ] Add more commenting where it's sparse. + +- [ ] Python port. (Porting started) + +## LICENSE + +[Apache License 2.0](LICENSE) diff --git a/furtop b/furtop new file mode 100755 index 0000000..48ad2a8 --- /dev/null +++ b/furtop @@ -0,0 +1,5312 @@ +#!/usr/bin/env bash +# indent type=tab +# tab size=4 +# shellcheck disable=SC2034 #Unused variables +# shellcheck disable=SC2068 #Double quote array warning +# shellcheck disable=SC2086 # Double quote warning +# shellcheck disable=SC2140 # Word form warning +# shellcheck disable=SC2162 #Read without -r +# shellcheck disable=SC2206 #Word split warning +# shellcheck disable=SC2178 #Array to string warning +# shellcheck disable=SC2102 #Ranges only match single +# shellcheck disable=SC2004 #arithmetic brackets warning +# shellcheck disable=SC2017 #arithmetic precision warning +# shellcheck disable=SC2207 #split array warning +# shellcheck disable=SC2154 #variable referenced but not assigned +# shellcheck disable=SC1003 #info: single quote escape +# shellcheck disable=SC2179 # array append warning +# shellcheck disable=SC2128 # expanding array without index warning + + +# Copyright 2020 Aristocratos (jakob@qvantnet.com) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +declare -x LC_MESSAGES="C" LC_NUMERIC="C" LC_ALL="" + +#* Fail if running on unsupported OS +case "$(uname -s)" in + Linux*) system=Linux;; + *BSD) system=BSD;; + Darwin*) system=MacOS;; + CYGWIN*) system=Cygwin;; + MINGW*) system=MinGw;; + *) system="Other" +esac +if [[ ! $system =~ Linux|MacOS|BSD ]]; then + echo "This version of furtop does not support $system platform." + exit 1 +fi + +#* Fail if Bash version is below 4.4 +bash_version_major=${BASH_VERSINFO[0]} +bash_version_minor=${BASH_VERSINFO[1]} +if [[ "$bash_version_major" -lt 4 ]] || [[ "$bash_version_major" == 4 && "$bash_version_minor" -lt 4 ]]; then + echo "ERROR: Bash 4.4 or later is required (you are using Bash $bash_version_major.$bash_version_minor)." + exit 1 +fi + +shopt -qu failglob nullglob +shopt -qs extglob globasciiranges globstar + +#* Check for UTF-8 locale and set LANG variable if not set +if [[ ! $LANG =~ UTF-8 ]]; then + if [[ -n $LANG && ${LANG::1} != "C" ]]; then old_lang="${LANG%.*}"; fi + for set_lang in $(locale -a); do + if [[ $set_lang =~ utf8|UTF-8 ]]; then + if [[ -n $old_lang && $set_lang =~ ${old_lang} ]]; then + declare -x LANG="${set_lang/utf8/UTF-8}" + set_lang_search="found" + break + elif [[ -z $first_lang ]]; then + first_lang="${set_lang/utf8/UTF-8}" + set_lang_first="found" + fi + if [[ -z $old_lang ]]; then break; fi + fi + done + if [[ $set_lang_search != "found" && $set_lang_first != "found" ]]; then + echo "ERROR: No UTF-8 locale found!" + exit 1 + elif [[ $set_lang_search != "found" ]]; then + declare -x LANG="${first_lang/utf8/UTF-8}" + fi + unset old_lang set_lang first_lang set_lang_search set_lang_first +fi + +declare -a banner banner_colors +banner=( + + +"███████╗██╗ ██╗██████╗ ████████╗ ██████╗ ██████╗ " +"██╔════╝██║ ██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗" +"█████╗ ██║ ██║██████╔╝ ██║ ██║ ██║██████╔╝" +"██╔══╝ ██║ ██║██╔══██╗ ██║ ██║ ██║██╔═══╝ " +"██║ ╚██████╔╝██║ ██║ ██║ ╚██████╔╝██║ " +"╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ") +declare version="1.0" + + +declare banner_width=${#banner[0]} +banner_colors=("#e525c2" "#d930ba" "#ca3eb0" "#bb4ca6" "#ac5a9c") + +#* Set correct names for GNU tools depending on OS +if [[ $system != "Linux" ]]; then tool_prefix="g"; fi +for tool in "dd" "df" "stty" "tail" "realpath" "wc" "rm" "mv" "sleep" "stdbuf" "mkfifo" "date" "kill" "sed"; do + declare -n set_tool="${tool}" + set_tool="${tool_prefix}${tool}" +done + +if ! command -v ${dd} >/dev/null 2>&1; then + echo "ERROR: Missing GNU coreutils!" + exit 1 +elif ! command -v ${sed} >/dev/null 2>&1; then + echo "ERROR: Missing GNU sed!" + exit 1 +fi + +read tty_height tty_width < <(${stty} size) + +#? Start default variables------------------------------------------------------------------------------> +#? These values are used to create "$HOME/.config/furtop/furtop.cfg" +#? Any changes made here will be ignored if config file exists +aaa_config() { : ; } #! Do not remove this line! + +#* Color theme, looks for a .theme file in "$HOME/.config/furtop/themes" and "$HOME/.config/furtop/user_themes" +#* Should be prefixed with either "themes/" or "user_themes/" depending on location, "Default" for builtin default theme +color_theme="Default" + +#* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs +update_ms="2500" + +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive" +#* "cpu lazy" updates sorting over time, "cpu responsive" updates sorting directly +proc_sorting="cpu lazy" + +#* Reverse sorting order, "true" or "false" +proc_reversed="false" + +#* Show processes as a tree +proc_tree="false" + +#* Check cpu temperature, only works if "sensors", "vcgencmd" or "osx-cpu-temp" commands is available +check_temp="true" + +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable +draw_clock="%X" + +#* Update main ui when menus are showing, set this to false if the menus is flickering too much for comfort +background_update="true" + +#* Custom cpu model name, empty string to disable +custom_cpu_name="" + +#* Enable error logging to "$HOME/.config/furtop/error.log", "true" or "false" +error_logging="true" + +#* Show color gradient in process list, "true" or "false" +proc_gradient="true" + +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power +proc_per_core="false" + +#* Optional filter for shown disks, should be names of mountpoints, "root" replaces "/", separate multiple values with space +disks_filter="" + +#* Enable check for new version from github.com/aristocratos/furtop at start +update_check="true" + +#* Enable graphs with double the horizontal resolution, increases cpu usage +hires_graphs="false" + +#* Enable the use of psutil python3 module for data collection, default on OSX +use_psutil="true" + +aaz_config() { : ; } #! Do not remove this line! +#? End default variables--------------------------------------------------------------------------------> + +declare -a menu_options menu_help menu_quit + +menu_options=( +"┌─┐┌─┐┌┬┐┬┌─┐┌┐┌┌─┐" +"│ │├─┘ │ ││ ││││└─┐" +"└─┘┴ ┴ ┴└─┘┘└┘└─┘") +menu_help=( +"┬ ┬┌─┐┬ ┌─┐" +"├─┤├┤ │ ├─┘" +"┴ ┴└─┘┴─┘┴ ") +menu_quit=( +"┌─┐ ┬ ┬ ┬┌┬┐" +"│─┼┐│ │ │ │ " +"└─┘└└─┘ ┴ ┴ ") + +menu_options_selected=( +"╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗" +"║ ║╠═╝ ║ ║║ ║║║║╚═╗" +"╚═╝╩ ╩ ╩╚═╝╝╚╝╚═╝") +menu_help_selected=( +"╦ ╦╔═╗╦ ╔═╗" +"╠═╣║╣ ║ ╠═╝" +"╩ ╩╚═╝╩═╝╩ ") +menu_quit_selected=( +"╔═╗ ╦ ╦ ╦╔╦╗ " +"║═╬╗║ ║ ║ ║ " +"╚═╝╚╚═╝ ╩ ╩ ") + +declare -A cpu mem swap proc net box theme disks +declare -a cpu_usage cpu_graph_a cpu_graph_b color_meter color_temp_graph color_cpu color_cpu_graph cpu_history color_mem_graph color_swap_graph +declare -a mem_history swap_history net_history_download net_history_upload mem_graph swap_graph proc_array download_graph upload_graph trace_array +declare resized=1 size_error clock tty_width tty_height hex="16#" cpu_p_box swap_on=1 draw_out esc_character boxes_out last_screen clock_out update_string +declare -a options_array=("color_theme" "update_ms" "use_psutil" "proc_sorting" "proc_tree" "check_temp" "draw_clock" "background_update" "custom_cpu_name" + "proc_per_core" "proc_reversed" "proc_gradient" "disks_filter" "hires_graphs" "net_totals_reset" "update_check" "error_logging") +declare -a save_array=(${options_array[*]/net_totals_reset/}) +declare -a sorting=( "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive") +declare -a detail_graph detail_history detail_mem_history disks_io +declare -A pid_history +declare time_left timestamp_start timestamp_end timestamp_input_start timestamp_input_end time_string mem_out proc_misc prev_screen pause_screen filter input_to_filter +declare no_epoch proc_det proc_misc2 sleeping=0 detail_mem_graph proc_det2 proc_out curled git_version has_iostat sensor_comm failed_pipes=0 py_error +declare esc_character tab backspace sleepy late_update skip_process_draw winches quitting theme_int notifier saved_stty nic_int net_misc skip_net_draw +declare psutil_disk_fail +declare -a disks_free disks_total disks_name disks_free_percent saved_key themes nic_list old_procs +printf -v esc_character "\u1b" +printf -v tab "\u09" +printf -v backspace "\u7F" #? Backspace set to DELETE +printf -v backspace_real "\u08" #? Real backspace +#printf -v enter_key "\uA" +printf -v enter_key "\uD" +printf -v ctrl_c "\u03" +printf -v ctrl_z "\u1A" + +hide_cursor='\033[?25l' #* Hide terminal cursor +show_cursor='\033[?25h' #* Show terminal cursor +alt_screen='\033[?1049h' #* Switch to alternate screen +normal_screen='\033[?1049l' #* Switch to normal screen +clear_screen='\033[2J' #* Clear screen + +#* Symbols for graphs +declare -a graph_symbol +graph_symbol=(" " "⡀" "⣀" "⣄" "⣤" "⣦" "⣴" "⣶" "⣷" "⣾" "⣿") +graph_symbol+=( " " "⣿" "⢿" "⡿" "⠿" "⠻" "⠟" "⠛" "⠙" "⠉" "⠈") +declare -A graph_symbol_up='( + [0_0]=⠀ [0_1]=⢀ [0_2]=⢠ [0_3]=⢰ [0_4]=⢸ + [1_0]=⡀ [1_1]=⣀ [1_2]=⣠ [1_3]=⣰ [1_4]=⣸ + [2_0]=⡄ [2_1]=⣄ [2_2]=⣤ [2_3]=⣴ [2_4]=⣼ + [3_0]=⡆ [3_1]=⣆ [3_2]=⣦ [3_3]=⣶ [3_4]=⣾ + [4_0]=⡇ [4_1]=⣇ [4_2]=⣧ [4_3]=⣷ [4_4]=⣿ +)' +declare -A graph_symbol_down='( + [0_0]=⠀ [0_1]=⠈ [0_2]=⠘ [0_3]=⠸ [0_4]=⢸ + [1_0]=⠁ [1_1]=⠉ [1_2]=⠙ [1_3]=⠹ [1_4]=⢹ + [2_0]=⠃ [2_1]=⠋ [2_2]=⠛ [2_3]=⠻ [2_4]=⢻ + [3_0]=⠇ [3_1]=⠏ [3_2]=⠟ [3_3]=⠿ [3_4]=⢿ + [4_0]=⡇ [4_1]=⡏ [4_2]=⡟ [4_3]=⡿ [4_4]=⣿ +)' +declare -A graph +box[boxes]="cpu mem net processes" + +cpu[threads]=0 + +#* Symbols for subscript function +subscript=("₀" "₁" "₂" "₃" "₄" "₅" "₆" "₇" "₈" "₉") + +#* Symbols for create_box function +box[single_hor_line]="─" +box[single_vert_line]="│" +box[single_left_corner_up]="┌" +box[single_right_corner_up]="┐" +box[single_left_corner_down]="└" +box[single_right_corner_down]="┘" +box[single_title_left]="├" +box[single_title_right]="┤" + +box[double_hor_line]="═" +box[double_vert_line]="║" +box[double_left_corner_up]="╔" +box[double_right_corner_up]="╗" +box[double_left_corner_down]="╚" +box[double_right_corner_down]="╝" +box[double_title_left]="╟" +box[double_title_right]="╢" + +init_() { #? Collect needed information and set options before startig main loop + if [[ -z $1 ]]; then + local i stx=0 + #* Set terminal options, save and clear screen + saved_stty="$(${stty} -g)" + echo -en "${alt_screen}${hide_cursor}${clear_screen}" + echo -en "\033]0;${TERMINAL_TITLE} furtop\a" + ${stty} -echo + + #* Wait for resize if terminal size is smaller then 80x24 + if (($tty_width<80 | $tty_height<24)); then resized; echo -en "${clear_screen}"; fi + + #* Draw banner to banner array + local letter b_color banner_line y=0 + local -a banner_out + #print -v banner_out[0] -t "\e[0m" + for banner_line in "${banner[@]}"; do + #* Read banner array letter by letter to set correct color for filled vs outline characters + while read -rN1 letter; do + if [[ $letter == "█" ]]; then b_color="${banner_colors[$y]}" + else b_color="#$((80-y*6))"; fi + if [[ $letter == " " ]]; then + print -v banner_out[y] -r 1 + else + print -v banner_out[y] -fg ${b_color} "${letter}" + fi + done <<<"$banner_line" + ((++y)) + done + banner=("${banner_out[@]}") + + #* Draw banner to screen and show status while running init + draw_banner $((tty_height/2-10)) + + #* Start psutil coprocess if enabled + if [[ $use_psutil == true ]]; then + print -m $(( (tty_height/2-3)+stx++ )) 0 -bg "#00" -fg "#cc" -b -c "Creating psutil coprocess..." + return + fi + fi + + if [[ -n $1 ]]; then local i stx=1; print -bg "#00" -fg "#30ff50" -r 1 -t "√"; fi + + #* Check if "sensors", "osx-cpu-temp" or "vcgencmd" commands is available, if not, disable temperature collection + print -m $(( (tty_height/2-3)+stx++ )) 0 -bg "#00" -fg "#cc" -b -c "Checking available tools..." + if [[ $check_temp == true ]]; then + local has_temp + sensor_comm="" + if [[ $use_psutil == true ]]; then + py_command -v has_temp "get_sensors_check()" + if [[ $has_temp == true ]]; then sensor_comm="psutil"; fi + fi + if [[ -z $sensor_comm ]]; then + local checker + for checker in "vcgencmd" "sensors" "osx-cpu-temp"; do + if command -v "${checker}" >/dev/null 2>&1; then sensor_comm="${checker}"; break; fi + done + fi + if [[ -z $sensor_comm ]]; then check_temp="false"; fi + fi + + #* Check if "curl" command is available, if not, disable update check and theme downloads + if command -v curl >/dev/null 2>&1; then curled=1; else unset curled; fi + + #* Check if "notify-send" command is available, if not, disable update notifier + if [[ -n $curled ]] && command -v notify-send >/dev/null 2>&1; then notifier=1; else unset notifier; fi + + #* Check if "iostat" command is available, if not, disable disk io stat collection + if command -v iostat >/dev/null 2>&1; then has_iostat=1; else unset has_iostat; fi + + #* Get number of cores and cpu threads + print -bg "#00" -fg "#30ff50" -r 1 -t "√" + print -m $(( (tty_height/2-3)+stx++ )) 0 -bg "#00" -fg "#cc" -c -b "Checking cpu..." + get_cpu_info + + #* Set graph resolution + graph[hires]="${hires_graphs}" + + #* Get processor BCLK + local param_var + if [[ $use_psutil == false ]] && [[ -e /usr/include/asm-generic/param.h ]]; then + param_var="$(/dev/null)" -k "version=" -r "[^0-9.]"; then unset git_version; fi + fi + + #* Add update notification to banner if new version is available + local banner_out_up + print -v banner_out_up -rs -fg "#cc" -b "← esc" + if [[ -n $git_version && $git_version != "$version" ]]; then + print -v banner_out_up -rs -fg "#80cc80" -r 15 "[${git_version} available!]" -r $((9-${#git_version})) + if [[ -n $notifier ]]; then + notify-send -u normal\ + "furtop Update!" "New version of furtop available\!\nCurrent version: ${version}\n\New version: ${git_version}\nDownload at github.com/aristocratos/furtop"\ + -i face-glasses -t 10000 + fi + else + print -v banner_out_up -r 37 + fi + print -v banner_out_up -fg "#cc" -i -b "Version: ${version}" -rs + banner+=("${banner_out_up}") + + #* Get theme and set colors + print -bg "#00" -fg "#30ff50" -r 1 -t "√" + print -m $(( (tty_height/2-3)+stx++ )) 0 -bg "#00" -fg "#cc" -c -b "Generating colors for theme..." + color_init_ + + #* Set up internals for quick processes sorting switching + for((i=0;i<${#sorting[@]};i++)); do + if [[ ${sorting[i]} == "${proc_sorting}" ]]; then + proc[sorting_int]=$i + break + fi + done + if [[ -z ${proc[sorting_int]} ]]; then + proc[sorting_int]=0 + proc_sorting="${sorting[0]}" + fi + + if [[ ${proc_reversed} == true ]]; then + proc[reverse]="+" + else + unset 'proc[reverse]' + fi + + if [[ ${proc_tree} == true ]]; then + proc[tree]="+" + else + unset 'proc[tree]' + fi + + #* Call init for processes data collection + print -bg "#00" -fg "#30ff50" -r 1 -t "√" + print -m $(( (tty_height/2-3)+stx++ )) 0 -bg "#00" -fg "#cc" -c -b "Running process collection init..." + proc[selected]=0 + proc[start]=1 + collect_processes init + + #* Draw first screen + print -bg "#00" -fg "#30ff50" -r 1 -t "√" + print -m $(( (tty_height/2-3)+stx++ )) 0 -bg "#00" -fg "#cc" -c -b "Drawing screen..." + + draw_bg quiet + get_ms timestamp_start + + for task in processes cpu mem net; do + collect_${task} + draw_${task} + done + last_screen="${draw_out}" + + print -bg "#00" -fg "#30ff50" -r 1 -t "√" -rs + sleep 0.5 + + draw_clock + echo -en "${clear_screen}${draw_out}${proc_out}${clock_out}" + resized=0 + unset draw_out +} + +color_init_() { #? Check for theme file and set colors + local main_bg="" main_fg="#ffccbb" title="#aabbbb" hi_fg="#aa77bb" inactive_fg="#40" cpu_box="#555566" mem_box="#336688" net_box="#665588" proc_box="#664488" proc_misc="#aa77bb" selected_bg="#ffffff" selected_fg="#775577" + local temp_start="#aaaadd" temp_mid="#88bbcc" temp_end="#ffeeff" cpu_start="#aaaadd" cpu_mid="#f2e266" cpu_end="#fa1e1e" div_line="#30" + local free_start="#aaaadd" free_mid="#88bbcc" free_end="#ffeeff" cached_start="#aaaadd" cached_mid="#88bbcc" cached_end="#ffeeff" available_start="#aaaadd" available_mid="#88bbcc" available_end="#ffeeff" + local used_start="#aaaadd" used_mid="#88bbcc" used_end="#ffeeff" download_start="#aaaadd" download_mid="#88bbcc" download_end="#ffeeff" upload_start="#aaaadd" upload_mid="#88bbcc" upload_end="#ffeeff" + local hex2rgb color_name array_name this_color main_fg_dec sourced theme_unset + local -i i y + local -A rgb + local -a dec_test + local -a convert_color=("main_bg" "temp_start" "temp_mid" "temp_end" "cpu_start" "cpu_mid" "cpu_end" "upload_start" "upload_mid" "upload_end" "download_start" "download_mid" "download_end" "used_start" "used_mid" "used_end" "available_start" "available_mid" "available_end" "cached_start" "cached_mid" "cached_end" "free_start" "free_mid" "free_end" "proc_misc" "main_fg_dec") + local -a set_color=("main_fg" "title" "hi_fg" "div_line" "inactive_fg" "selected_fg" "selected_bg" "cpu_box" "mem_box" "net_box" "proc_box") + for theme_unset in ${!theme[@]}; do + unset 'theme[${theme_unset}]' + done + + color_theme="furtop" + + main_fg_dec="${theme[main_fg]:-$main_fg}" + theme[main_fg_dec]="${main_fg_dec}" + + #* Convert colors for graphs and meters from rgb hexadecimal to rgb decimal if needed + for color_name in ${convert_color[@]}; do + if [[ -n $sourced ]]; then hex2rgb="${theme[${color_name}]}" + else hex2rgb="${!color_name}"; fi + + hex2rgb=${hex2rgb//#/} + + if [[ ${#hex2rgb} == 6 ]] && is_hex "$hex2rgb"; then hex2rgb="$((${hex}${hex2rgb:0:2})) $((${hex}${hex2rgb:2:2})) $((${hex}${hex2rgb:4:2}))" + elif [[ ${#hex2rgb} == 2 ]] && is_hex "$hex2rgb"; then hex2rgb="$((${hex}${hex2rgb:0:2})) $((${hex}${hex2rgb:0:2})) $((${hex}${hex2rgb:0:2}))" + else + dec_test=(${hex2rgb}) + if [[ ${#dec_test[@]} -eq 3 ]] && is_int "${dec_test[@]}"; then hex2rgb="${dec_test[*]}" + else unset hex2rgb; fi + fi + + theme[${color_name}]="${hex2rgb}" + done + + #* Set background color if set, otherwise use terminal default + if [[ -n ${theme[main_bg]} ]]; then theme[main_bg_dec]="${theme[main_bg]}"; theme[main_bg]=";48;2;${theme[main_bg]// /;}"; fi + + #* Set colors from theme file if found and valid hexadecimal or integers, otherwise use default values + for color_name in "${set_color[@]}"; do + if [[ -z ${theme[$color_name]} ]] || ! is_hex "${theme[$color_name]}" && ! is_int "${theme[$color_name]}"; then theme[${color_name}]="${!color_name}"; fi + done + + box[cpu_color]="${theme[cpu_box]}" + box[mem_color]="${theme[mem_box]}" + box[net_color]="${theme[net_box]}" + box[processes_color]="${theme[proc_box]}" + + #* Create color arrays from one, two or three color gradient, 100 values in each + for array_name in "temp" "cpu" "upload" "download" "used" "available" "cached" "free"; do + local -n color_array="color_${array_name}_graph" + local -a rgb_start=(${theme[${array_name}_start]}) rgb_mid=(${theme[${array_name}_mid]}) rgb_end=(${theme[${array_name}_end]}) + local pf_calc middle=1 + + rgb[red]=${rgb_start[0]}; rgb[green]=${rgb_start[1]}; rgb[blue]=${rgb_start[2]} + + if [[ -z ${rgb_mid[*]} ]] && ((rgb_end[0]+rgb_end[1]+rgb_end[2]>rgb_start[0]+rgb_start[1]+rgb_start[2])); then + rgb_mid=( $(( rgb_start[0]+( (rgb_end[0]-rgb_start[0])/2) )) $((rgb_start[1]+( (rgb_end[1]-rgb_start[1])/2) )) $((rgb_start[2]+( (rgb_end[2]-rgb_start[2])/2) )) ) + elif [[ -z ${rgb_mid[*]} ]]; then + rgb_mid=( $(( rgb_end[0]+( (rgb_start[0]-rgb_end[0])/2) )) $(( rgb_end[1]+( (rgb_start[1]-rgb_end[1])/2) )) $(( rgb_end[2]+( (rgb_start[2]-rgb_end[2])/2) )) ) + fi + + for((i=0;i<=100;i++,y=0)); do + + if [[ -n ${rgb_end[*]} ]]; then + for this_color in "red" "green" "blue"; do + if ((i==50)); then rgb_start[y]=${rgb[$this_color]}; fi + + if ((middle==1 & rgb[$this_color]rgb_mid[y])); then + printf -v pf_calc "%.0f" "-$(( i*( (rgb_start[y]-rgb_mid[y])*100/50*100) ))e-4" + + elif ((middle==0 & rgb[$this_color]rgb_end[y])); then + printf -v pf_calc "%.0f" "-$(( (i-50)*( (rgb_start[y]-rgb_end[y])*100/50*100) ))e-4" + + else + pf_calc=0 + fi + + rgb[$this_color]=$((rgb_start[y]+pf_calc)) + if ((rgb[$this_color]<0)); then rgb[$this_color]=0 + elif ((rgb[$this_color]>255)); then rgb[$this_color]=255; fi + + y+=1 + if ((i==49 & y==3 & middle==1)); then middle=0; fi + done + fi + color_array[i]="${rgb[red]} ${rgb[green]} ${rgb[blue]}" + done + + done +} + +quit_() { #? Clean exit + #* Restore terminal options and screen + if [[ $use_psutil == true && $2 != "psutil" ]]; then + py_command quit + sleep 0.1 + rm -rf "${pytmpdir}" + fi + echo -en "${clear_screen}${normal_screen}${show_cursor}" + ${stty} "${saved_stty}" + echo -en "\033]0;\a" + + #* Save any changed values to config file + if [[ $config_file != "/dev/null" ]]; then + save_config "${save_array[@]}" + fi + + if [[ $1 == "restart" ]]; then exec "$(${realpath} "$0")"; fi + + exit ${1:-0} +} + +sleep_() { #? Restore terminal options, stop and send to background if caught SIGTSTP (ctrl+z) + echo -en "${clear_screen}${normal_screen}${show_cursor}" + ${stty} "${saved_stty}" + echo -en "\033]0;\a" + + if [[ $use_psutil == true ]]; then + if ((failed_pipes>1)); then ((failed_pipes--)); fi + py_command quit + failed_pipe=1 + wait ${pycoproc_PID} + fi + + ${kill} -s SIGSTOP $$ +} + +resume_() { #? Set terminal options and resume if caught SIGCONT ('fg' from terminal) + sleepy=0 + echo -en "${alt_screen}${hide_cursor}${clear_screen}" + echo -en "\033]0;${TERMINAL_TITLE} furtop\a" + ${stty} -echo + + if [[ -n $pause_screen ]]; then + echo -en "$pause_screen" + else + echo -en "${boxes_out}${proc_det}${last_screen}${mem_out}${proc_misc}${proc_misc2}${update_string}${clock_out}" + fi +} + +traperr() { #? Function for reporting error line numbers + local match len trap_muted err="${BASH_LINENO[0]}" + + len=$((${#trace_array[@]})) + if ((len-->=1)); then + while ((len>=${#trace_array[@]}-2)); do + if [[ $err == "${trace_array[$((len--))]}" ]]; then ((++match)) ; fi + done + if ((match==2 & len != -2)); then return + elif ((match>=1)); then trap_muted="(MUTED!)" + fi + fi + if ((len>100)); then unset 'trace_array[@]'; fi + trace_array+=("$err") + printf "%(%X)T ERROR: On line %s %s\n" -1 "$err" "$trap_muted" >> "${config_dir}/error.log" + +} + +resized() { #? Get new terminal size if terminal is resized + resized=1 + unset winches + while ((++winches<5)); do + read tty_height tty_width < <(${stty} size) + if (($tty_width<80 | $tty_height<24)); then + size_error_msg + winches=0 + else + echo -en "${clear_screen}" + create_box -w 30 -h 3 -c 1 -l 1 -lc "#EE2020" -title "resizing" + print -jc 28 -fg ${theme[title]} "New size: ${tty_width}x${tty_height}" + ${sleep} 0.2 + if [[ $(${stty} size) != "$tty_height $tty_width" ]]; then winches=0; fi + fi + done +} + +size_error_msg() { #? Shows error message if terminal size is below 80x25 + local width=$tty_width + local height=$tty_height + echo -en "${clear_screen}" + create_box -full -lc "#EE2020" -title "resize window" + print -rs -m $((tty_height/2-1)) 2 -fg ${theme[title]} -c -l 11 "Current size: " -bg "#00" -fg "#dd2020" -d 1 -c "${tty_width}x${tty_height}" -rs + print -d 1 -fg ${theme[title]} -c -l 15 "Need to be atleast:" -bg "#00" -fg "#30dd50" -d 1 -c "80x24" -rs + while [[ $(${stty} size) == "$tty_height $tty_width" ]]; do ${sleep} 0.2; if [[ -n $quitting ]]; then quit_; fi ; done +} + +draw_banner() { #? Draw banner, usage: draw_banner [output variable] + local y letter b_color x_color xpos ypos=$1 banner_out + if [[ -n $2 ]]; then local -n banner_out=$2; fi + xpos=$(( (tty_width/2)-(banner_width/2) )) + + for banner_line in "${banner[@]}"; do + print -v banner_out -rs -move $((ypos+++y)) $xpos -t "${banner_line}" + done + + if [[ -z $2 ]]; then echo -en "${banner_out}"; fi +} + +create_config() { #? Creates a new config file with default values from above + local c_line c_read this_file + this_file="$(${realpath} "$0")" + echo "#? Config file for furtop v. ${version}" > "$config_file" + while IFS= read -r c_line; do + if [[ $c_line =~ aaz_config() ]]; then break + elif [[ $c_read == "1" ]]; then echo "$c_line" >> "$config_file" + elif [[ $c_line =~ aaa_config() ]]; then c_read=1; fi + done < "$this_file" +} + +save_config() { #? Saves variables to config file if not same, usage: save_config "var1" ["var2"] ["var3"]... + if [[ -z $1 || $config_file == "/dev/null" ]]; then return; fi + local var tmp_conf tmp_value quote original new + tmp_conf="$(<"$config_file")" + for var in "$@"; do + if [[ ${tmp_conf} =~ ${var} ]]; then + get_value -v "tmp_value" -sv "tmp_conf" -k "${var}=" + if [[ ${tmp_value//\"/} != "${!var}" ]]; then + original="${var}=${tmp_value}" + new="${var}=\"${!var}\"" + original="${original//'/'/'\/'}" + new="${new//'/'/'\/'}" + ${sed} -i "s/${original}/${new}/" "${config_file}" + fi + else + echo "${var}=\"${!var}\"" >> "$config_file" + fi + done +} + +set_font() { #? Take a string and generate a string of unicode characters of given font, usage; set_font "font-name [bold] [italic]" "string" + local i letter letter_hex new_hex add_hex start font="$1" string_in="$2" string_out hex="16#" + if [[ -z $font || -z $string_in ]]; then return; fi + case "$font" in + "sans-serif") lower_start="1D5BA"; upper_start="1D5A0"; digit_start="1D7E2";; + "sans-serif bold") lower_start="1D5EE"; upper_start="1D5D4"; digit_start="1D7EC";; + "sans-serif italic") lower_start="1D622"; upper_start="1D608"; digit_start="1D7E2";; + #"sans-serif bold italic") start="1D656"; upper_start="1D63C"; digit_start="1D7EC";; + "script") lower_start="1D4B6"; upper_start="1D49C"; digit_start="1D7E2";; + "script bold") lower_start="1D4EA"; upper_start="1D4D0"; digit_start="1D7EC";; + "fraktur") lower_start="1D51E"; upper_start="1D504"; digit_start="1D7E2";; + "fraktur bold") lower_start="1D586"; upper_start="1D56C"; digit_start="1D7EC";; + "monospace") lower_start="1D68A"; upper_start="1D670"; digit_start="1D7F6";; + "double-struck") lower_start="1D552"; upper_start="1D538"; digit_start="1D7D8";; + *) echo -n "${string_in}"; return;; + esac + + for((i=0;i<${#string_in};i++)); do + letter=${string_in:i:1} + if [[ $letter =~ [a-z] ]]; then #61 + printf -v letter_hex '%X\n' "'$letter" + printf -v add_hex '%X' "$((${hex}${letter_hex}-${hex}61))" + printf -v new_hex '%X' "$((${hex}${lower_start}+${hex}${add_hex}))" + string_out="${string_out}\U${new_hex}" + #if [[ $font =~ sans-serif && $letter =~ m|w ]]; then string_out="${string_out} "; fi + #\U205F + elif [[ $letter =~ [A-Z] ]]; then #41 + printf -v letter_hex '%X\n' "'$letter" + printf -v add_hex '%X' "$((${hex}${letter_hex}-${hex}41))" + printf -v new_hex '%X' "$((${hex}${upper_start}+${hex}${add_hex}))" + string_out="${string_out}\U${new_hex}" + #if [[ $font =~ sans-serif && $letter =~ M|W ]]; then string_out="${string_out} "; fi + elif [[ $letter =~ [0-9] ]]; then #30 + printf -v letter_hex '%X\n' "'$letter" + printf -v add_hex '%X' "$((${hex}${letter_hex}-${hex}30))" + printf -v new_hex '%X' "$((${hex}${digit_start}+${hex}${add_hex}))" + string_out="${string_out}\U${new_hex}" + else + string_out="${string_out} \e[1D${letter}" + fi + done + + echo -en "${string_out}" +} + +sort_array_int() { #? Copy and sort an array of integers from largest to smallest value, usage: sort_array_int "input array" "output array" + #* Return if given array has no values + if [[ -z ${!1} ]]; then return; fi + local start_n search_n tmp_array + + #* Create pointers to arrays + local -n in_arr="$1" + local -n out_arr="$2" + + #* Create local copy of array + local array=("${in_arr[@]}") + + #* Start sorting + for ((start_n=0;start_n<=${#array[@]}-1;++start_n)); do + for ((search_n=start_n+1;search_n<=${#array[@]}-1;++search_n)); do + if ((array[start_n] [-ps,-per-second] [-s,-start "1024 multiplier start"] [-v,-variable-output] + local value selector per_second unit_mult decimals out_var ext_var short sep=" " + local -a unit + until (($#==0)); do + case "$1" in + -b|-bit) unit=(bit Kib Mib Gib Tib Pib); unit_mult=8;; + -B|-Byte) unit=(Byte KiB MiB GiB TiB PiB); unit_mult=1;; + -ps|-per-second) per_second=1;; + -short) short=1; sep="";; + -s|-start) selector="$2"; shift;; + -v|-variable-output) local -n out_var="$2"; ext_var=1; shift;; + *) if is_int "$1"; then value=$1; break; fi;; + esac + shift + done + + if [[ -z $value || $value -lt 0 || -z $unit_mult ]]; then return; fi + + if ((per_second==1 & unit_mult==1)); then per_second="/s" + elif ((per_second==1)); then per_second="ps"; fi + + if ((value>0)); then + value=$((value*100*unit_mult)) + + until ((${#value}<6)); do + value=$((value>>10)) + if ((value<100)); then value=100; fi + ((++selector)) + done + + if ((${#value}<5 & ${#value}>=2 & selector>0)); then + decimals=$((5-${#value})) + value="${value::-2}.${value:(-${decimals})}" + elif ((${#value}>=2)); then + value="${value::-2}" + fi + fi + + if [[ -n $short ]]; then value="${value%.*}"; fi + + out_var="${value}${sep}${unit[$selector]::${short:-${#unit[$selector]}}}${per_second}" + if [[ -z $ext_var ]]; then echo -n "${out_var}"; fi +} + +get_cpu_info() { + local lscpu_var pyin + if [[ $use_psutil == true ]]; then + if [[ -z ${cpu[threads]} || -z ${cpu[cores]} ]]; then + py_command -v pyin "get_cpu_cores()" + read cpu[cores] cpu[threads] <<<"${pyin}" + fi + + else + if command -v lscpu >/dev/null 2>&1; then lscpu_var="$(lscpu)"; fi + if [[ -z ${cpu[threads]} || -z ${cpu[cores]} ]]; then + if ! get_value -v 'cpu[threads]' -sv "lscpu_var" -k "CPU(s):" -i || [[ ${cpu[threads]} == "0" ]]; then + cpu[threads]="$(nproc 2>/dev/null ||true)" + if [[ -z ${cpu[threads]} ]]; then cpu[threads]="1"; fi + cpu[cores]=${cpu[threads]} + else + get_value -v 'cpu[cores]' -sv "lscpu_var" -k "Core(s)" -i + fi + fi + fi + + if [[ $use_psutil == false && -z $custom_cpu_name ]]; then + if ! get_value -v 'cpu[model]' -sv "lscpu_var" -k "Model name:" -a -b -k "CPU" -mk -1; then + if ! get_value -v 'cpu[model]' -sv "lscpu_var" -k "Model name:" -r " "; then + cpu[model]="cpu" + fi + fi + elif [[ $use_psutil == true && -z $custom_cpu_name ]]; then + py_command -v cpu[model] "get_cpu_name()" + else + cpu[model]="${custom_cpu_name}" + fi +} + +get_value() { #? Get a value from a file, variable or array by searching for a non spaced "key name" on the same line + local match line_pos=1 int reg key all tmp_array input found input_line line_array line_val ext_var line_nr current_line match_key math removing ext_arr + local -a remove + until (($#==0)); do + until (($#==0)); do + case "$1" in + -k|-key) key="$2"; shift;; #? Key "string" on the same line as target value + -m|-match) match="$2"; shift;; #? If multiple matches on a line, match occurrence "x" + -mk|-match-key) match_key=$2; line_pos=0; shift;; #? Match in relation to key position, -1 for previous value, 1 for next value + -b|-break) shift; break;; #? Break up arguments for multiple searches + -a|-all) all=1;; #? Prints back found line including key + -l|-line) line_nr="$2"; shift;; #? Set target line if no key is available + -ss|-source-string) input="$2"; shift;; #? Argument string as source + -sf|-source-file) input="$(<"$2")"; shift;; #? File as source + -sv|-source-var) input="${!2}"; shift;; #? Variable as source + -sa|-source-array) local -n tmp_array=$2; input="${tmp_array[*]}"; shift;; #? Array as source + -fp|-floating-point) reg="[\-]?[0-9]*[.,][0-9]+"; match=1;; #? Match floating point value + -math) math="$2"; shift;; #? Perform math on a integer value, "x" represents value, only works if "integer" argument is given + -i|-integer) reg="[\-]?[0-9]+[.,]?[0-9]*"; int=1; match=1;; #? Match integer value or float and convert to int + -r|-remove) remove+=("$2"); shift;; #? Format output by removing entered regex, can be used multiple times + -v|-variable-out) local -n found="$2"; ext_var=1; shift;; #? Output to variable + -map|-map-array) local -n array_out="$2"; ext_var=1; ext_arr=1; shift;; #? Map output to array + esac + shift + done + + found="" + + if [[ -z $input ]]; then return 1; fi + if [[ -z $line_nr && -z $key ]]; then line_nr=1; fi + + while IFS='' read -r input_line; do + ((++current_line)) + if [[ -n $line_nr && $current_line -eq $line_nr || -z $line_nr && -n $key && ${input_line/${key}/} != "$input_line" ]]; then + if [[ -n $all ]]; then + found="${input_line}" + break + + elif [[ -z $match && -z $match_key && -z $reg ]]; then + found="${input_line/${key}/}" + break + + else + line_array=(${input_line/${key}/${key// /}}) + + fi + + for line_val in "${line_array[@]}"; do + if [[ -n $match_key && $line_val == "${key// /}" ]]; then + if ((match_key<0 & line_pos+match_key>=0)) || ((match_key>=0 & line_pos+match_key<${#line_array[@]})); then + found="${line_array[$((line_pos+match_key))]}" + break 2 + else + return 1 + fi + + elif [[ -n $match_key ]]; then + ((++line_pos)) + + elif [[ -n $reg && $line_val =~ ^${reg}$ || -z $reg && -n $match ]]; then + if ((line_pos==match)); then + found=${line_val} + break 2 + fi + ((++line_pos)) + fi + done + fi + done <<<"${input}" + + if [[ -z $found ]]; then return 1; fi + + if [[ -n ${remove[*]} ]]; then + for removing in "${remove[@]}"; do + found="${found//${removing}/}" + done + fi + + if [[ -n $int && $found =~ [.,] ]]; then + found="${found/,/.}" + printf -v found "%.0f" "${found}" + fi + + if [[ -n $math && -n $int ]]; then + math="${math//x/$found}" + found=$((${math})) + fi + + if (($#>0)); then + input="${found}" + unset key match match_key all reg found int 'remove[@]' current_line + line_pos=1 + fi + + done + + if [[ -z $ext_var ]]; then echo "${found}"; fi + if [[ -n $ext_arr ]]; then array_out=(${found}); fi +} + +get_themes() { + local file + theme_int=0 + themes=("Default") + for file in "${config_dir}/themes"/*.theme; do + file="${file##*/}" + if [[ ${file} != "*.theme" ]]; then themes+=("themes/${file%.theme}"); fi + if [[ ${themes[-1]} == "${color_theme}" ]]; then theme_int=${#themes[@]}-1; fi + done + for file in "${config_dir}/user_themes"/*.theme; do + file="${file##*/}" + if [[ ${file} != "*.theme" ]]; then themes+=("user_themes/${file%.theme}"); fi + if [[ ${themes[-1]} == "${color_theme}" ]]; then theme_int=${#themes[@]}-1; fi + done +} + +get_net_device() { #? Check for internet connection, name of default network device and create list of all devices + if [[ $use_psutil == true ]]; then get_net_device_psutil; return; fi + local -a netdev + local ndline + if ! get_value -v 'net[device]' -ss "$(ip route get 1.1.1.1 2>/dev/null)" -k "dev" -mk 1; then + net[no_device]=1 + else + unset 'net[no_device]' 'nic_list[@]' nic_int + readarray -t netdev + #? Optional arguments: [-p, -place ] [-w, -width ] [-f, -fill-empty] + #? [-c, -color "array-name"] [-i, -invert-color] [-v, -variable "variable-name"] + if [[ -z $1 ]]; then return; fi + local val width colors color block="■" i fill_empty col line var ext_var out meter_var print_var invert bg_color=${theme[inactive_fg]} + + #* Argument parsing + until (($#==0)); do + case $1 in + -p|-place) if is_int "${@:2:2}"; then line=$2; col=$3; shift 2; fi;; #? Placement for meter + -w|-width) width=$2; shift;; #? Width of meter in columns + -c|-color) local -n colors=$2; shift;; #? Name of an array containing colors from index 0-100 + -i|-invert) invert=1;; #? Invert meter + -f|-fill-empty) fill_empty=1;; #? Fill unused space with dark blocks + -v|-variable) local -n meter_var=$2; ext_var=1; shift;; #? Output meter to a variable + *) if is_int "$1"; then val=$1; fi;; + esac + shift + done + + if [[ -z $val ]]; then return; fi + + #* Set default width if not given + width=${width:-10} + + #* If no color array was given, create a simple greyscale array + if [[ -z $colors ]]; then + for ((i=0,ic=50;i<=100;i++,ic=ic+2)); do + colors[i]="${ic} ${ic} ${ic}" + done + fi + + #* Create the meter + meter_var="" + if [[ -n $line && -n $col ]]; then print -v meter_var -rs -m $line $col + else print -v meter_var -rs; fi + + if [[ -n $invert ]]; then print -v meter_var -r $((width+1)); fi + for((i=1;i<=width;i++)); do + if [[ -n $invert ]]; then print -v meter_var -l 2; fi + + if ((val>=i*100/width)); then + print -v meter_var -fg ${colors[$((i*100/width))]} -t "${block}" + elif ((fill_empty==1)); then + if [[ -n $invert ]]; then print -v meter_var -l $((width-i)); fi + print -v meter_var -fg $bg_color -rp $((1+width-i)) -t "${block}"; break + else + if [[ -n $invert ]]; then break; print -v meter_var -l $((1+width-i)) + else print -v meter_var -r $((1+width-i)); break; fi + fi + done + if [[ -z $ext_var ]]; then echo -en "${meter_var}"; fi +} + +create_graph() { #? Create a graph from an array of percentage values, usage; create_graph + #? Create a graph from an array of non percentage values: create_graph <-max "max value"> + #? Add a value to existing graph; create_graph [-i, -invert] [-max "max value"] -add-value "graph_array" + #? Add last value from an array to existing graph; create_graph [-i, -invert] [-max "max value"] -add-last "graph_array" "value-array" + #? Options: < -d, -dimensions > [-i, -invert] [-n, -no-guide] [-c, -color "array-name"] [-o, -output-array "variable-name"] + if [[ -z $1 ]]; then return; fi + if [[ ${graph[hires]} == true ]]; then create_graph_hires "$@"; return; fi + + local val col s_col line s_line height s_height width s_width colors color i var ext_var out side_num side_nums=1 add add_array invert no_guide max + local -a graph_array input_array + + #* Argument parsing + until (($#==0)); do + case $1 in + -d|-dimensions) if is_int "${@:2:4}"; then line=$2; col=$3; height=$4; width=$5; shift 4; fi;; #? Graph dimensions + -c|-color) local -n colors=$2; shift;; #? Name of an array containing colors from index 0-100 + -o|-output-array) local -n output_array=$2; ext_var=1; shift;; #? Output meter to an array + -add-value) if is_int "$3"; then local -n output_array=$2; add=$3; break; else return; fi;; #? Add a value to existing graph + -add-last) local -n output_array=$2; local -n add_array=$3; add=${add_array[-1]}; break;; #? Add last value from array to existing graph + -i|-invert) invert=1;; #? Invert graph, drawing from top to bottom + -n|-no-guide) no_guide=1;; #? Don't print side and bottom guide lines + -max) if is_int "$2"; then max=$2; shift; fi;; #? Needed max value for non percentage arrays + *) local -n tmp_in_array=$1; input_array=("${tmp_in_array[@]}");; + esac + shift + done + + if [[ -z $no_guide ]]; then + ((--height)) + else + if [[ -n $invert ]]; then ((line--)); fi + fi + + + if ((width<3)); then width=3; fi + if ((height<1)); then height=1; fi + + + #* If argument "add" was passed check for existing graph and make room for new value(s) + local add_start add_end + if [[ -n $add ]]; then + local cut_left search + if [[ -n ${input_array[0]} ]]; then return; fi + if [[ -n $output_array ]]; then + graph_array=("${output_array[@]}") + if [[ -z ${graph_array[0]} ]]; then return; fi + else + return + fi + height=$((${#graph_array[@]}-1)) + input_array[0]=${add} + + #* Remove last value in current graph + + for ((i=0;i=0;i--)); do + g_index+=($i) + done + + else + for((i=0;i<=height;i++)); do + g_index+=($i) + done + fi + + if [[ -n $no_guide ]]; then unset normal_vals + elif [[ -n $invert ]]; then g_char=(" ⡇" " ⡤" "⠤") + fi + + #* Set up graph array print side numbers and lines + print -v graph_array[0] -rs + print -v graph_array[0] -m $((line+g_index[0])) ${col} ${normal_vals:+-jr 3 -fg "#ee" -b -t "${side_num[0]}" -rs -fg ${theme[main_fg]} -t "${g_char[0]}"} -fg ${colors[100]} + for((i=1;i=max)); then + input_array[i]=100 + else + input_array[i]=$((input_array[i]*100/max)) + fi + done + fi + + until ((y==done_val)); do + + #* Print spaces to right-justify graph if number of values is less than graph width + if [[ -z $add ]] && ((value_width3)); then input_array[x]=0; fi + + #* Print empty space if current value is less than percentage for current line + while ((x0)); then + print -v graph_array[y] -rp ${count} -t " " + count=0 + fi + + #* Print current value in percent relative to graph size if current value is less than line percentage but greater than next line percentage + while ((x=next_value)); do + print -v graph_array[y] -t "${graph_symbol[${invert:+-}$(( (input_array[x]*virt_height/100)-next_value ))]}" + ((++x)) + done + + #* Print full block if current value is greater than percentage for current line + while ((x=cur_value)); do + ((++count)) + ((++x)) + done + if ((count>0)); then + print -v graph_array[y] -rp ${count} -t "${graph_symbol[10]}" + count=0 + fi + done + + if [[ -n $invert ]]; then + ((y--)) || true + else + ((++y)) + fi + done + + #* Echo out graph if no argument for a output array was given + if [[ -z $ext_var && -z $add ]]; then echo -en "${graph_array[*]}" + else output_array=("${graph_array[@]}"); fi +} + +create_mini_graph() { #? Create a one line high graph from an array of percentage values, usage; create_mini_graph + #? Add a value to existing graph; create_mini_graph [-i, -invert] [-nc, -no-color] [-c, -color "array-name"] -add-value "graph_variable" + #? Add last value from an array to existing graph; create_mini_graph [-i, -invert] [-nc, -no-color] [-c, -color "array-name"] -add-last "graph_variable" "value-array" + #? Options: [-w, -width ] [-i, -invert] [-nc, -no-color] [-c, -color "array-name"] [-o, -output-variable "variable-name"] + if [[ -z $1 ]]; then return; fi + + if [[ ${graph[hires]} == true ]]; then create_mini_graph_hires "$@"; return; fi + + local val col s_col line s_line height s_height width s_width colors color i var ext_var out side_num side_nums=1 add invert no_guide graph_var no_color color_value + + #* Argument parsing + until (($#==0)); do + case $1 in + -w|-width) if is_int "$2"; then width=$2; shift; fi;; #? Graph width + -c|-color) local -n colors=$2; shift;; #? Name of an array containing colors from index 0-100 + -nc|-no-color) no_color=1;; #? Set no color + -o|-output-variable) local -n output_var=$2; ext_var=1; shift;; #? Output graph to a variable + -add-value) if is_int "$3"; then local -n output_var=$2; add=$3; break; else return; fi;; #? Add a value to existing graph + -add-last) local -n output_var=$2 add_array=$3; add="${add_array[-1]}"; break;; #? Add last value from array to existing graph + -i|-invert) invert=1;; #? Invert graph, drawing from top to bottom + *) local -n input_array=$1;; + esac + shift + done + + if ((width<1)); then width=1; fi + + #* If argument "add" was passed check for existing graph and make room for new value(s) + local add_start add_end + if [[ -n $add ]]; then + local cut_left search + #if [[ -n ${input_array[0]} ]]; then return; fi + if [[ -n $output_var ]]; then + graph_var="${output_var}" + if [[ -z ${graph_var} ]]; then return; fi + else + return + fi + + declare -a input_array + input_array[0]=${add} + + #* Remove last value in current graph + if [[ -n ${graph_var} && -z $no_color ]]; then + if [[ ${graph_var::5} == "\e[1C" ]]; then + graph_var="${graph_var#'\e[1C'}" + else + cut_left="${graph_var%%m*}" + search=$((${#cut_left}+1)) + graph_var="${graph_var:$((search+1))}" + fi + elif [[ -n ${graph_var} && -n $no_color ]]; then + if [[ ${graph_var::5} == "\e[1C" ]]; then + #cut_left="${graph_var%%C*}" + #search=$((${#cut_left}+1)) + #graph_var="${graph_var:$((search))}" + graph_var="${graph_var#'\e[1C'}" + else + graph_var="${graph_var:1}" + fi + fi + fi + + + #* If no color array was given, create a simple greyscale array + if [[ -z $colors && -z $no_color ]]; then + for ((i=0,ic=50;i<=100;i++,ic=ic+2)); do + colors[i]="${ic} ${ic} ${ic}" + done + fi + + + #* Create the graph + local value_width x=0 y a cur_value virt_height=$((height*10)) offset=0 org_value + if [[ -n $add ]]; then + value_width=1 + elif ((${#input_array[@]}<=width)); then + value_width=${#input_array[@]}; + else + value_width=${width} + offset=$((${#input_array[@]}-width)) + fi + + #* Print spaces to right-justify graph if number of values is less than graph width + if [[ -z $add && -z $no_color ]] && ((value_width=100)); then cur_value=10; org_value=100 + elif [[ ${#org_value} -gt 1 && ${org_value:(-1)} -ge 5 ]]; then cur_value=$((${org_value::1}+1)) + elif [[ ${#org_value} -gt 1 && ${org_value:(-1)} -lt 5 ]]; then cur_value=$((${org_value::1})) + elif [[ ${org_value:(-1)} -ge 5 ]]; then cur_value=1 + else cur_value=0 + fi + if [[ -z $no_color ]]; then + color="-fg ${colors[$org_value]} " + else + color="" + fi + + if [[ $cur_value == 0 ]]; then + print -v graph_var -t "\e[1C" + else + print -v graph_var ${color}-t "${graph_symbol[${invert:+-}$cur_value]}" + fi + ((++x)) + done + + #* Echo out graph if no argument for a output array was given + if [[ -z $ext_var && -z $add ]]; then echo -en "${graph_var}" + else output_var="${graph_var}"; fi +} + +create_graph_hires() { #? Create a graph from an array of percentage values, usage; create_graph + #? Create a graph from an array of non percentage values: create_graph <-max "max value"> + #? Add a value to existing graph; create_graph [-i, -invert] [-max "max value"] -add-value "graph_array" + #? Add last value from an array to existing graph; create_graph [-i, -invert] [-max "max value"] -add-last "graph_array" "value-array" + #? Options: < -d, -dimensions > [-i, -invert] [-n, -no-guide] [-c, -color "array-name"] [-o, -output-array "variable-name"] + if [[ -z $1 ]]; then return; fi + local val col s_col line s_line height s_height width s_width colors color var ext_var out side_num side_nums=1 add add_array invert no_guide max graph_name offset=0 last_val + local -a input_array + local -i i + + #* Argument parsing + until (($#==0)); do + case $1 in + -d|-dimensions) if is_int "${@:2:4}"; then line=$2; col=$3; height=$4; width=$5; shift 4; fi;; #? Graph dimensions + -c|-color) local -n colors=$2; shift;; #? Name of an array containing colors from index 0-100 + -o|-output-array) local -n output_array=$2; graph_name=$2; ext_var=1; shift;; #? Output meter to an array + -add-value) if is_int "$3"; then local -n output_array=$2; graph_name=$2; add=$3; break; else return; fi;; #? Add a value to existing graph + -add-last) local -n output_array=$2; graph_name=$2; local -n add_array=$3; add=${add_array[-1]}; break;; #? Add last value from array to existing graph + -i|-invert) invert=1;; #? Invert graph, drawing from top to bottom + -n|-no-guide) no_guide=1;; #? Don't print side and bottom guide lines + -max) if is_int "$2"; then max=$2; shift; fi;; #? Needed max value for non percentage arrays + *) local -n tmp_in_array="$1"; input_array=("${tmp_in_array[@]}");; + esac + shift + done + + local -n last_val="graph[${graph_name}_last_val]" + local -n last_type="graph[${graph_name}_last_type]" + + + if [[ -z $add ]]; then + last_type="even" + last_val=0 + local -n graph_array="${graph_name}_odd" + local -n graph_even="${graph_name}_even" + graph_even=("") + graph_array=("") + elif [[ ${last_type} == "even" ]]; then + local -n graph_array="${graph_name}_odd" + last_type="odd" + elif [[ ${last_type} == "odd" ]]; then + local -n graph_array="${graph_name}_even" + last_type="even" + fi + + if [[ -z $no_guide ]]; then ((--height)) + elif [[ -n $invert ]]; then ((line--)) + fi + + if ((width<3)); then width=3; fi + if ((height<1)); then height=1; fi + + + #* If argument "add" was passed check for existing graph and make room for new value(s) + local add_start add_end + if [[ -n $add ]]; then + local cut_left search + if [[ -n ${input_array[*]} || -z ${graph_array[0]} ]]; then return; fi + + height=$((${#graph_array[@]}-1)) + input_array=("${add}") + + #* Remove last value in current graph + + for ((i=0;i=0;i--)); do + g_index+=($i) + done + + else + for((i=0;i<=height;i++)); do + g_index+=($i) + done + fi + + if [[ -n $no_guide ]]; then unset normal_vals + elif [[ -n $invert ]]; then g_char=(" ⡇" " ⡤" "⠤") + fi + + #* Set up graph array print side numbers and lines + print -v graph_array[0] -rs -m $((line+g_index[0])) ${col} ${normal_vals:+-jr 3 -fg "#ee" -b -t "${side_num[0]}" -rs -fg ${theme[main_fg]} -t "${g_char[0]}"} -fg ${colors[100]} + for((i=1;i=max)); then + input_array[i]=100 + else + input_array[i]=$((input_array[i]*100/max)) + fi + done + if [[ -n $converted ]]; then + last_val=$((${last_val}*100/max)) + if ((${last_val}>100)); then last_val=100; fi + fi + fi + + if [[ -n $invert ]]; then local -n symbols=graph_symbol_down + else local -n symbols=graph_symbol_up + fi + + until ((y==done_val)); do + + next_line=$(( virt_height-((y+1)*4) )) + unset p_val + + #* Create graph by walking through all values for each line + for ((x=0;x<${#input_array[@]};x++)); do + c_val=${input_array[x]} + p_val=${p_val:-${last_val}} + cur_value="$((c_val*virt_height/100-next_line))" + prev_value=$((p_val*virt_height/100-next_line)) + + if ((cur_value<0)); then cur_value=0 + elif ((cur_value>4)); then cur_value=4; fi + if ((prev_value<0)); then prev_value=0 + elif ((prev_value>4)); then prev_value=4; fi + + if [[ -z $add ]] && ((x==0)); then + print -v graph_even[y] -t "${symbols[${prev_value}_${cur_value}]}" + print -v graph_array[y] -t "${symbols[0_${prev_value}]}" + elif [[ -z $add ]] && ! ((x%2)); then + print -v graph_even[y] -t "${symbols[${prev_value}_${cur_value}]}" + else + print -v graph_array[y] -t "${symbols[${prev_value}_${cur_value}]}" + fi + + if [[ -z $add ]]; then p_val=${input_array[x]}; else unset p_val; fi + + done + + if [[ -n $invert ]]; then + ((y--)) || true + else + ((++y)) + fi + + done + + if [[ -z $add && ${last_type} == "even" ]]; then + declare -n graph_array="${graph_name}_even" + fi + + last_val=$c_val + + output_array=("${graph_array[@]}") +} + + +create_mini_graph_hires() { #? Create a one line high graph from an array of percentage values, usage; create_mini_graph + #? Add a value to existing graph; create_mini_graph [-i, -invert] [-nc, -no-color] [-c, -color "array-name"] -add-value "graph_variable" + #? Add last value from an array to existing graph; create_mini_graph [-i, -invert] [-nc, -no-color] [-c, -color "array-name"] -add-last "graph_variable" "value-array" + #? Options: [-w, -width ] [-i, -invert] [-nc, -no-color] [-c, -color "array-name"] [-o, -output-variable "variable-name"] + if [[ -z $1 ]]; then return; fi + local val col s_col line s_line height s_height width s_width colors color var ext_var out side_num side_nums=1 add invert no_guide graph_var no_color color_value graph_name + local -a input_array + local -i i + + #* Argument parsing + until (($#==0)); do + case $1 in + -w|-width) if is_int "$2"; then width=$2; shift; fi;; #? Graph width + -c|-color) local -n colors=$2; shift;; #? Name of an array containing colors from index 0-100 + -nc|-no-color) no_color=1;; #? Set no color + -o|-output-variable) local -n output_var=$2; graph_name=$2; ext_var=1; shift;; #? Output graph to a variable + -add-value) if is_int "$3"; then local -n output_var=$2; graph_name=$2; add=$3; break; else return; fi;; #? Add a value to existing graph + -add-last) local -n output_var=$2; local -n add_array=$3; graph_name=$2; add="${add_array[-1]:-0}"; break;; #? Add last value from array to existing graph + -i|-invert) invert=1;; #? Invert graph, drawing from top to bottom + *) local -n tmp_in_arr=$1; input_array=("${tmp_in_arr[@]}");; + esac + shift + done + + local -n last_val="${graph_name}_last_val" + local -n last_type="${graph_name}_last_type" + + if [[ -z $add ]]; then + last_type="even" + last_val=0 + local -n graph_var="${graph_name}_odd" + local -n graph_other="${graph_name}_even" + graph_var=""; graph_other="" + elif [[ ${last_type} == "even" ]]; then + local -n graph_var="${graph_name}_odd" + last_type="odd" + elif [[ ${last_type} == "odd" ]]; then + local -n graph_var="${graph_name}_even" + last_type="even" + fi + + if ((width<1)); then width=1; fi + + #* If argument "add" was passed check for existing graph and make room for new value(s) + local add_start add_end + if [[ -n $add ]]; then + local cut_left search + input_array[0]=${add} + + #* Remove last value in current graph + if [[ -n ${graph_var} && -z $no_color ]]; then + if [[ ${graph_var::5} == '\e[1C' ]]; then + graph_var="${graph_var#'\e[1C'}" + else + cut_left="${graph_var%m*}" + search=$((${#cut_left}+1)) + graph_var="${graph_var::$search}${graph_var:$((search+1))}" + fi + elif [[ -n ${graph_var} && -n $no_color ]]; then + if [[ ${graph_var::5} == "\e[1C" ]]; then + #cut_left="${graph_var%%C*}" + #search=$((${#cut_left}+1)) + #graph_var="${graph_var:$((search))}" + graph_var="${graph_var#'\e[1C'}" + else + graph_var="${graph_var:1}" + fi + fi + fi + + + #* If no color array was given, create a simple greyscale array + if [[ -z $colors && -z $no_color ]]; then + for ((i=0,ic=50;i<=100;i++,ic=ic+2)); do + colors[i]="${ic} ${ic} ${ic}" + done + fi + + + #* Create the graph + local value_width x=0 y a cur_value prev_value p_val c_val acolor jump odd offset=0 + if [[ -n $add ]]; then + value_width=1 + elif ((${#input_array[@]}<=width*2)); then + value_width=$((${#input_array[@]}*2)) + else + value_width=$((width*2)) + input_array=("${input_array[@]:(-${value_width})}") + fi + + if [[ -z $add ]] && ! ((${#input_array[@]}%2)); then last_val=${input_array[0]}; input_array=("${input_array[@]:1}"); fi + + #* Print spaces to right-justify graph if number of values is less than graph width + if [[ -z $add ]] && ((${#input_array[@]}/2=85)); then cur_value=4 + elif ((c_val>=60)); then cur_value=3 + elif ((c_val>=30)); then cur_value=2 + elif ((c_val>=10)); then cur_value=1 + elif ((c_val<10)); then cur_value=0; fi + + if ((p_val>=85)); then prev_value=4 + elif ((p_val>=60)); then prev_value=3 + elif ((p_val>=30)); then prev_value=2 + elif ((p_val>=10)); then prev_value=1 + elif ((p_val<10)); then prev_value=0; fi + + if [[ -z $no_color ]]; then + if ((c_val>p_val)); then acolor=$((c_val-p_val)) + else acolor=$((p_val-c_val)); fi + if ((acolor>100)); then acolor=100; elif ((acolor<0)); then acolor=0; fi + color="-fg ${colors[${acolor:-0}]} " + else + unset color + fi + + if ((cur_value==0 & prev_value==0)); then jump="\e[1C"; else unset jump; fi + + if [[ -z $add ]] && ((i==0)); then + print -v graph_other ${color}-t "${jump:-${symbols[${prev_value}_${cur_value}]}}" + print -v graph_var ${color}-t "${jump:-${symbols[0_${prev_value}]}}" + elif [[ -z $add ]] && ((i%2)); then + print -v graph_other ${color}-t "${jump:-${symbols[${prev_value}_${cur_value}]}}" + else + print -v graph_var ${color}-t "${jump:-${symbols[${prev_value}_${cur_value}]}}" + fi + + if [[ -z $add ]]; then p_val=$c_val; else unset p_val; fi + done + + #if [[ -z $add ]]; then + # declare -n graph_var="${graph_name}_even" + # #echo "yup" >&2 + #fi + + last_val=$c_val + + output_var="${graph_var}" +} + +print() { #? Print text, set true-color foreground/background color, add effects, center text, move cursor, save cursor position and restore cursor postion + #? Effects: [-fg, -foreground | ] [-bg, -background | ] [-rs, -reset] [-/+b, -/+bold] [-/+da, -/+dark] + #? [-/+ul, -/+underline] [-/+i, -/+italic] [-/+bl, -/+blink] [-f, -font "sans-serif|script|fraktur|monospace|double-struck"] + #? Manipulation: [-m, -move ] [-l, -left ] [-r, -right ] [-u, -up ] [-d, -down ] [-c, -center] [-sc, -save] [-rc, -restore] + #? [-jl, -justify-left ] [-jr, -justify-right ] [-jc, -justify-center ] [-rp, -repeat ] + #? Text: [-v, -variable "variable-name"] [-stdin] [-t, -text "string"] ["string"] + + #* Return if no arguments is given + if [[ -z $1 ]]; then return; fi + + #* Just echo and return if only one argument and not a valid option + if [[ $# -eq 1 && ${1::1} != "-" ]]; then echo -en "$1"; return; fi + + local effect color add_command text text2 esc center clear fgc bgc fg_bg_div tmp tmp_len bold italic custom_font val var out ext_var hex="16#" + local justify_left justify_right justify_center repeat r_tmp trans + + + #* Loop function until we are out of arguments + until (($#==0)); do + + #* Argument parsing + until (($#==0)); do + case $1 in + -t|-text) text="$2"; shift 2; break;; #? String to print + -stdin) text="$( <0-255> <0-255>" + if [[ ${2::1} == "#" ]]; then + val=${2//#/} + if [[ ${#val} == 6 ]]; then fgc="\e[38;2;$((${hex}${val:0:2}));$((${hex}${val:2:2}));$((${hex}${val:4:2}))m"; shift + elif [[ ${#val} == 2 ]]; then fgc="\e[38;2;$((${hex}${val:0:2}));$((${hex}${val:0:2}));$((${hex}${val:0:2}))m"; shift + fi + elif is_int "${@:2:3}"; then fgc="\e[38;2;$2;$3;$4m"; shift 3 + fi + ;; + -bg|-background) #? Set text background color, accepts either 6 digit hexadecimal "#RRGGBB", 2 digit hex (greyscale) or decimal RGB "<0-255> <0-255> <0-255>" + if [[ ${2::1} == "#" ]]; then + val=${2//#/} + if [[ ${#val} == 6 ]]; then bgc="\e[48;2;$((${hex}${val:0:2}));$((${hex}${val:2:2}));$((${hex}${val:4:2}))m"; shift + elif [[ ${#val} == 2 ]]; then bgc="\e[48;2;$((${hex}${val:0:2}));$((${hex}${val:0:2}));$((${hex}${val:0:2}))m"; shift + fi + elif is_int "${@:2:3}"; then bgc="\e[48;2;$2;$3;$4m"; shift 3 + fi + ;; + -c|-center) center=1;; #? Center text horizontally on screen + -rs|-reset) effect="0${effect}${theme[main_bg]}";; #? Reset text colors and effects + -b|-bold) effect="${effect}${effect:+;}1"; bold=1;; #? Enable bold text + +b|+bold) effect="${effect}${effect:+;}21"; bold=0;; #? Disable bold text + -da|-dark) effect="${effect}${effect:+;}2";; #? Enable dark text + +da|+dark) effect="${effect}${effect:+;}22";; #? Disable dark text + -i|-italic) effect="${effect}${effect:+;}3"; italic=1;; #? Enable italic text + +i|+italic) effect="${effect}${effect:+;}23"; italic=0;; #? Disable italic text + -ul|-underline) effect="${effect}${effect:+;}4";; #? Enable underlined text + +ul|+underline) effect="${effect}${effect:+;}24";; #? Disable underlined text + -bl|-blink) effect="${effect}${effect:+;}5";; #? Enable blinking text + +bl|+blink) effect="${effect}${effect:+;}25";; #? Disable blinking text + -f|-font) if [[ $2 =~ ^(sans-serif|script|fraktur|monospace|double-struck)$ ]]; then custom_font="$2"; shift; fi;; #? Set custom font + -m|-move) add_command="${add_command}\e[${2};${3}f"; shift 2;; #? Move to postion "LINE" "COLUMN" + -l|-left) add_command="${add_command}\e[${2}D"; shift;; #? Move left x columns + -r|-right) add_command="${add_command}\e[${2}C"; shift;; #? Move right x columns + -u|-up) add_command="${add_command}\e[${2}A"; shift;; #? Move up x lines + -d|-down) add_command="${add_command}\e[${2}B"; shift;; #? Move down x lines + -jl|-justify-left) justify_left="${2}"; shift;; #? Justify string left within given width + -jr|-justify-right) justify_right="${2}"; shift;; #? Justify string right within given width + -jc|-justify-center) justify_center="${2}"; shift;; #? Justify string center within given width + -rp|-repeat) repeat=${2}; shift;; #? Repeat next string x number of times + -sc|-save) add_command="\e[s${add_command}";; #? Save cursor position + -rc|-restore) add_command="${add_command}\e[u";; #? Restore cursor position + -trans) trans=1;; #? Make whitespace transparent + -v|-variable) local -n var=$2; ext_var=1; shift;; #? Send output to a variable, appending if not unset + *) text="$1"; shift; break;; #? Assumes text string if no argument is found + esac + shift + done + + #* Repeat string if repeat is enabled + if [[ -n $repeat ]]; then + printf -v r_tmp "%${repeat}s" "" + text="${r_tmp// /$text}" + fi + + #* Set correct placement for screen centered text + if ((center==1 & ${#text}>0 & ${#text}tty_width*4)); then + cpu_history=( "${cpu_history[@]:$((tty_width*2))}" "${cpu_usage[0]}") + else + cpu_history+=("${cpu_usage[0]}") + fi + + for((i=1;i<=threads;i++)); do + local -n cpu_core_history="cpu_core_history_$i" + if ((${#cpu_core_history[@]}>40)); then + cpu_core_history=( "${cpu_core_history[@]:20}" "${cpu_usage[$i]}") + else + cpu_core_history+=("${cpu_usage[$i]}") + fi + done + + #* Get current cpu frequency from "/proc/cpuinfo" and convert to appropriate unit + if [[ $use_psutil == false && -z ${cpu[no_cpu_info]} ]] && ! get_value -v 'cpu[freq]' -sf "/proc/cpuinfo" -k "cpu MHz" -i; then + cpu[no_cpu_info]=1 + fi + + #* If getting cpu frequency from "proc/cpuinfo" was unsuccessfull try "/sys/devices/../../scaling_cur_freq" + if [[ $use_psutil == false && -n ${cpu[no_cpu_info]} && -e "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq" ]]; then + get_value -v 'cpu[freq]' -sf "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq" -i + printf -v 'cpu[freq]' "%.0f0" "${cpu[freq]}e-4" + fi + + if ((${#cpu[freq]}>3)); then cpu[freq_string]="${cpu[freq]::-3}.${cpu[freq]:(-3):1} GHz" + elif ((${#cpu[freq]}>1)); then cpu[freq_string]="${cpu[freq]} MHz" + else cpu[freq_string]=""; fi + + #* Get load average and uptime from uptime command + if [[ $use_psutil == false ]]; then + local uptime_var + read -r uptime_var < <(uptime 2>/dev/null || true) + cpu[load_avg]="${uptime_var#*average: }" + cpu[load_avg]="${cpu[load_avg]//,/}" + cpu[uptime]="${uptime_var#*up }" + cpu[uptime]="${cpu[uptime]%%, *}" + fi + + #* Collect cpu temps if enabled + if [[ $check_temp == true ]]; then collect_cpu_temps; fi +} + +collect_cpu_temps() { #? Collect cpu temperatures + local unit c div threads=${cpu[threads]} sens_var i it ccd_value breaking core_value misc_var + local -a ccd_array core_array + + #* Fetch output from "sensors" command or psutil to a variable + if [[ $sensor_comm == "psutil" ]]; then + if ! py_command -vn sens_var "get_sensors()"; then + if command -v sensors >/dev/null 2>&1; then sensor_comm="sensors" + else sensor_comm=""; check_temp="false"; resized=1; return; fi + fi + fi + if [[ $sensor_comm == "sensors" ]]; then + if [[ $use_psutil == true ]]; then + py_command -vn sens_var "get_cmd_out('sensors 2>/dev/null')" + else + read -rd '' sens_var < <(sensors 2>/dev/null || true) || true + fi + elif [[ $sensor_comm != "sensors" && $sensor_comm != "psutil" ]]; then + if [[ $use_psutil == true ]]; then + py_command -v misc_var "get_cmd_out('${sensor_comm} measure_temp 2>/dev/null')" + else + read -r misc_var < <(${sensor_comm} measure_temp 2>/dev/null ||true) + fi + fi + + #* Get CPU package temp for intel cpus + if get_value -v 'cpu[temp_0]' -sv "sens_var" -k "Package*:" -mk 1 || get_value -v 'cpu[temp_0]' -sv "sens_var" -k "Core 0:" -mk 1; then + #* If successful get temperature unit, convert temp to integer and get high and crit + cpu[temp_unit]="${cpu[temp_0]:(-2)}"; cpu[temp_0]="${cpu[temp_0]%.*}"; if [[ ${cpu[temp_0]::1} == "+" ]]; then cpu[temp_0]="${cpu[temp_0]#+}"; fi + if [[ -z ${cpu[temp_high]} ]]; then + if ! get_value -v 'cpu[temp_high]' -sv "sens_var" -k "Package*high =" -m 2 -r "[^0-9.]" -b -i; then cpu[temp_high]="85"; cpu[temp_crit]=$((cpu[temp_high]+10)) + else get_value -v 'cpu[temp_crit]' -sv "sens_var" -k "Package*crit =" -m 2 -r "[^0-9.]" -b -i; fi + fi + + #* Get core temps + i=0 + while get_value -v "core_value" -sv "sens_var" -k "Core ${i}:" -mk 1 -r "[^0-9.]" -b -i && ((i<=threads)); do core_array+=("$core_value"); ((++i)) ; done + + if [[ -z ${core_array[0]} ]]; then core_array=("${cpu[temp_0]}"); fi + + if ((${#core_array[@]}100)); then tmp_temp=100; elif ((tmp_temp<0)); then tmp_temp=0; fi + local -n cpu_temp_history="cpu_temp_history_$i" + if ((${#cpu_temp_history[@]}>20)); then + cpu_temp_history=( "${cpu_temp_history[@]:10}" "${tmp_temp}") + else + cpu_temp_history+=("${tmp_temp}") + fi + done + fi +} + +collect_mem() { #? Collect memory information from "/proc/meminfo" + ((++mem[counter])) + + #if [[ $use_psutil == false ]] && ((mem[counter]<4)); then return; fi + if ((mem[counter]<4)); then return; fi + mem[counter]=0 + + local i tmp value array mem_info height=$((box[mem_height]-2)) skip filter_value + local -a mem_array swap_array available=("mem") + unset 'mem[total]' + + #* Get memory and swap information from "/proc/meminfo" or psutil and calculate percentages + if [[ $use_psutil == true ]]; then + local pymemout + + py_command -v pymemout "get_mem()" || return + read mem[total] mem[free] mem[available] mem[cached] swap[total] swap[free] <<<"$pymemout" + + if [[ -z ${mem[total]} ]]; then return; fi + if [[ -n ${swap[total]} ]] && ((swap[total]>0)); then + swap[free_percent]=$((swap[free]*100/swap[total])) + swap[used]=$((swap[total]-swap[free])) + swap[used_percent]=$((swap[used]*100/swap[total])) + available+=("swap") + else + unset swap_on + fi + else + read -rd '' mem_info 0)); then + get_value -v 'swap[free]' -sv "mem_info" -k "SwapFree:" -i + swap[free_percent]=$((swap[free]*100/swap[total])) + + swap[used]=$((swap[total]-swap[free])) + swap[used_percent]=$((swap[used]*100/swap[total])) + + available+=("swap") + elif [[ $use_psutil == false ]]; then + unset swap_on + fi + + #* Convert values to floating point and humanize + for array in ${available[@]}; do + for value in total used free available cached; do + if [[ $array == "swap" && $value == "available" ]]; then break 2; fi + local -n this_value="${array}[${value}]" this_string="${array}[${value}_string]" + floating_humanizer -v this_string -s 1 -B "${this_value}" + done + done + + #* Get disk information + local df_line line_array dev_path dev_name iostat_var disk_read disk_write disk_io_string df_count=0 filtering psutil_on + local -a device_array iostat_array df_array + unset 'disks_free[@]' 'disks_used[@]' 'disks_used_percent[@]' 'disks_total[@]' 'disks_name[@]' 'disks_free_percent[@]' 'disks_io[@]' + if [[ -n $psutil_disk_fail ]]; then psutil_on="false"; else psutil_on="$use_psutil"; fi + if [[ $psutil_on == true ]]; then + if [[ -n $disks_filter ]]; then filtering=", filtering='${disks_filter}'"; fi + if ! py_command -a df_array "get_disks(exclude='squashfs'${filtering})"; then psutil_disk_fail=1; psutil_on="false"; fi + fi + if [[ $psutil_on == false ]]; then + readarray -t df_array < <(${df} -x squashfs -x tmpfs -x devtmpfs -x overlay -x 9p 2>/dev/null || true) + fi + for df_line in "${df_array[@]:1}"; do + line_array=(${df_line}) + if ! is_int "${line_array[1]}" || ((line_array[1]<=0)); then continue; fi + + if [[ $psutil_on == false && ${line_array[5]} == "/" ]]; then disks_name+=("root") + elif [[ $psutil_on == false ]]; then disks_name+=("${line_array[5]##*/}") + elif [[ $psutil_on == true ]]; then disks_name+=("${line_array[*]:7}"); fi + + #* Filter disks showed if $disks_filter is set + if [[ $psutil_on == false && -n $disks_filter ]]; then + unset found + for filter_value in ${disks_filter}; do + if [[ $filter_value == "${disks_name[-1]}" ]]; then found=1; fi + done + fi + + if [[ $psutil_on == true || -z $disks_filter || -n $found ]]; then + disks_total+=("$(floating_humanizer -s 1 -B ${line_array[1]})") + disks_used+=("$(floating_humanizer -s 1 -B ${line_array[2]})") + disks_used_percent+=("${line_array[4]%'%'}") + disks_free+=("$(floating_humanizer -s 1 -B ${line_array[3]})") + disks_free_percent+=("$((100-${line_array[4]%'%'}))") + + #* Get read/write stats for disk from iostat or psutil if available + if [[ $psutil_on == true || -n $has_iostat ]]; then + unset disk_io_string + dev_name="${line_array[0]##*/}" + if [[ $psutil_on == false && ${dev_name::2} == "md" ]]; then dev_name="${dev_name::3}"; fi + if [[ $psutil_on == false ]]; then + unset iostat_var 'iostat_array[@]' + dev_path="${line_array[0]%${dev_name}}" + read -r iostat_var < <(iostat -dkz "${dev_path}${dev_name}" | tail -n +4) + iostat_array=(${iostat_var}) + fi + if [[ $psutil_on == true || -n ${iostat_var} ]]; then + + if [[ $psutil_on == true ]]; then + disk_read=${line_array[5]} + disk_write=${line_array[6]} + else + disk_read=$((iostat_array[-2]-${disks[${dev_name}_read]:-${iostat_array[-2]}})) + disk_write=$((iostat_array[-1]-${disks[${dev_name}_write]:-${iostat_array[-1]}})) + fi + + if ((box[m_width2]>25)); then + if ((disk_read>0)); then disk_io_string="▲$(floating_humanizer -s 1 -short -B ${disk_read}) "; fi + if ((disk_write>0)); then disk_io_string+="▼$(floating_humanizer -s 1 -short -B ${disk_write})"; fi + elif ((disk_read+disk_write>0)); then + disk_io_string+="▼▲$(floating_humanizer -s 1 -short -B $((disk_read+disk_write)))" + fi + + if [[ $psutil_on == false ]]; then + disks[${dev_name}_read]="${iostat_array[-2]}" + disks[${dev_name}_write]="${iostat_array[-1]}" + fi + fi + disks_io+=("${disk_io_string:-0}") + fi + else + unset 'disks_name[-1]' + disks_name=("${disks_name[@]}") + fi + + if ((${#disks_name[@]}>=height/2)); then break; fi + + done + + +} + +collect_processes() { #? Collect process information and calculate accurate cpu usage + if [[ $use_psutil == true ]]; then collect_processes_psutil $1; return; fi + local argument="$1" + if [[ -n $skip_process_draw && $argument != "now" ]]; then return; fi + local width=${box[processes_width]} height=${box[processes_height]} format_args format_cmd readline sort symbol="▼" cpu_title options pid_string tmp selected + local tree tree_compare1 tree_compare2 tree_compare3 no_core_divide pids + local -a grep_array saved_proc_array + + if [[ $argument == "now" ]]; then skip_process_draw=1; fi + + if [[ -n ${proc[reverse]} ]]; then symbol="▲"; fi + case ${proc_sorting} in + "pid") selected="Pid:"; sort="pid";; + "program") selected="Program:"; sort="comm";; + "arguments") selected="Arguments:"; sort="args";; + "threads") selected="Threads:"; sort="nlwp";; + "user") selected="User:"; sort="euser";; + "memory") selected="Mem%"; sort="pmem";; + "cpu lazy"|"cpu responsive") sort="pcpu"; selected="Cpu%";; + esac + + if [[ $proc_tree == true ]]; then tree="Tree:"; fi + if [[ $proc_per_core == true ]]; then no_core_divide="1"; fi + + #* Collect output from ps command to array + if ((width>60)) && [[ $proc_tree != true ]] ; then format_args=",args:$(( width-(47+proc[pid_len]) ))=Arguments:"; format_cmd=15 + else format_cmd=$(( width-(31+proc[pid_len]) )); fi + saved_proc_array=("${proc_array[@]}") + unset 'proc_array[@]' 'pid_array[@]' + + if ((proc[detailed]==0)) && [[ -n ${proc[detailed_name]} ]]; then + unset 'proc[detailed_name]' 'proc[detailed_killed]' 'proc[detailed_cpu_int]' 'proc[detailed_cmd]' + unset 'proc[detailed_mem]' 'proc[detailed_mem_int]' 'proc[detailed_user]' 'proc[detailed_threads]' + unset 'detail_graph[@]' 'detail_mem_graph' 'detail_history[@]' 'detail_mem_history[@]' + unset 'proc[detailed_runtime]' 'proc[detailed_mem_string]' 'proc[detailed_parent_pid]' 'proc[detailed_parent_name]' + fi + + unset 'proc[detailed_cpu]' + + if [[ -z $filter ]]; then + options="-t" + fi + + readarray ${options} proc_array < <(ps ax${tree:+f} -o pid:${proc[pid_len]}=Pid:,comm:${format_cmd}=${tree:-Program:}${format_args},nlwp:3=Tr:,euser:6=User:,pmem=Mem%,pcpu:10=Cpu% --sort ${proc[reverse]:--}${sort}) + + proc_array[0]="${proc_array[0]/ Tr:/ Threads:}" + proc_array[0]="${proc_array[0]/ ${selected}/${symbol}${selected}}" + + if [[ -n $filter ]]; then + grep_array[0]="${proc_array[0]}" + readarray -O 1 -t grep_array < <(echo -e " ${proc_array[*]:1}" | grep -e "${filter}" ${proc[detailed_pid]:+-e ${proc[detailed_pid]}} | cut -c 2- || true) + proc_array=("${grep_array[@]}") + fi + + + #* Get accurate cpu usage by fetching and comparing values in /proc/"pid"/stat + local operations operation utime stime count time_elapsed cpu_percent_string rgb=231 step add proc_out tmp_value_array i pcpu_usage cpu_int tmp_percent breaking + local -a cpu_percent statfile work_array + + #* Timestamp the values in milliseconds to accurately calculate cpu usage + get_ms proc[new_timestamp] + + for readline in "${proc_array[@]:1}"; do + ((++count)) + + if ((count==height-3 & breaking==0)); then + if [[ -n $filter || $proc_sorting != "cpu lazy" || ${proc[selected]} -gt 0 || ${proc[start]} -gt 1 || ${proc_reversed} == true ]]; then : + else breaking=1; fi + fi + + #if get_key -save && [[ ${#saved_key[@]} -gt 0 ]]; then proc_array=("${saved_proc_array[@]}"); return; fi + + if ((breaking==2)); then + work_array=(${proc_array[-1]}) + else + work_array=(${readline}) + fi + + pid="${work_array[0]}" + pcpu_usage="${work_array[-1]}" + + #* If showing tree structure replace slashes and pipes with actual lines and terminate them at the correct places + if [[ $proc_tree == true ]]; then + tree_compare1="${proc_array[$((count+1))]%'\_'*}" + tree_compare2="${proc_array[count]%'\_'*}" + tree_compare3="${proc_array[$((count+1))]%'|'*}" + proc_array[count]="${proc_array[count]//'|'/│}" + proc_array[count]="${proc_array[count]//'\_'/└─}" + if ((count<${#proc_array[@]}-1)) && [[ ${#tree_compare1} -eq ${#tree_compare2} || ${#tree_compare2} -eq ${#tree_compare3} ]]; then + proc_array[count]="${proc_array[count]//'└'/├}" + fi + fi + + pid_history[${pid}]="1" + + if [[ -n $filter || $proc_sorting == "cpu responsive" ]] && [[ ${proc_array[count]:${proc[pid_len]}:1} != " " ]]; then + unset pid_string + printf -v pid_string "%${proc[pid_len]}s" "${pid}" + proc_array[count]="${pid_string}${proc_array[count]#*${pid}}" + fi + + if [[ -r "/proc/${pid}/stat" ]] && read -ra statfile /dev/null; then + + utime=${statfile[13]} + stime=${statfile[14]} + + proc[new_${pid}_ticks]=$((utime+stime)) + + + if [[ -n ${proc[old_${pid}_ticks]} ]]; then + + time_elapsed=$((proc[new_timestamp]-proc[old_timestamp])) + + #* Calculate current cpu usage for process, * 1000 (for conversion from ms to seconds) * 1000 (for conversion to floating point) + cpu_percent[count]=$(( ( ( ${proc[new_${pid}_ticks]}-${proc[old_${pid}_ticks]} ) * 1000 * 1000 ) / ( cpu[hz]*time_elapsed*${no_core_divide:-${cpu[threads]}} ) )) + + if ((cpu_percent[count]<0)); then cpu_percent[count]=0 + elif [[ -z $no_core_divide ]] && ((cpu_percent[count]>1000)); then cpu_percent[count]=1000; fi + + if ((${#cpu_percent[count]}<=3)); then + printf -v cpu_percent_string "%01d%s" "${cpu_percent[count]::-1}" ".${cpu_percent[count]:(-1)}" + else + cpu_percent_string=${cpu_percent[count]::-1} + fi + + printf -v cpu_percent_string "%5s" "${cpu_percent_string::4}" + + proc_array[count]="${proc_array[count]::-5}${cpu_percent_string}" + + + pid_graph="pid_${pid}_graph" + local -n pid_count="pid_${pid}_count" + + printf -v cpu_int "%01d" "${cpu_percent[count]::-1}" + + #* Get info for detailed box if enabled + if [[ ${pid} == "${proc[detailed_pid]}" ]]; then + if [[ -z ${proc[detailed_name]} ]]; then + local get_mem mem_string cmdline="" + local -a det_array + read -r proc[detailed_name] 5)); then + proc[detailed_mem_count]=0 + proc[detailed_mem]="${work_array[-2]}" + proc[detailed_mem_int]="${proc[detailed_mem]/./}" + if [[ ${proc[detailed_mem_int]::1} == "0" ]]; then proc[detailed_mem_int]="${proc[detailed_mem_int]:1}0"; fi + #* Scale up low mem values to see any changes on mini graph + if ((proc[detailed_mem_int]>900)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]/10)) + elif ((proc[detailed_mem_int]>600)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]/8)) + elif ((proc[detailed_mem_int]>300)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]/5)) + elif ((proc[detailed_mem_int]>100)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]/2)) + elif ((proc[detailed_mem_int]<50)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]*2)); fi + unset 'proc[detailed_mem_string]' + read -r mem_string < <(ps -o rss:1 --no-headers -p ${pid} || true) + floating_humanizer -v proc[detailed_mem_string] -B -s 1 $mem_string + if [[ -z ${proc[detailed_mem_string]} ]]; then proc[detailed_mem_string]="? Byte"; fi + fi + + #* Copy process cpu usage to history array and trim earlier entries + if ((${#detail_history[@]}>box[details_width]*2)); then + detail_history=( "${detail_history[@]:${box[details_width]}}" "$((cpu_int+4))") + else + detail_history+=("$((cpu_int+4))") + fi + + #* Copy process mem usage to history array and trim earlier entries + if ((${#detail_mem_history[@]}>box[details_width])); then + detail_mem_history=( "${detail_mem_history[@]:$((box[details_width]/2))}" "${proc[detailed_mem_int]}") + else + detail_mem_history+=("${proc[detailed_mem_int]}") + fi + + #* Remove selected process from array if process is excluded by filtering or not on first page + if [[ -n $filter && ! ${proc[detailed_name]} =~ $filter ]]; then + unset 'proc_array[count]' + cpu_int=0; pid_count=0 + fi + fi + + #* Create small graphs for all visible processes using more than 1% cpu time + if [[ ${cpu_int} -gt 0 ]]; then pid_count=5; fi + + if [[ -z ${!pid_graph} && ${cpu_int} -gt 0 ]]; then + tmp_value_array=("$((cpu_int+4))") + create_mini_graph -o "pid_${pid}_graph" -nc -w 5 "tmp_value_array" + elif [[ ${pid_count} -gt 0 ]]; then + if [[ ${cpu_int} -gt 9 ]]; then + create_mini_graph -nc -add-value "pid_${pid}_graph" "$((cpu_int+15))" + else + create_mini_graph -nc -add-value "pid_${pid}_graph" "$((cpu_int+9))" + fi + + pid_count=$((${pid_count}-1)) + elif [[ ${pid_count} == "0" ]]; then + unset "pid_${pid}_graph" "pid_${pid}_graph_even" "pid_${pid}_graph_odd" "pid_${pid}_graph_last_type" "pid_${pid}_graph_last_val" + unset "pid_${pid}_count" + fi + else + tmp_percent="${proc_array[count]:(-5)}"; tmp_percent="${tmp_percent// /}"; if [[ ${tmp_percent//./} != "$tmp_percent" ]]; then tmp_percent="${tmp_percent::-2}"; fi + if ((tmp_percent>100)); then + proc_array[count]="${proc_array[count]::-5} 100" + fi + fi + + proc[old_${pid}_ticks]=${proc[new_${pid}_ticks]} + + fi + + if ((breaking==1)); then + if [[ ${proc[detailed]} == "1" && -z ${proc[detailed_cpu]} ]] && ps ${proc[detailed_pid]} >/dev/null 2>&1; then + readarray ${options} -O ${#proc_array[@]} proc_array < <(ps -o pid:${proc[pid_len]}=Pid:,comm:${format_cmd}=${tree:-Program:}${format_args},nlwp:3=Tr:,euser:6=User:,pmem=Mem%,pcpu:10=Cpu% --no-headers -p ${proc[detailed_pid]} || true) + ((++breaking)) + else + break + fi + elif ((breaking==2)); then + unset 'proc_array[-1]' + break + fi + + done + + + proc[old_timestamp]=${proc[new_timestamp]} + + if ((proc[detailed]==1)) && [[ -z ${proc[detailed_cpu]} && -z ${proc[detailed_killed]} ]]; then proc[detailed_killed]=1; proc[detailed_change]=1 + elif [[ -n ${proc[detailed_cpu]} ]]; then unset 'proc[detailed_killed]'; fi + + #* Sort output array based on cpu usage if "cpu responsive" is selected + if [[ ${proc_sorting} == "cpu responsive" && ${proc_tree} != true ]]; then + local -a sort_array + if [[ -z ${proc[reverse]} ]]; then local sort_rev="-r"; fi + sort_array[0]="${proc_array[0]}" + readarray -O 1 -t sort_array < <(printf "%s\n" "${proc_array[@]:1}" | awk '{ print $NF, $0 }' | sort -n -k1 ${sort_rev}| sed 's/^[0-9\.]* //') + proc_array=("${sort_array[@]}") + fi + + #* Clear up memory by removing variables and graphs of no longer running processes + ((++proc[general_counter])) + if ((proc[general_counter]>100)); then + proc[general_counter]=0 + for pids in ${!pid_history[@]}; do + if [[ ! -e /proc/${pids} ]]; then + unset "pid_${pids}_graph" "pid_${pids}_graph_even" "pid_${pids}_graph_odd" "pid_${pids}_graph_last_type" "pid_${pids}_graph_last_val" + unset "pid_${pids}_count" + unset "proc[new_${pids}_ticks]" + unset "proc[old_${pids}_ticks]" + unset "pid_history[${pids}]" + fi + done + fi + +} + +collect_processes_psutil() { + local argument=$1 + if [[ -n $skip_process_draw && $argument != "now" ]]; then return; fi + if [[ $argument == "now" ]]; then skip_process_draw=1; fi + local prog_len arg_len symbol="▼" selected width=${box[processes_width]} height=${box[processes_height]} + local pcpu_usage pids p_count cpu_int pids max_lines i pi + + case ${proc_sorting} in + "pid") selected="Pid:";; + "program") selected="Program:";; + "arguments") selected="Arguments:";; + "threads") selected="Threads:";; + "user") selected="User:";; + "memory") selected="Mem%";; + "cpu lazy"|"cpu responsive") selected="Cpu%";; + esac + + if [[ ${proc_tree} == true && ${proc_sorting} =~ pid|program|arguments ]]; then selected="Tree:"; fi + + if [[ -n ${proc[reverse]} ]]; then symbol="▲"; fi + + if ((proc[detailed]==0)) && [[ -n ${proc[detailed_name]} ]]; then + unset 'proc[detailed_name]' 'proc[detailed_killed]' 'proc[detailed_cpu_int]' 'proc[detailed_cmd]' + unset 'proc[detailed_mem]' 'proc[detailed_mem_int]' 'proc[detailed_user]' 'proc[detailed_threads]' + unset 'detail_graph[@]' 'detail_mem_graph' 'detail_history[@]' 'detail_mem_history[@]' + unset 'proc[detailed_runtime]' 'proc[detailed_mem_string]' 'proc[detailed_parent_pid]' 'proc[detailed_parent_name]' + fi + + unset 'proc[detailed_cpu]' + + + if ((width>60)); then + arg_len=$((width-55)) + prog_len=15 + else + prog_len=$((width-40)) + arg_len=0 + if [[ $proc_sorting == "threads" ]]; then selected="Tr:"; fi + fi + + + unset 'proc_array[@]' + if ! py_command -a proc_array "get_proc(sorting='${proc_sorting}', tree=${proc_tree^}, prog_len=${prog_len}, arg_len=${arg_len}, search='${filter}', reverse=${proc_reversed^}, proc_per_cpu=${proc_per_core^})"; then + proc_array=(""); return + fi + + proc_array[0]="${proc_array[0]/ ${selected}/${symbol}${selected}}" + + for((i=1;i<${#proc_array[@]};i++)); do + if [[ -z ${proc_array[i]} ]]; then continue; fi + + out_arr=(${proc_array[i]}) + + pi=0 + if [[ $proc_tree == true ]]; then + while ! is_int "${out_arr[pi]}" && ((pi<${#out_arr[@]}-1)); do ((++pi)); done + fi + pid="${out_arr[pi]}" + if ! is_int "${pid}"; then continue; fi + + pcpu_usage="${out_arr[-1]}" + + if ! printf -v cpu_int "%.0f" "${pcpu_usage}" 2>/dev/null; then continue; fi + + pid_history[${pid}]="1" + + #* Create small graphs for all visible processes using more than 1% rounded cpu time + pid_graph="pid_${pid}_graph" + if ! local -n pid_count="pid_${pid}_count" 2>/dev/null; then continue; fi + + if [[ ${cpu_int} -gt 0 ]]; then pid_count=5; fi + + if [[ -z ${!pid_graph} && ${cpu_int} -gt 0 ]]; then + tmp_value_array=("$((cpu_int+4))") + create_mini_graph -o "pid_${pid}_graph" -nc -w 5 "tmp_value_array" + elif [[ ${pid_count} -gt 0 ]]; then + if [[ ${cpu_int} -gt 9 ]]; then + create_mini_graph -nc -add-value "pid_${pid}_graph" "$((cpu_int+15))" + elif [[ ${cpu_int} -gt 0 ]]; then + create_mini_graph -nc -add-value "pid_${pid}_graph" "$((cpu_int+9))" + else + create_mini_graph -nc -add-value "pid_${pid}_graph" "0" + fi + pid_count=$((${pid_count}-1)) + elif [[ ${pid_count} == "0" ]]; then + unset "pid_${pid}_graph" "pid_${pid}_graph_even" "pid_${pid}_graph_odd" "pid_${pid}_graph_last_type" "pid_${pid}_graph_last_val" + unset "pid_${pid}_count" + fi + + #* Get info for detailed box if enabled + if [[ ${pid} == "${proc[detailed_pid]}" ]]; then + local -a det_array + if [[ -z ${proc[detailed_name]} ]]; then + local get_mem mem_string cmdline="" + + py_command -a det_array "get_detailed_names_cmd(${pid})" + + if [[ -z ${det_array[0]} ]]; then continue; fi + proc[detailed_name]="${det_array[0]::15}" + proc[detailed_parent_name]="${det_array[1]}" + proc[detailed_user]="${det_array[2]}" + proc[detailed_cmd]="${det_array[3]}" + fi + proc[detailed_cpu]="${out_arr[-1]}" + proc[detailed_cpu_int]="${cpu_int}" + proc[detailed_threads]="${out_arr[-4]}" + + unset 'det_array[@]' + py_command -a det_array "get_detailed_mem_time(${pid})" + + if [[ -z ${det_array[0]} ]]; then continue; fi + unset 'proc[detailed_mem_string]' + floating_humanizer -v proc[detailed_mem_string] -B ${det_array[0]} + if [[ -z ${proc[detailed_mem_string]} ]]; then proc[detailed_mem_string]="? Byte"; fi + if ((${#det_array[1]}>8)); then proc[detailed_runtime]="${det_array[1]/ days, /-}" + else proc[detailed_runtime]="${det_array[1]}"; fi + + proc[detailed_mem_count]=0 + proc[detailed_mem]="${out_arr[-2]}" + proc[detailed_mem_int]="${proc[detailed_mem]/./}" + if [[ ${proc[detailed_mem_int]::1} == "0" ]]; then proc[detailed_mem_int]="${proc[detailed_mem_int]:1}0"; fi + #* Scale up low mem values to see any changes on mini graph + if ((proc[detailed_mem_int]>900)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]/10)) + elif ((proc[detailed_mem_int]>600)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]/8)) + elif ((proc[detailed_mem_int]>300)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]/5)) + elif ((proc[detailed_mem_int]>100)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]/2)) + elif ((proc[detailed_mem_int]<50)); then proc[detailed_mem_int]=$((proc[detailed_mem_int]*2)); fi + + #* Copy process cpu usage to history array and trim earlier entries + if ((${#detail_history[@]}>box[details_width]*2)); then + detail_history=( "${detail_history[@]:${box[details_width]}}" "$((cpu_int+4))") + else + detail_history+=("$((cpu_int+4))") + fi + + #* Copy process mem usage to history array and trim earlier entries + if ((${#detail_mem_history[@]}>box[details_width])); then + detail_mem_history=( "${detail_mem_history[@]:$((box[details_width]/2))}" "${proc[detailed_mem_int]}") + else + detail_mem_history+=("${proc[detailed_mem_int]}") + fi + fi + + if ((i==height-2)); then + if [[ ${proc[selected]} -gt 0 || -n $filter || ${proc[start]} -gt 1 ]] || [[ ${proc[detailed]} -eq 1 && -z ${proc[detailed_cpu]} && -z ${proc[detailed_killed]} ]]; then : + else break; fi + fi + + done + + if ((proc[detailed]==1)) && [[ -z ${proc[detailed_cpu]} && -z ${proc[detailed_killed]} ]]; then proc[detailed_killed]=1; proc[detailed_change]=1 + elif [[ -n ${proc[detailed_cpu]} ]]; then unset 'proc[detailed_killed]'; fi + + + + #* Clear up memory + ((++proc[general_counter])) + if ((proc[general_counter]>100)); then + proc[general_counter]=0 + for pids in ${!pid_history[@]}; do + unset "pid_${pids}_graph" "pid_${pids}_graph_even" "pid_${pids}_graph_odd" "pid_${pids}_graph_last_type" "pid_${pids}_graph_last_val" + unset "pid_${pids}_count" + unset "pid_history[${pids}]" + done + fi + +} + +collect_net() { #? Collect information from "/proc/net/dev" + local operations operation direction index unit_selector speed speed_B total + local -a net_dev history_sorted history_last + + if [[ -n ${net[no_device]} ]]; then return; fi + + if [[ $1 == "init" ]]; then + for direction in "download" "upload"; do + net[${direction}_max]=0 + net[${direction}_new_low]=0 + net[${direction}_new_max]=0 + net[${direction}_max_current]=0 + net[${direction}_graph_max]=$((50<<10)) + done + unset 'download_graph[@]' 'upload_graph[@]' 'net_history_download[@]' 'net_history_upload[@]' + fi + + #* Get the line with relevant net device from /proc/net/dev or psutil into array net_dev, index 1 is download, index 9 is upload + if [[ $use_psutil == true ]]; then + py_command -v net_dev "get_net('${net[device]}')" || return + net_dev=(${net_dev}) + if ! is_int "${net_dev[0]}"; then net[no_device]=1; return; fi + else + if ! get_value -map net_dev -sf "/proc/net/dev" -k "${net[device]}" -a; then net[no_device]=1; return; fi + fi + + #* Timestamp the values to accurately calculate values in seconds + get_ms net[new_timestamp] + for direction in "download" "upload"; do + if [[ $direction == "download" ]]; then index=1 + else index=9; fi + + net[new_${direction}]=${net_dev[index]} + + if [[ -n ${net[old_${direction}]} ]]; then + #* Get total, convert to floating point and format string to best fitting unit in Bytes + if ((net[nic_change]==1 & net[reset]==1)); then unset "net[total_offset_${direction}]"; net[reset]=0; fi + if ((net[reset]==1)) && [[ -z ${net[total_offset_${direction}]} || ${net[total_offset_${direction}]} -gt ${net[new_${direction}]} ]]; then net[total_offset_${direction}]=${net[new_${direction}]} + elif ((net[reset]==0)) && [[ -n ${net[total_offset_${direction}]} ]]; then unset "net[total_offset_${direction}]"; fi + + floating_humanizer -Byte -v net[total_${direction}] $((${net[new_${direction}]}-${net[total_offset_${direction}]:-0})) + + #* Calculate current speeds: ("New value" - "Old value") * 1000(for ms to seconds) / ("new_timestamp" - "old_timestamp") + net[speed_${direction}]=$(( (${net[new_${direction}]}-${net[old_${direction}]})*1000/(net[new_timestamp]-net[old_timestamp]) )) + + #* Convert to floating point and format string to best fitting unit in Bytes and Bits per second + floating_humanizer -Byte -per-second -v net[speed_${direction}_byteps] ${net[speed_${direction}]} + floating_humanizer -bit -per-second -v net[speed_${direction}_bitps] ${net[speed_${direction}]} + + #* Update download and upload max values for graph + if ((${net[speed_${direction}]}>${net[${direction}_max]})); then + net[${direction}_max]=${net[speed_${direction}]} + fi + + if ((${net[speed_${direction}]}>${net[${direction}_graph_max]})); then + ((++net[${direction}_new_max])) + if ((net[${direction}_new_low]>0)); then ((net[${direction}_new_low]--)); fi + elif ((${net[${direction}_graph_max]}>10<<10 & ${net[speed_${direction}]}<${net[${direction}_graph_max]}/10)); then + ((++net[${direction}_new_low])) + if ((net[${direction}_new_max]>0)); then ((net[${direction}_new_max]--)); fi + fi + + #* Copy download and upload speed to history arrays and trim earlier entries + local -n history="net_history_${direction}" + if ((${#history[@]}>box[net_width]*4)); then + history=( "${history[@]:$((box[net_width]*2))}" "${net[speed_${direction}]}") + else + history+=("${net[speed_${direction}]}") + fi + + #* Check for new max value and set flag to adjust resolution of graph if needed + if ((${net[${direction}_new_max]}>=5)); then + net[${direction}_graph_max]=$((${net[${direction}_max]}+(${net[${direction}_max]}/3) )) + net[${direction}_redraw]=1 + net[${direction}_new_max]=0 + + #* If current max value isn't relevant, sort array to get the next largest value to set graph resolution + elif ((${net[${direction}_new_low]}>=5 & ${#history[@]}>5)); then + history_last=("${history[@]:(-5)}") + sort_array_int "history_last" "history_sorted" + net[${direction}_max]=${history_sorted[0]} + net[${direction}_graph_max]=$(( ${net[${direction}_max]}*3 )) + if ((${net[${direction}_graph_max]}<10<<10)); then net[${direction}_graph_max]=$((10<<10)); fi + net[${direction}_redraw]=1 + net[${direction}_new_low]=0 + fi + fi + + floating_humanizer -Byte -short -v net[${direction}_max_string] ${net[${direction}_graph_max]} + + net[old_${direction}]=${net[new_${direction}]} + done + + net[old_timestamp]=${net[new_timestamp]} + +} + +calc_sizes() { #? Calculate width and height of all boxes + local pos calc_size calc_total percent threads=${cpu[threads]} + + #* Calculate heights + for pos in ${box[boxes]/processes/}; do + if [[ $pos = "cpu" ]]; then percent=32; + elif [[ $pos = "mem" ]]; then percent=40; + else percent=28; fi + + #* Multiplying with 10 to convert to floating point + calc_size=$(( (tty_height*10)*(percent*10)/100 )) + + #* Round down if last 2 digits of value is below "50" and round up if above + if ((${calc_size:(-2):1}==0)); then calc_size=$((calc_size+10)); fi + if ((${calc_size:(-2)}<50)); then + calc_size=$((${calc_size::-2})) + else + calc_size=$((${calc_size::-2}+1)) + fi + + #* Subtract from last value if the total of all rounded numbers is larger then terminal height + while ((calc_total+calc_size>tty_height)); do ((--calc_size)); done + calc_total=$((calc_total+calc_size)) + + #* Set calculated values in box array + box[${pos}_line]=$((calc_total-calc_size+1)) + box[${pos}_col]=1 + box[${pos}_height]=$calc_size + box[${pos}_width]=$tty_width + done + + + #* Calculate widths + unset calc_total + for pos in net processes; do + if [[ $pos = "net" ]]; then percent=45; else percent=55; fi + + #* Multiplying with 10 to convert to floating point + calc_size=$(( (tty_width*10)*(percent*10)/100 )) + + #* Round down if last 2 digits of value is below "50" and round up if above + if ((${calc_size:(-2)}<50)); then + calc_size=$((${calc_size::-2})) + else + calc_size=$((${calc_size::-2}+1)) + fi + + #* Subtract from last value if the total of all rounded numbers is larger then terminal width + while ((calc_total+calc_size>tty_width)); do ((--calc_size)); done + calc_total=$((calc_total+calc_size)) + + #* Set calculated values in box array + box[${pos}_col]=$((calc_total-calc_size+1)) + box[${pos}_width]=$calc_size + done + + #* Copy numbers around to get target layout + box[mem_width]=${box[net_width]} + box[processes_line]=${box[mem_line]} + box[processes_height]=$((box[mem_height]+box[net_height])) + + # threads=${box[testing]} #! For testing, remove <-------------- + + #* Recalculate size of process box if currently showing detailed process information + if ((proc[detailed]==1)); then + box[details_line]=${box[processes_line]} + box[details_col]=${box[processes_col]} + box[details_width]=${box[processes_width]} + box[details_height]=8 + box[processes_line]=$((box[processes_line]+box[details_height])) + box[processes_height]=$((box[processes_height]-box[details_height])) + fi + + #* Calculate number of columns and placement of cpu meter box + local cpu_line=$((box[cpu_line]+1)) cpu_width=$((box[cpu_width]-2)) cpu_height=$((box[cpu_height]-2)) box_cols + if ((threads>(cpu_height-3)*3 && tty_width>=200)); then box[p_width]=$((24*4)); box[p_height]=$((threads/4+4)); box_cols=4 + elif ((threads>(cpu_height-3)*2 && tty_width>=150)); then box[p_width]=$((24*3)); box[p_height]=$((threads/3+5)); box_cols=3 + elif ((threads>cpu_height-3 && tty_width>=100)); then box[p_width]=$((24*2)); box[p_height]=$((threads/2+4)); box_cols=2 + else box[p_width]=24; box[p_height]=$((threads+4)); box_cols=1 + fi + + if [[ $check_temp == true ]]; then + box[p_width]=$(( box[p_width]+13*box_cols)) + fi + + if ((box[p_height]>cpu_height)); then box[p_height]=$cpu_height; fi + box[p_col]="$((cpu_width-box[p_width]+2))" + box[p_line]="$((cpu_line+(cpu_height/2)-(box[p_height]/2)+1))" + + #* Calculate placement of mem divider + local mem_line=$((box[mem_line]+1)) mem_width=$((box[mem_width]-2)) mem_height=$((box[mem_height]-2)) mem_col=$((box[mem_col]+1)) + box[m_width]=$((mem_width/2)) + box[m_width2]=${box[m_width]} + if ((box[m_width]+box[m_width2]9)); then box[n_height]=9 + else box[n_height]=$net_height; fi + box[n_col]="$((net_width-box[n_width]+2))" + box[n_line]="$((net_line+(net_height/2)-(box[n_height]/2)+1))" + + +} + +draw_bg() { #? Draw all box outlines + local this_box cpu_p_width i cpu_model_len + + unset boxes_out + for this_box in ${box[boxes]}; do + create_box -v boxes_out -col ${box[${this_box}_col]} -line ${box[${this_box}_line]} -width ${box[${this_box}_width]} -height ${box[${this_box}_height]} -fill -lc "${box[${this_box}_color]}" -title ${this_box} + done + + #* Misc cpu box + if [[ $check_temp == true ]]; then cpu_model_len=18; else cpu_model_len=9; fi + create_box -v boxes_out -col $((box[p_col]-1)) -line $((box[p_line]-1)) -width ${box[p_width]} -height ${box[p_height]} -lc ${theme[div_line]} -t "${cpu[model]:0:${cpu_model_len}}" + print -v boxes_out -m ${box[cpu_line]} $((box[cpu_col]+10)) -rs \ + -fg ${box[cpu_color]} -t "┤" -b -fg ${theme[hi_fg]} -t "m" -fg ${theme[title]} -t "enu" -rs -fg ${box[cpu_color]} -t "├" + + #* Misc mem + print -v boxes_out -m ${box[mem_line]} $((box[mem_col]+box[m_width]+2)) -rs -fg ${box[mem_color]} -t "┤" -fg ${theme[title]} -b -t "disks" -rs -fg ${box[mem_color]} -t "├" + print -v boxes_out -m ${box[mem_line]} $((box[mem_col]+box[m_width])) -rs -fg ${box[mem_color]} -t "┬" + print -v boxes_out -m $((box[mem_line]+box[mem_height]-1)) $((box[mem_col]+box[m_width])) -fg ${box[mem_color]} -t "┴" + for((i=1;i<=box[mem_height]-2;i++)); do + print -v boxes_out -m $((box[mem_line]+i)) $((box[mem_col]+box[m_width])) -fg ${theme[div_line]} -t "│" + done + + + #* Misc net box + create_box -v boxes_out -col $((box[n_col]-1)) -line $((box[n_line]-1)) -width ${box[n_width]} -height ${box[n_height]} -lc ${theme[div_line]} -t "Download" + print -v boxes_out -m $((box[n_line]+box[n_height]-2)) $((box[n_col]+1)) -rs -fg ${theme[div_line]} -t "┤" -fg ${theme[title]} -b -t "Upload" -rs -fg ${theme[div_line]} -t "├" + + + if [[ $1 == "quiet" ]]; then draw_out="${boxes_out}" + else echo -en "${boxes_out}"; fi + draw_update_string $1 +} + +draw_cpu() { #? Draw cpu and core graphs and print percentages + local cpu_out i name cpu_p_color temp_color y pt_line pt_col p_normal_color="${theme[main_fg]}" threads=${cpu[threads]} + local meter meter_size meter_width temp_var cpu_out_var core_name temp_name temp_width + + #* Get variables from previous calculations + local col=$((box[cpu_col]+1)) line=$((box[cpu_line]+1)) width=$((box[cpu_width]-2)) height=$((box[cpu_height]-2)) + local p_width=${box[p_width]} p_height=${box[p_height]} p_col=${box[p_col]} p_line=${box[p_line]} + + #* If resized recreate cpu meter/graph box, cpu graph and core graphs + if ((resized>0)); then + local graph_a_size graph_b_size + graph_a_size=$((height/2)); graph_b_size=${graph_a_size} + + if ((graph_a_size*224+temp_width)); then + name="CPU Total "; meter_width=$((p_width-17-temp_width)) + fi + + + #* Create cpu usage meter + if ((i==0)); then + create_meter -v meter -w $meter_width -f -c color_cpu_graph ${cpu_usage[i]} + else + core_name="cpu_core_${i}_graph" + meter="${!core_name}" + fi + + if ((p_width>84+temp_width & i>=(p_height-2)*3-2)); then pt_line=$((p_line+i-y*4)); pt_col=$((p_col+72+temp_width*3)) + elif ((p_width>54+temp_width & i>=(p_height-2)*2-1)); then pt_line=$((p_line+i-y*3)); pt_col=$((p_col+48+temp_width*2)) + elif ((p_width>24+temp_width & i>=p_height-2)); then pt_line=$((p_line+i-y*2)); pt_col=$((p_col+24+temp_width)) + else y=$i; fi + + print -v cpu_out_var -m $((pt_line+y)) $pt_col -rs -fg $p_normal_color -jl 7 -t "$name" -fg ${theme[inactive_fg]} "⡀⡀⡀⡀⡀⡀⡀⡀⡀⡀" -l 10 -fg $cpu_p_color -t "$meter"\ + -jr 4 -fg $cpu_p_color -t "${cpu_usage[i]}" -fg $p_normal_color -t "%" + if [[ $check_temp == true && -n ${cpu[temp_${i}]} ]]; then + print -v cpu_out_var -fg ${theme[inactive_fg]} " ⡀⡀⡀⡀⡀" -l 7 -fg $temp_color -jl 7 -t " ${!temp_name}" -jr 4 -t ${cpu[temp_${i}]} -fg $p_normal_color -t ${cpu[temp_unit]} + fi + + if (( i>(p_height-2)*( p_width/(24+temp_width) )-( p_width/(24+temp_width) )-1 )); then break; fi + done + + #* Print load average and uptime + if ((pt_line+y+30 & resized==0)); then return; fi + + local i swap_used_meter swap_free_meter mem_available_meter mem_free_meter mem_used_meter mem_cached_meter normal_color="${theme[main_fg]}" value_text + local meter_mod_w meter_mod_pos value type m_title meter_options values="used available cached free" + local -a types=("mem") + unset mem_out + + if [[ -n ${swap[total]} && ${swap[total]} -gt 0 ]]; then types+=("swap"); fi + + #* Get variables from previous calculations + local col=$((box[mem_col]+1)) line=$((box[mem_line]+1)) width=$((box[mem_width]-2)) height=$((box[mem_height]-2)) + local m_width=${box[m_width]} m_height=${box[m_height]} m_col=${box[m_col]} m_line=${box[m_line]} mem_line=$((box[mem_col]+box[m_width])) + + #* Create text and meters for memory and swap and adapt sizes based on available height + local y_pos=$m_line v_height=8 list value meter inv_meter + + for type in ${types[@]}; do + local -n type_name="$type" + if [[ $type == "mem" ]]; then + m_title="memory" + else + m_title="$type" + if ((height>14)); then ((y_pos++)); fi + fi + + #* Print name of type and total amount in humanized base 2 bytes + print -v mem_out -m $y_pos $m_col -rs -fg ${theme[title]} -b -jl 9 -t "${m_title^}:" -m $((y_pos++)) $((mem_line-10)) -jr 9 -t " ${type_name[total_string]::$((m_width-11))}" + + for value in ${values}; do + if [[ $type == "swap" && $value =~ available|cached ]]; then continue; fi + + if [[ $system == "MacOS" && $value == "cached" ]]; then value_text="active" + else value_text="${value::$((m_width-12))}"; fi + if ((height<14)); then value_text="${value_text::5}"; fi + + #* Print name of value and value amount in humanized base 2 bytes + print -v mem_out -m $y_pos $m_col -rs -fg $normal_color -jl 9 -t "${value_text^}:" -m $((y_pos++)) $((mem_line-10)) -jr 9 -t " ${type_name[${value}_string]::$((m_width-11))}" + + #* Create meter for value and calculate size and placement depending on terminal size + if ((height>v_height++ | tty_width>100)); then + if ((height<=v_height & tty_width<150)); then + meter_mod_w=12 + meter_mod_pos=7 + ((y_pos--)) + elif ((height<=v_height)); then + print -v mem_out -m $((--y_pos)) $((m_col+5)) -jr 4 -t "${type_name[${value}_percent]}%" + meter_mod_w=14 + meter_mod_pos=10 + fi + create_meter -v ${type}_${value}_meter -w $((m_width-7-meter_mod_w)) -f -c color_${value}_graph ${type_name[${value}_percent]} + + meter="${type}_${value}_meter" + print -v mem_out -m $((y_pos++)) $((m_col+meter_mod_pos)) -t "${!meter}" -rs -fg $normal_color + + if [[ -z $meter_mod_w ]]; then print -v mem_out -jr 4 -t "${type_name[${value}_percent]}%"; fi + fi + #if [[ $system == "MacOS" && -z $swap_on ]] && ((height>14)); then ((y_pos++)); fi + done + done + + + #* Create text and meters for disks and adapt sizes based on available height + local disk_num disk_name disk_value v_height2 just_val name_len + y_pos=$m_line + m_col=$((m_col+m_width)) + m_width=${box[m_width2]} + v_height=$((${#disks_name[@]})) + unset meter_mod_w meter_mod_pos + + for disk_name in "${disks_name[@]}"; do + if ((y_pos>m_line+height-2)); then break; fi + + #* Print folder disk is mounted on, total size in humanized base 2 bytes and io stats if enabled + print -v mem_out -m $((y_pos++)) $m_col -rs -fg ${theme[title]} -b -t "${disks_name[disk_num]::10}" + name_len=${#disks_name[disk_num]}; if ((name_len>10)); then name_len=10; fi + if [[ -n ${disks_io[disk_num]} && ${disks_io[disk_num]} != "0" ]] && ((m_width-11-name_len>6)); then + print -v mem_out -jc $((m_width-name_len-10)) -rs -fg ${theme[main_fg]} -t "${disks_io[disk_num]::$((m_width-10-name_len))}" + just_val=8 + else + just_val=$((m_width-name_len-2)) + fi + print -v mem_out -jr ${just_val} -fg ${theme[title]} -b -t "${disks_total[disk_num]::$((m_width-11))}" + + for value in "used" "free"; do + if ((height=v_height*5 | tty_width>100)); then + local -n disk_value_percent="disks_${value}_percent" + if ((height<=v_height*5 & tty_width<150)); then + meter_mod_w=12 + meter_mod_pos=7 + ((y_pos--)) + elif ((height<=v_height*5)); then + print -v mem_out -m $((--y_pos)) $((m_col+5)) -jr 4 -t "${disk_value_percent[disk_num]}%" + meter_mod_w=14 + meter_mod_pos=10 + fi + create_meter -v disk_${disk_num}_${value}_meter -w $((m_width-7-meter_mod_w)) -f -c color_${value}_graph ${disk_value_percent[disk_num]} + + meter="disk_${disk_num}_${value}_meter" + print -v mem_out -m $((y_pos++)) $((m_col+meter_mod_pos)) -t "${!meter}" -rs -fg $normal_color + + if [[ -z $meter_mod_w ]]; then print -v mem_out -jr 4 -t "${disk_value_percent[disk_num]}%"; fi + fi + if ((y_pos>m_line+height-1)); then break; fi + done + if ((height>=v_height*4 & height=v_height*6)); then ((y_pos++)); fi + ((++disk_num)) + done + + if ((resized>0)); then ((resized++)); fi + #* Print created text, graph and meters to output variable + draw_out+="${mem_graph[*]}${swap_graph[*]}${mem_out}" + +} + +draw_processes() { #? Draw processes and values to screen + local argument="$1" + if [[ -n $skip_process_draw && $argument != "now" ]]; then return; fi + local line=${box[processes_line]} col=${box[processes_col]} width=${box[processes_width]} height=${box[processes_height]} out_line y=1 fg_step_r=0 fg_step_g=0 fg_step_b=0 checker=2 page_string sel_string + local reverse_string reverse_pos order_left="───────────┤" filter_string current_num detail_location det_no_add com_fg pg_arrow_up_fg pg_arrow_down_fg p_height=$((height-3)) + local pid=0 pid_graph pid_step_r pid_step_g pid_step_b pid_add_r pid_add_g pid_add_b bg_add bg_step proc_start up_fg down_fg page_up_fg page_down_fg this_box=processes + local d_width=${box[details_width]} d_height=${box[details_height]} d_line=${box[details_line]} d_col=${box[details_col]} + local detail_graph_width=$((d_width/3+2)) detail_graph_height=$((d_height-1)) kill_fg det_mod fg_add_r fg_add_g fg_add_b + local right_width=$((d_width-detail_graph_width-2)) + local right_col=$((d_col+detail_graph_width+4)) + local -a pid_rgb=(${theme[proc_misc]}) fg_rgb=(${theme[main_fg_dec]}) + local pid_r=${pid_rgb[0]} pid_g=${pid_rgb[1]} pid_b=${pid_rgb[2]} fg_r=${fg_rgb[0]} fg_g=${fg_rgb[1]} fg_b=${fg_rgb[2]} + + if [[ $argument == "now" ]]; then skip_process_draw=1; fi + + if [[ $proc_gradient == true ]]; then + if ((fg_r+fg_g+fg_b<(255*3)/2)); then + fg_add_r="$(( (fg_r-255-((fg_r-255)/6) )/height))" + fg_add_g="$(( (fg_g-255-((fg_g-255)/6) )/height))" + fg_add_b="$(( (fg_b-255-((fg_b-255)/6) )/height))" + + pid_add_r="$(( (pid_r-255-((pid_r-255)/6) )/height))" + pid_add_g="$(( (pid_g-255-((pid_g-255)/6) )/height))" + pid_add_b="$(( (pid_b-255-((pid_b-255)/6) )/height))" + else + fg_add_r="$(( (fg_r-(fg_r/6) )/height))" + fg_add_g="$(( (fg_g-(fg_g/6) )/height))" + fg_add_b="$(( (fg_b-(fg_b/6) )/height))" + + pid_add_r="$(( (pid_r-(pid_r/6) )/height))" + pid_add_g="$(( (pid_g-(pid_g/6) )/height))" + pid_add_b="$(( (pid_b-(pid_b/6) )/height))" + fi + fi + + unset proc_out + + #* Details box + if ((proc[detailed_change]>0)) || ((proc[detailed]>0 & resized>0)); then + proc[detailed_change]=0 + proc[order_change]=1 + proc[page_change]=1 + if ((proc[detailed]==1)); then + unset proc_det + local enter_fg enter_a_fg misc_fg misc_a_fg i det_y=6 dets cmd_y + + if [[ ${#detail_history[@]} -eq 1 ]] || ((resized>0)); then + unset proc_det2 + create_graph -o detail_graph -d $((d_line+1)) $((d_col+1)) ${detail_graph_height} ${detail_graph_width} -c color_cpu_graph -n detail_history + if ((tty_width>120)); then create_mini_graph -o detail_mem_graph -w $((right_width/3-3)) -nc detail_mem_history; fi + det_no_add=1 + + for detail_location in "${d_line}" "$((d_line+d_height))"; do + print -v proc_det2 -m ${detail_location} $((d_col+1)) -rs -fg ${box[processes_color]} -rp $((d_width-2)) -t "─" + done + for((i=1;i128)); then print -v proc_det2 -r 1 -t "┤" -fg ${theme[title]} -b -t "${proc[detailed_pid]}" -rs -fg ${box[processes_color]} -t "├"; fi + + + + if ((${#proc[detailed_cmd]}>(right_width-6)*2)); then ((det_y--)); dets=2 + elif ((${#proc[detailed_cmd]}>right_width-6)); then dets=1; fi + + print -v proc_det2 -fg ${theme[title]} -b + for i in C M D; do + print -v proc_det2 -m $((d_line+5+cmd_y++)) $right_col -t "$i" + done + + + print -v proc_det2 -m $((d_line+det_y++)) $((right_col+1)) -jc $((right_width-4)) -rs -fg ${theme[main_fg]} -t "${proc[detailed_cmd]::$((right_width-6))}" + if ((dets>0)); then print -v proc_det2 -m $((d_line+det_y++)) $((right_col+2)) -jl $((right_width-6)) -t "${proc[detailed_cmd]:$((right_width-6)):$((right_width-6))}"; fi + if ((dets>1)); then print -v proc_det2 -m $((d_line+det_y)) $((right_col+2)) -jl $((right_width-6)) -t "${proc[detailed_cmd]:$(( (right_width-6)*2 )):$((right_width-6))}"; fi + + fi + + + if ((proc[selected]>0)); then enter_fg="${theme[inactive_fg]}"; enter_a_fg="${theme[inactive_fg]}"; else enter_fg="${theme[title]}"; enter_a_fg="${theme[hi_fg]}"; fi + if [[ -n ${proc[detailed_killed]} ]]; then misc_fg="${theme[title]}"; misc_a_fg="${theme[hi_fg]}" + else misc_fg=$enter_fg; misc_a_fg=$enter_a_fg; fi + print -v proc_det -m ${d_line} $((d_col+d_width-11)) -fg ${box[processes_color]} -t "┤" -fg $enter_fg -b -t "close " -fg $enter_a_fg -t "↲" -rs -fg ${box[processes_color]} -t "├" + if ((tty_width<129)); then det_mod="-8"; fi + + print -v proc_det -m ${d_line} $((d_col+detail_graph_width+4+det_mod)) -t "┤" -fg $misc_a_fg -b -t "t" -fg $misc_fg -t "erminate" -rs -fg ${box[processes_color]} -t "├" + print -v proc_det -r 1 -t "┤" -fg $misc_a_fg -b -t "k" -fg $misc_fg -t "ill" -rs -fg ${box[processes_color]} -t "├" + if ((tty_width>104)); then print -v proc_det -r 1 -t "┤" -fg $misc_a_fg -b -t "i" -fg $misc_fg -t "nterrupt" -rs -fg ${box[processes_color]} -t "├"; fi + + + proc_det="${proc_det2}${proc_det}" + proc_out="${proc_det}" + + elif ((resized==0)); then + unset proc_det + create_box -v proc_out -col ${box[${this_box}_col]} -line ${box[${this_box}_line]} -width ${box[${this_box}_width]} -height ${box[${this_box}_height]} -fill -lc "${box[${this_box}_color]}" -title ${this_box} + fi + fi + + if [[ ${proc[detailed]} -eq 1 ]]; then + local det_status status_color det_columns=3 + if ((tty_width>140)); then ((det_columns++)); fi + if ((tty_width>150)); then ((det_columns++)); fi + if [[ -z $det_no_add && $1 != "now" && -z ${proc[detailed_killed]} ]]; then + create_graph -add-last detail_graph detail_history + if ((tty_width>120)); then create_mini_graph -w $((right_width/3-3)) -nc -add-last detail_mem_graph detail_mem_history; fi + fi + + print -v proc_out -fg ${theme[title]} -b + cmd_y=0 + for i in C P U; do + print -v proc_out -m $((d_line+3+cmd_y++)) $((d_col+1)) -t "$i" + done + print -v proc_out -m $((d_line+1)) $((d_col+1)) -fg ${theme[title]} -t "${proc[detailed_cpu]}%" + + if [[ -n ${proc[detailed_killed]} ]]; then det_status="stopped"; status_color="${theme[inactive_fg]}" + else det_status="running"; status_color="${theme[proc_misc]}"; fi + print -v proc_out -m $((d_line+1)) ${right_col} -fg ${theme[title]} -b -jc $((right_width/det_columns-1)) -t "Status:" -jc $((right_width/det_columns)) -t "Elapsed:" -jc $((right_width/det_columns)) -t "Parent:" + if ((det_columns>=4)); then print -v proc_out -jc $((right_width/det_columns-1)) -t "User:"; fi + if ((det_columns>=5)); then print -v proc_out -jc $((right_width/det_columns-1)) -t "Threads:"; fi + print -v proc_out -m $((d_line+2)) ${right_col} -rs -fg ${status_color} -jc $((right_width/det_columns-1)) -t "${det_status}" -jc $((right_width/det_columns)) -fg ${theme[main_fg]} -t "${proc[detailed_runtime]::$((right_width/det_columns-1))}" -jc $((right_width/det_columns)) -t "${proc[detailed_parent_name]::$((right_width/det_columns-2))}" + if ((det_columns>=4)); then print -v proc_out -jc $((right_width/det_columns-1)) -t "${proc[detailed_user]::$((right_width/det_columns-2))}"; fi + if ((det_columns>=5)); then print -v proc_out -jc $((right_width/det_columns-1)) -t "${proc[detailed_threads]}"; fi + + print -v proc_out -m $((d_line+4)) ${right_col} -fg ${theme[title]} -b -jr $((right_width/3+2)) -t "Memory: ${proc[detailed_mem]}%" -t " " + if ((tty_width>120)); then print -v proc_out -rs -fg ${theme[inactive_fg]} -rp $((right_width/3-3)) "⡀" -l $((right_width/3-3)) -fg ${theme[proc_misc]} -t "${detail_mem_graph}" -t " "; fi + print -v proc_out -fg ${theme[title]} -b -t "${proc[detailed_mem_string]}" + fi + + #* Print processes + if ((${#proc_array[@]}<=p_height)); then + proc[start]=1 + elif (( proc[start]>(${#proc_array[@]}-1)-p_height )); then + proc[start]=$(( (${#proc_array[@]}-1)-p_height )) + fi + + if ((proc[selected]>${#proc_array[@]}-1)); then proc[selected]=$((${#proc_array[@]}-1)); fi + + if [[ $proc_gradient == true ]] && ((proc[selected]>1)); then + fg_r="$(( fg_r-( fg_add_r*(proc[selected]-1) ) ))" + fg_g="$(( fg_g-( fg_add_g*(proc[selected]-1) ) ))" + fg_b="$(( fg_b-( fg_add_b*(proc[selected]-1) ) ))" + + pid_r="$(( pid_r-( pid_add_r*(proc[selected]-1) ) ))" + pid_g="$(( pid_g-( pid_add_g*(proc[selected]-1) ) ))" + pid_b="$(( pid_b-( pid_add_b*(proc[selected]-1) ) ))" + fi + + current_num=1 + + print -v proc_out -rs -m $((line+y++)) $((col+1)) -fg ${theme[title]} -b -t "${proc_array[0]::$((width-3))} " -rs + + local -a out_arr + for out_line in "${proc_array[@]:${proc[start]}}"; do + + if [[ $use_psutil == true ]]; then + out_arr=(${out_line}) + pi=0 + if [[ $proc_tree == true ]]; then + while [[ ! ${out_arr[pi]} =~ ^[0-9]+$ ]]; do ((++pi)); done + fi + pid="${out_arr[pi]}" + + else + pid="${out_line::$((proc[pid_len]+1))}"; pid="${pid// /}" + out_line="${out_line//'\'/'\\'}" + out_line="${out_line//'$'/'\$'}" + out_line="${out_line//'"'/'\"'}" + fi + + pid_graph="pid_${pid}_graph" + + if ((current_num==proc[selected])); then print -v proc_out -bg ${theme[selected_bg]} -fg ${theme[selected_fg]} -b; proc[selected_pid]="$pid" + else print -v proc_out -rs -fg $((fg_r-fg_step_r)) $((fg_g-fg_step_g)) $((fg_b-fg_step_b)); fi + + print -v proc_out -m $((line+y)) $((col+1)) -t "${out_line::$((width-3))} " + + if ((current_num==proc[selected])); then print -v proc_out -rs -bg ${theme[selected_bg]}; fi + + print -v proc_out -m $((line+y)) $((col+width-12)) -fg ${theme[inactive_fg]} -t "⡀⡀⡀⡀⡀" + + if [[ -n ${!pid_graph} ]]; then + print -v proc_out -m $((line+y)) $((col+width-12)) -fg $((pid_r-pid_step_r)) $((pid_g-pid_step_g)) $((pid_b-pid_step_b)) -t "${!pid_graph}" + fi + + ((y++)) + ((current_num++)) + if ((y>height-2)); then break; fi + if [[ $proc_gradient == false ]]; then : + elif ((current_num=proc[selected])); then + fg_step_r=$((fg_step_r+fg_add_r)); fg_step_g=$((fg_step_g+fg_add_g)); fg_step_b=$((fg_step_b+fg_add_b)) + pid_step_r=$((pid_step_r+pid_add_r)); pid_step_g=$((pid_step_g+pid_add_g)); pid_step_b=$((pid_step_b+pid_add_b)) + fi + + done + print -v proc_out -rs + while ((y<=height-2)); do + print -v proc_out -m $((line+y++)) $((col+1)) -rp $((width-2)) -t " " + done + + if ((proc[selected]>0)); then sel_string=$((proc[start]-1+proc[selected])); else sel_string=0; fi + page_string="${sel_string}/$((${#proc_array[@]}-2${filter:++1}))" + print -v proc_out -m $((line+height-1)) $((col+width-20)) -fg ${box[processes_color]} -rp 19 -t "─" + print -v proc_out -m $((line+height-1)) $((col+width-${#page_string}-4)) -fg ${box[processes_color]} -t "┤" -b -fg ${theme[title]} -t "$page_string" -rs -fg ${box[processes_color]} -t "├" + + + if ((proc[order_change]==1 | proc[filter_change]==1 | resized>0)); then + unset proc_misc + proc[order_change]=0 + proc[filter_change]=0 + proc[page_change]=1 + print -v proc_misc -m $line $((col+13)) -fg ${box[processes_color]} -rp $((box[processes_width]-14)) -t "─" -rs + + if ((proc[detailed]==1)); then + print -v proc_misc -m $((d_line+d_height)) $((d_col+detail_graph_width+2)) -fg ${box[processes_color]} -t "┴" -rs + fi + + if ((tty_width>100)); then + reverse_string="-fg ${box[processes_color]} -t ┤ -fg ${theme[hi_fg]}${proc[reverse]:+ -ul} -b -t r -fg ${theme[title]} -t everse -rs -fg ${box[processes_color]} -t ├" + reverse_pos=9 + fi + print -v proc_misc -m $line $((col+width-${#proc_sorting}-14-reverse_pos)) -rs\ + ${reverse_string}\ + -fg ${box[processes_color]} -t ┤ -fg ${theme[title]}${proc[tree]:+ -ul} -b -t "tre" -fg ${theme[hi_fg]} -t "e" -rs -fg ${box[processes_color]} -t ├\ + -fg ${box[processes_color]} -t "┤" -fg ${theme[hi_fg]} -b -t "‹" -fg ${theme[title]} -t " ${proc_sorting} " -fg ${theme[hi_fg]} -t "›" -rs -fg ${box[processes_color]} -t "├" + + if [[ -z $filter && -z $input_to_filter ]]; then + print -v proc_misc -m $line $((col+14)) -fg ${box[processes_color]} -t "┤" -fg ${theme[hi_fg]} -b -t "f" -fg ${theme[title]} -t "ilter" -rs -fg ${box[processes_color]} -t "├" + elif [[ -n $input_to_filter ]]; then + if [[ ${#filter} -le $((width-35-reverse_pos)) ]]; then filter_string="${filter}" + elif [[ ${#filter} -gt $((width-35-reverse_pos)) ]]; then filter_string="${filter: (-$((width-35-reverse_pos)))}" + fi + print -v proc_misc -m $line $((col+14)) -fg ${box[processes_color]} -t "┤" -fg ${theme[title]} -b -t "${filter_string}" -fg ${theme[proc_misc]} -bl -t "█" -rs -fg ${box[processes_color]} -t "├" + elif [[ -n $filter ]]; then + if [[ ${#filter} -le $((width-35-reverse_pos-4)) ]]; then filter_string="${filter}" + elif [[ ${#filter} -gt $((width-35-reverse_pos-4)) ]]; then filter_string="${filter::$((width-35-reverse_pos-4))}" + fi + print -v proc_misc -m $line $((col+14)) -fg ${box[processes_color]} -t "┤" -fg ${theme[hi_fg]} -b -t "f" -fg ${theme[title]} -t " ${filter_string} " -fg ${theme[hi_fg]} -t "c" -rs -fg ${box[processes_color]} -t "├" + fi + + proc_out+="${proc_misc}" + fi + + if ((proc[page_change]==1 | resized>0)); then + unset proc_misc2 + proc[page_change]=0 + if ((proc[selected]>0)); then kill_fg="${theme[hi_fg]}"; com_fg="${theme[title]}"; else kill_fg="${theme[inactive_fg]}"; com_fg="${theme[inactive_fg]}"; fi + if ((proc[selected]==(${#proc_array[@]}-1${filter:++1})-proc[start])); then down_fg="${theme[inactive_fg]}"; else down_fg="${theme[hi_fg]}"; fi + if ((proc[selected]>0 | proc[start]>1)); then up_fg="${theme[hi_fg]}"; else up_fg="${theme[inactive_fg]}"; fi + + print -v proc_misc2 -m $((line+height-1)) $((col+2)) -fg ${box[processes_color]} -t "┤" -fg $up_fg -b -t "↑" -fg ${theme[title]} -t " select " -fg $down_fg -t "↓" -rs -fg ${box[processes_color]} -t "├" + print -v proc_misc2 -r 1 -fg ${box[processes_color]} -t "┤" -fg $com_fg -b -t "info " -fg $kill_fg "↲" -rs -fg ${box[processes_color]} -t "├" + if ((tty_width>100)); then print -v proc_misc2 -r 1 -t "┤" -fg $kill_fg -b -t "t" -fg $com_fg -t "erminate" -rs -fg ${box[processes_color]} -t "├"; fi + if ((tty_width>111)); then print -v proc_misc2 -r 1 -t "┤" -fg $kill_fg -b -t "k" -fg $com_fg -t "ill" -rs -fg ${box[processes_color]} -t "├"; fi + if ((tty_width>126)); then print -v proc_misc2 -r 1 -t "┤" -fg $kill_fg -b -t "i" -fg $com_fg -t "nterrupt" -rs -fg ${box[processes_color]} -t "├"; fi + + proc_out+="${proc_misc2}" + fi + + proc_out="${detail_graph[*]}${proc_out}" + + if ((resized>0)); then ((resized++)); fi + + if [[ $argument == "now" ]]; then + echo -en "${proc_out}" + fi + +} + +draw_net() { #? Draw net information and graphs to screen + local net_out argument=$1 + if [[ -n ${net[no_device]} ]]; then return; fi + if [[ -n $skip_net_draw && $argument != "now" ]]; then return; fi + if [[ $argument == "now" ]]; then skip_net_draw=1; fi + + #* Get variables from previous calculations + local col=$((box[net_col]+1)) line=$((box[net_line]+1)) width=$((box[net_width]-2)) height=$((box[net_height]-2)) + local n_width=${box[n_width]} n_height=${box[n_height]} n_col=${box[n_col]} n_line=${box[n_line]} main_fg="${theme[main_fg]}" + + #* If resized recreate net meter box and net graphs + if ((resized>0)); then + local graph_a_size graph_b_size + graph_a_size=$(( (height)/2 )); graph_b_size=${graph_a_size} + if ((graph_a_size*20)); then + create_graph -o download_graph -d $line $col ${net[graph_a_size]} $((width-n_width-2)) -c color_download_graph -n -max "${net[download_graph_max]}" net_history_download + else + create_graph -max "${net[download_graph_max]}" -add-last download_graph net_history_download + fi + if ((net[upload_redraw]==1 | net[nic_change]==1 | resized>0)); then + create_graph -o upload_graph -d $((line+net[graph_a_size])) $col ${net[graph_b_size]} $((width-n_width-2)) -c color_upload_graph -i -n -max "${net[upload_graph_max]}" net_history_upload + else + create_graph -max "${net[upload_graph_max]}" -i -add-last upload_graph net_history_upload + fi + + if ((net[nic_change]==1 | resized>0)); then + local dev_len=${#net[device]} + if ((dev_len>15)); then dev_len=15; fi + unset net_misc 'net[nic_change]' + print -v net_out -m $((line-1)) $((width-23)) -rs -fg ${box[net_color]} -rp 23 -t "─" + print -v net_misc -m $((line-1)) $((width-7-dev_len)) -rs -fg ${box[net_color]} -t "┤" -fg ${theme[hi_fg]} -b -t "‹b " -fg ${theme[title]} -t "${net[device]::15}" -fg ${theme[hi_fg]} -t " n›" -rs -fg ${box[net_color]} -t "├" + net_out+="${net_misc}" + fi + + #* Create text depening on box height + local ypos=$n_line + + print -v net_out -fg ${main_fg} -m $((ypos++)) $n_col -jl 10 -t "▼ Byte:" -jr 12 -t "${net[speed_download_byteps]}" + if ((height>4)); then print -v net_out -fg ${main_fg} -m $((ypos++)) $n_col -jl 10 -t "▼ Bit:" -jr 12 -t "${net[speed_download_bitps]}"; fi + if ((height>6)); then print -v net_out -fg ${main_fg} -m $((ypos++)) $n_col -jl 10 -t "▼ Total:" -jr 12 -t "${net[total_download]}"; fi + + if ((height>8)); then ((ypos++)); fi + print -v net_out -fg ${main_fg} -m $((ypos++)) $n_col -jl 10 -t "▲ Byte:" -jr 12 -t "${net[speed_upload_byteps]}" + if ((height>7)); then print -v net_out -fg ${main_fg} -m $((ypos++)) $n_col -jl 10 -t "▲ Bit:" -jr 12 -t "${net[speed_upload_bitps]}"; fi + if ((height>5)); then print -v net_out -fg ${main_fg} -m $((ypos++)) $n_col -jl 10 -t "▲ Total:" -jr 12 -t "${net[total_upload]}"; fi + + print -v net_out -fg ${theme[inactive_fg]} -m $line $col -t "${net[download_max_string]}" + print -v net_out -fg ${theme[inactive_fg]} -m $((line+height-1)) $col -t "${net[upload_max_string]}" + + + #* Print graphs and text to output variable + draw_out+="${download_graph[*]}${upload_graph[*]}${net_out}" + if [[ $argument == "now" ]]; then echo -en "${download_graph[*]}${upload_graph[*]}${net_out}"; fi +} + +draw_clock() { #? Draw a clock at top of screen + if [[ -z $draw_clock ]]; then return; fi + if [[ $resized -gt 0 && $resized -lt 5 ]]; then unset clock_out; return; fi + local width=${box[cpu_width]} color=${box[cpu_color]} old_time_string="${time_string}" + #time_string="$(date ${draw_clock})" + printf -v time_string "%(${draw_clock})T" + if [[ $old_time_string != "$time_string" || -z $clock_out ]]; then + unset clock_out + print -v clock_out -m 1 $((width/2-${#time_string}/2)) -rs -fg ${color} -t "┤" -fg ${theme[title]} -b -t "${time_string}" -fg ${color} -t "├" + fi + if [[ $1 == "now" ]]; then echo -en "${clock_out}"; fi +} + +draw_update_string() { + unset update_string + print -v update_string -m ${box[cpu_line]} $((box[cpu_col]+box[cpu_width]-${#update_ms}-14)) -rs -fg ${box[cpu_color]} -t "────┤" -fg ${theme[hi_fg]} -b -t "+" -fg ${theme[title]} -b -t " ${update_ms}ms " -fg ${theme[hi_fg]} -b -t "-" -rs -fg ${box[cpu_color]} -t "├" + if [[ $1 == "quiet" ]]; then draw_out+="${update_string}" + else echo -en "${update_string}"; fi +} + +pause_() { #? Pause input and draw a darkened version of main ui + local pause_out ext_var + if [[ -n $1 && $1 != "off" ]]; then local -n pause_out=${1}; ext_var=1; fi + if [[ $1 != "off" ]]; then + prev_screen="${boxes_out}${proc_det}${last_screen}${net_misc}${mem_out}${detail_graph[*]}${proc_out}${proc_misc}${proc_misc2}${update_string}${clock_out}" + if [[ -n $skip_process_draw ]]; then + prev_screen+="${proc_out}" + unset skip_process_draw proc_out + fi + + unset pause_screen + print -v pause_screen -rs -b -fg ${theme[inactive_fg]} + pause_screen+="${theme[main_bg]}m$(${sed} -E 's/\\e\[[0-9;\-]*m//g' <<< "${prev_screen}")\e[0m" #\e[1;38;5;236 + + if [[ -z $ext_var ]]; then echo -en "${pause_screen}" + else pause_out="${pause_screen}"; fi + + elif [[ $1 == "off" ]]; then + echo -en "${prev_screen}" + unset pause_screen prev_screen + fi +} + +unpause_() { #? Unpause + pause_ off +} + +menu_() { #? Shows the main menu overlay + local menu i count keypress selected_int=0 selected up local_rez d_banner=1 menu_out bannerd skipped menu_pause out_out wait_string trans + local -a menus=("options" "help" "quit") color + unset bannerd menu_out + until false; do + + #* Put program to sleep if caught ctrl-z + if ((sleepy==1)); then sleep_; fi + + if [[ $background_update == true || -z $menu_out ]]; then + draw_clock + pause_ menu_pause + else + unset menu_pause + fi + + unset draw_out + + if [[ -z ${bannerd} ]]; then + draw_banner "$((tty_height/2-10))" bannerd + unset d_banner + fi + if [[ -n ${keypress} || -z ${menu_out} ]]; then + unset menu_out + print -v menu_out -t "${bannerd}" + print -v menu_out -d 1 -rs + selected="${menus[selected_int]}" + unset up + if [[ -n ${theme[main_bg_dec]} ]] && ((${theme[main_bg_dec]// /*}>255**3/2)); then print -v menu_out -bg "#00"; unset trans; else trans=" -trans"; fi + for menu in "${menus[@]}"; do + if [[ $menu == "$selected" ]]; then + local -n menu_array="menu_${menu}_selected" + color=("#c55e5e" "#c23d3d" "#a13030" "#8c2626") + else + local -n menu_array="menu_${menu}" + color=("#bb" "#aa" "#99" "#88") + fi + up=$((up+${#menu_array[@]})) + for((i=0;i<${#menu_array[@]};i++)); do + print -v menu_out -d 1 -fg ${color[i]} -c${trans} -t "${menu_array[i]}" + done + done + print -v menu_out -rs -u ${up} + fi + unset out_out + out_out="${menu_pause}${menu_out}" + echo -e "${out_out}" + + + get_ms timestamp_end + time_left=$((timestamp_start+update_ms-timestamp_end)) + + if ((time_left>1000)); then wait_string=10; time_left=$((time_left-1000)) + elif ((time_left>100)); then wait_string=$((time_left/100)); time_left=0 + else wait_string="0"; time_left=0; fi + + get_key -v keypress -w ${wait_string} + if [[ $(${stty} size) != "$tty_height $tty_width" ]]; then resized; fi + if ((resized>0)); then + calc_sizes; draw_bg quiet; time_left=0; unset menu_out + unset bannerd + echo -en "${clear_screen}" + fi + + case "$keypress" in + up|shift_tab) if ((selected_int>0)); then ((selected_int--)); else selected_int=$((${#menus[@]}-1)); fi ;; + down|tab) if ((selected_int<${#menus[@]}-1)); then ((++selected_int)); else selected_int=0; fi ;; + enter|space) + case "$selected" in + options) options_ ;; + help) help_ ;; + quit) quit_ ;; + esac + ;; + m|M|escape|backspace) break ;; + q|Q) quit_ ;; + esac + + if ((time_left==0)) && [[ -z $keypress ]]; then get_ms timestamp_start; collect_and_draw; fi + if ((resized>=5)); then resized=0; fi + + done + unpause_ + +} + +help_() { #? Shows the help overlay + local help_key from_menu col line y i help_out help_pause redraw=1 wait_string pages page=1 height + local -a shortcuts descriptions + + shortcuts=( + "(Esc, M, m)" + "(F2, O, o)" + "(F1, H, h)" + "(Ctrl-C, Q, q)" + "(+, A, a) (-, S, s)" + "(Up) (Down)" + "(Enter)" + "(Pg Up) (Pg Down)" + "(Home) (End)" + "(Left) (Right)" + "(b, B) (n, N)" + "(E, e)" + "(R, r)" + "(F, f)" + "(C, c)" + "Selected (T, t)" + "Selected (K, k)" + "Selected (I, i)" + " " + " " + " " + ) + descriptions=( + "Shows main menu." + "Shows options." + "Shows this window." + "Quits program." + "Add/Subtract 100ms to/from update timer." + "Select in process list." + "Show detailed information for selected process." + "Jump 1 page in process list." + "Jump to first or last page in process list." + "Select previous/next sorting column." + "Select previous/next network device." + "Toggle processes tree view" + "Reverse sorting order in processes box." + "Input a string to filter processes with." + "Clear any entered filter." + "Terminate selected process with SIGTERM - 15." + "Kill selected process with SIGKILL - 9." + "Interrupt selected process with SIGINT - 2." + " " + "For bug reporting and project updates, visit:" + "\e[1mhttps://github.com/aristocratos/furtop" + ) + + if [[ -n $pause_screen ]]; then from_menu=1; fi + + until [[ -n $help_key ]]; do + + #* Put program to sleep if caught ctrl-z + if ((sleepy==1)); then sleep_; redraw=1; fi + + if [[ $background_update == true || -n $redraw ]]; then + draw_clock + pause_ help_pause + else + unset help_pause + fi + + + if [[ -n $redraw ]]; then + col=$((tty_width/2-36)); line=$((tty_height/2-4)); y=1; height=$((tty_height-2-line)) + if ((${#shortcuts[@]}>height)); then pages=$(( (${#shortcuts[@]}/height)+1 )); else height=${#shortcuts[@]}; unset pages; fi + unset redraw help_out + draw_banner "$((tty_height/2-11))" help_out + print -d 1 + create_box -v help_out -w 72 -h $((height+3)) -l $((line++)) -c $((col++)) -fill -lc ${theme[div_line]} -title "help" + + if [[ -n $pages ]]; then + print -v help_out -m $((line+height+1)) $((col+72-16)) -rs -fg ${theme[div_line]} -t "┤" -fg ${theme[title]} -b -t "pg" -fg ${theme[hi_fg]} -t "↑"\ + -fg ${theme[title]} -t " ${page}/${pages} " -fg ${theme[title]} -t "pg" -fg ${theme[hi_fg]} -t "↓" -rs -fg ${theme[div_line]} -t "├" + fi + ((++col)) + + print -v help_out -m $line $col -fg ${theme[title]} -b -jl 20 -t "Key:" -jl 48 -t "Description:" -m $((line+y++)) $col + + for((i=(page-1)*height;i1000)); then wait_string=10; time_left=$((time_left-1000)) + elif ((time_left>100)); then wait_string=$((time_left/100)); time_left=0 + else wait_string="0"; time_left=0; fi + + get_key -v help_key -w "${wait_string}" + + if [[ -n $pages ]]; then + case $help_key in + down|page_down) if ((page1)); then ((page--)); else page=${pages}; fi; redraw=1; unset help_key ;; + esac + fi + + if [[ $(${stty} size) != "$tty_height $tty_width" ]]; then resized; fi + if ((resized>0)); then + ${sleep} 0.5 + calc_sizes; draw_bg quiet; redraw=1 + d_banner=1 + unset bannerd menu_out + fi + if ((time_left==0)); then get_ms timestamp_start; collect_and_draw; fi + if ((resized>0)); then resized=0; fi + done + + if [[ -n $from_menu ]]; then pause_ + else unpause_; fi +} + +options_() { #? Shows the options overlay + local keypress from_menu col line y=1 i=1 options_out selected_int=0 ypos option_string options_misc option_value bg fg skipped start_t end_t left_t changed_cpu_name theme_int=0 page=1 pages height + local desc_col right left enter lr inp valid updated_ms local_rez redraw_misc=1 desc_pos desc_height options_pause updated_proc inputting inputting_value inputting_key file theme_check net_totals_reset + + if ((net[reset]==1)); then net_totals_reset="On"; else net_totals_reset="Off"; fi + + #* Check theme folder for theme files + get_themes + + desc_color_theme=( "Set furtop color theme." + " " + "Choose between theme files located in" + "\"\$HOME/.config/furtop/themes\" &" + "\"\$HOME/.config/furtop/user_themes" + " " + "User themes are prefixed with \"*\"." + "\"Default\" for builtin default." + " ") + if [[ -z $curled ]]; then desc_color_theme+=("Get more themes at:" + "https://github.com/aristocratos/furtop") + else desc_color_theme+=("\e[1mPress ENTER to download the default themes." + "Will overwrite changes made to the default" + "themes if not copied to user_themes folder."); fi + + desc_update_ms=( "Update time in milliseconds." + "Recommended 2000 ms or above for better sample" + "times for graphs." + " " + "Increases automatically if set below internal" + "loops processing time." + " " + "Max value: 86400000 ms = 24 hours.") + desc_use_psutil=( "Enable the use of psutil python3 module for" + "data collection. Default on non Linux." + "" + "Program will automatically restart if changing" + "this setting to check for compatibility." + " " + "True or false." + " " + "Can only be switched off when on Linux.") + desc_proc_sorting=( "Processes sorting." + "Valid values are \"pid\", \"program\", \"arguments\"," + "\"threads\", \"user\", \"memory\", \"cpu lazy\"" + "\"cpu responsive\" and \"tree\"." + " " + "\"cpu lazy\" shows cpu usage over the lifetime" + "of a process." + " " + "\"cpu responsive\" updates sorting directly at a" + "cost of cpu time (unless using psutil)." + " " + "\"tree\" shows a tree structure of running" + "processes. (not available with psutil)") + desc_proc_tree=( "Processes tree view." + " " + "Set true to show processes grouped by parents," + "with lines drawn between parent and child" + "process." + " " + "True or false.") + desc_check_temp=( "Check cpu temperature." + " " + "True or false." + " " + "Only works if sensors, vcgencmd or osx-cpu-temp" + "commands is available.") + desc_draw_clock=( "Draw a clock at top of screen." + " " + "Formatting according to strftime, empty" + "string to disable." + " " + "\"%X\" locale HH:MM:SS" + "\"%H\" 24h hour, \"%I\" 12h hour" + "\"%M\" minute, \"%S\" second" + "\"%d\" day, \"%m\" month, \"%y\" year") + desc_background_update=( "Update main ui when menus are showing." + " " + "True or false." + " " + "Set this to false if the menus is flickering" + "too much for a comfortable experience.") + desc_custom_cpu_name=( "Custom cpu model name in cpu percentage box." + " " + "Empty string to disable.") + desc_error_logging=("Enable error logging to" + "\"\$HOME/.config/furtop/error.log\"" + " " + "Program will be automatically restarted if" + "changing this option." + " " + "True or false.") + desc_proc_reversed=("Reverse sorting order." + " " + "True or false.") + desc_proc_gradient=("Show color gradient in process list." + " " + "True or False.") + desc_disks_filter=("Optional filter for shown disks." + " " + "Should be names of mountpoints." + "\"root\" replaces \"/\"" + " " + "Separate multiple values with space." + "Example: \"root home external\"") + desc_net_totals_reset=("Press ENTER to toggle network upload" + "and download totals reset." + " " + "Shows totals since system start or" + "network adapter reset when Off.") + desc_proc_per_core=("Process usage per core." + " " + "If process cpu usage should be of the core" + "it's running on or usage of the total" + "available cpu power." + "" + "If true and process is multithreaded" + "cpu usage can reach over 100%.") + desc_update_check=( "Check for updates." + " " + "Enable check for new version from" + "github.com/aristocratos/furtop at start." + " " + "True or False.") + desc_hires_graphs=("Enable high resolution graphs." + " " + "Doubles the horizontal resolution of all" + "graphs. At a cpu usage cost." + "Needs restart to take effect." + " " + "True or False.") + + if [[ -n $pause_screen ]]; then from_menu=1; fi + + until false; do + + #* Put program to sleep if caught ctrl-z + if ((sleepy==1)); then sleep_; fi + + + if [[ $background_update == true || -n $redraw_misc ]]; then + draw_clock + if [[ -z $inputting ]]; then pause_ options_pause; fi + else + unset options_pause + fi + + if [[ -n $redraw_misc ]]; then + unset options_misc redraw_misc + col=$((tty_width/2-39)) + line=$((tty_height/2-4)) + height=$(( (tty_height-2-line)/2 )) + if ((${#options_array[@]}>height)); then pages=$(( (${#options_array[@]}/height)+1 )); else height=${#options_array[@]}; unset pages; fi + desc_col=$((col+30)) + draw_banner "$((tty_height/2-11))" options_misc + create_box -v options_misc -w 29 -h $((height*2+2)) -l $line -c $((col-1)) -fill -lc ${theme[div_line]} -title "options" + if [[ -n $pages ]]; then + print -v options_misc -m $((line+height*2+1)) $((col+29-16)) -rs -fg ${theme[div_line]} -t "┤" -fg ${theme[title]} -b -t "pg" -fg ${theme[hi_fg]} -t "↑"\ + -fg ${theme[title]} -t " ${page}/${pages} " -fg ${theme[title]} -t "pg" -fg ${theme[hi_fg]} -t "↓" -rs -fg ${theme[div_line]} -t "├" + fi + fi + + if [[ -n $keypress || -z $options_out ]]; then + unset options_out desc_height lr inp valid + selected="${options_array[selected_int]}" + local -n selected_desc="desc_${selected}" + if [[ $background_update == false ]]; then desc_pos=$line; desc_height=$((height*2+2)) + elif (( (selected_int-( (page-1)*height) )*2+${#selected_desc[@]}/dev/null)) + if [[ ${theme_index[*]} =~ .theme ]]; then + for git_theme in ${theme_index[@]}; do + unset new_theme + if [[ ! -e "${config_dir}/themes/${git_theme}" ]]; then new_theme=1; fi + if curl -m 3 --raw "https://raw.githubusercontent.com/aristocratos/furtop/master/themes/${git_theme}" >"${config_dir}/themes/${git_theme}" 2>/dev/null; then + ((++down_themes)) + if [[ -n $new_theme ]]; then + ((++new_themes)) + themes+=("themes/${git_theme%.theme}") + fi + fi + done + desc_color_theme+=("Downloaded ${down_themes} theme(s).") + desc_color_theme+=("Found ${new_themes} new theme(s)!") + else + desc_color_theme+=("ERROR: Couldn't get theme index!") + fi + fi + + + get_ms timestamp_end + if [[ -z $theme_check ]]; then time_left=$((timestamp_start+update_ms-timestamp_end)) + else unset theme_check; time_left=0; fi + + if ((time_left>500)); then wait_string=5; time_left=$((time_left-500)) + elif ((time_left>100)); then wait_string=$((time_left/100)); time_left=0 + else wait_string="0"; time_left=0; fi + + get_key -v keypress -w ${wait_string} + + if [[ -n $inputting ]]; then + case "$keypress" in + escape) unset inputting inputting_value ;; + enter|backspace) valid=1 ;; + *) if [[ ${#keypress} -eq 1 ]]; then valid=1; fi ;; + esac + else + case "$keypress" in + escape|q|backspace) break 1 ;; + down|tab) if ((selected_int<${#options_array[@]}-1)); then ((++selected_int)); else selected_int=0; fi ;; + up|shift_tab) if ((selected_int>0)); then ((selected_int--)); else selected_int=$((${#options_array[@]}-1)); fi ;; + left|right) if [[ -n $lr && -z $inputting ]]; then valid=1; fi ;; + enter) if [[ -n $inp ]]; then valid=1; fi ;; + page_down) if ((page1)); then ((page--)); else page=${pages}; fi; redraw_misc=1; selected_int=$(( (page-1)*height )) ;; + esac + if (( selected_int<(page-1)*height | selected_int>=page*height )); then page=$(( (selected_int/height)+1 )); redraw_misc=1; fi + fi + + if [[ ${selected} == "color_theme" && ${keypress} =~ left|right && ${#themes} -lt 2 ]]; then unset valid; fi + + if [[ -n $valid ]]; then + case "${selected} ${keypress}" in + "update_ms right") + if ((update_ms<86399900)); then + update_ms=$((update_ms+100)) + updated_ms=1 + fi + ;; + "update_ms left") + if ((update_ms>100)); then + update_ms=$((update_ms-100)) + updated_ms=1 + fi + ;; + "update_ms enter") + if [[ -z $inputting ]]; then inputting=1; inputting_value="${update_ms}" + else + if ((inputting_value<86400000)); then update_ms="${inputting_value:-0}"; updated_ms=1; fi + unset inputting inputting_value + fi + ;; + "update_ms backspace"|"draw_clock backspace"|"custom_cpu_name backspace"|"disks_filter backspace") + if [[ ${#inputting_value} -gt 0 ]]; then + inputting_value="${inputting_value::-1}" + fi + ;; + "update_ms"*) + inputting_value+="${keypress//[^0-9]/}" + ;; + "draw_clock enter") + if [[ -z $inputting ]]; then inputting=1; inputting_value="${draw_clock}" + else draw_clock="${inputting_value}"; unset inputting inputting_value clock_out; fi + ;; + "custom_cpu_name enter") + if [[ -z $inputting ]]; then inputting=1; inputting_value="${custom_cpu_name}" + else custom_cpu_name="${inputting_value}"; changed_cpu_name=1; unset inputting inputting_value; fi + ;; + "disks_filter enter") + if [[ -z $inputting ]]; then inputting=1; inputting_value="${disks_filter}" + else disks_filter="${inputting_value}"; mem[counter]=10; resized=1; unset inputting inputting_value; fi + ;; + "net_totals_reset enter") + if ((net[reset]==1)); then net_totals_reset="Off"; net[reset]=0 + else net_totals_reset="On"; net[reset]=1; fi + ;; + "check_temp"*|"error_logging"*|"background_update"*|"proc_reversed"*|"proc_gradient"*|"proc_per_core"*|"update_check"*|"hires_graphs"*|"use_psutil"*|"proc_tree"*) + local -n selected_var=${selected} + if [[ ${selected_var} == "true" ]]; then + selected_var="false" + if [[ $selected == "proc_reversed" ]]; then proc[order_change]=1; unset 'proc[reverse]' + elif [[ $selected == "proc_tree" ]]; then proc[order_change]=1; unset 'proc[tree]'; fi + else + selected_var="true" + if [[ $selected == "proc_reversed" ]]; then proc[order_change]=1; proc[reverse]="+" + elif [[ $selected == "proc_tree" ]]; then proc[order_change]=1; proc[tree]="+"; fi + fi + if [[ $selected == "check_temp" && $check_temp == true ]]; then + local has_temp + sensor_comm="" + if [[ $use_psutil == true ]]; then + py_command -v has_temp "get_sensors_check()" + if [[ $has_temp == true ]]; then sensor_comm="psutil"; fi + fi + if [[ -z $sensor_comm ]]; then + local checker + for checker in "vcgencmd" "sensors" "osx-cpu-temp"; do + if command -v "${checker}" >/dev/null 2>&1; then sensor_comm="${checker}"; break; fi + done + fi + if [[ -z $sensor_comm ]]; then check_temp="false" + else resized=1; fi + elif [[ $selected == "check_temp" ]]; then + resized=1 + fi + if [[ $selected == "use_psutil" && $system != "Linux" ]]; then use_psutil="true" + elif [[ $selected == "use_psutil" ]]; then quit_ restart psutil; fi + if [[ $selected == "error_logging" ]]; then quit_ restart; fi + + ;; + "proc_sorting right") + if ((proc[sorting_int]<${#sorting[@]}-1)); then ((++proc[sorting_int])) + else proc[sorting_int]=0; fi + proc_sorting="${sorting[proc[sorting_int]]}" + proc[order_change]=1 + ;; + "proc_sorting left") + if ((proc[sorting_int]>0)); then ((proc[sorting_int]--)) + else proc[sorting_int]=$((${#sorting[@]}-1)); fi + proc_sorting="${sorting[proc[sorting_int]]}" + proc[order_change]=1 + ;; + "color_theme right") + if ((theme_int<${#themes[@]}-1)); then ((++theme_int)) + else theme_int=0; fi + color_theme="${themes[$theme_int]}" + color_init_ + resized=1 + ;; + "color_theme left") + if ((theme_int>0)); then ((theme_int--)) + else theme_int=$((${#themes[@]}-1)); fi + color_theme="${themes[$theme_int]}" + color_init_ + resized=1 + ;; + "color_theme enter") + theme_check=1 + if ((${#desc_color_theme[@]}>8)); then unset 'desc_color_theme[-1]'; fi + desc_color_theme+=("Checking for new themes...") + ;; + "draw_clock"*|"custom_cpu_name"*|"disks_filter"*) + inputting_value+="${keypress//[\\\$\"\']/}" + ;; + + esac + + fi + + if [[ -n $changed_cpu_name ]]; then + changed_cpu_name=0 + get_cpu_info + calc_sizes + draw_bg quiet + fi + + if [[ $(${stty} size) != "$tty_height $tty_width" ]]; then resized; fi + + if ((resized>0)); then + calc_sizes; draw_bg quiet + redraw_misc=1 + unset options_out bannerd menu_out + fi + + get_ms timestamp_end + time_left=$((timestamp_start+update_ms-timestamp_end)) + if ((time_left<=0 | resized>0)); then get_ms timestamp_start; if [[ -z $inputting ]]; then collect_and_draw; fi; fi + if ((resized>0)); then resized=0; page=1; selected_int=0; fi + + if [[ -n $updated_ms ]] && ((updated_ms++==2)); then + unset updated_ms + draw_update_string quiet + fi + + done + + if [[ -n $from_menu ]]; then pause_ + elif [[ -n ${pause_screen} ]]; then unpause_; draw_update_string; fi +} + +killer_() { #? Kill process with selected signal + local kill_op="$1" kill_pid="$2" killer_out killer_box col line program keypress selected selected_int=0 sig confirmed=0 option killer_pause status msg + local -a options=("yes" "no") + + if ! program="$(ps -o comm -p ${kill_pid})"; then return + else program="$(tail -n1 <<<"$program")"; fi + + case $kill_op in + t|T) kill_op="terminate"; sig="SIGTERM" ;; + k|K) kill_op="kill"; sig="SIGKILL" ;; + i|I) kill_op="interrupt"; sig="SIGINT" ;; + esac + + until false; do + + #* Put program to sleep if caught ctrl-z + if ((sleepy==1)); then sleep_; fi + + if [[ $background_update == true || -z $killer_box ]]; then + draw_clock + pause_ killer_pause + else + unset killer_pause + fi + + if [[ -z $killer_box ]]; then + col=$((tty_width/2-15)); line=$((tty_height/2-4)); y=1 + unset redraw killer_box + create_box -v killer_box -w 40 -h 9 -l $line -c $((col++)) -fill -lc "${theme[proc_box]}" -title "${kill_op}" + fi + + if ((confirmed==0)); then + selected="${options[selected_int]}" + print -v killer_out -m $((line+2)) $col -fg ${theme[title]} -b -jc 38 -t "${kill_op^} ${program::20}?" -m $((line+4)) $((col+3)) + for option in "${options[@]}"; do + if [[ $option == "${selected}" ]]; then print -v killer_out -bg ${theme[selected_bg]} -fg ${theme[selected_fg]}; else print -v killer_out -fg ${theme[title]}; fi + print -v killer_out -b -r 5 -t "[ ${option^} ]" -rs + done + + elif ((confirmed==1)); then + selected="ok" + print -v killer_out -m $((line+2)) $col -fg ${theme[title]} -b -jc 38 -t "Sending signal ${sig} to pid ${kill_pid}!" + print -v killer_out -m $((line+4)) $col -fg ${theme[main_fg]} -jc 38 -t "${status^}!" -m $((line+6)) $col + if [[ -n $msg ]]; then print -v killer_out -m $((line+5)) $col -fg ${theme[main_fg]} -jc 38 -t "${msg}" -m $((line+7)) $col; fi + print -v killer_out -fg ${theme[selected_fg]} -bg ${theme[selected_bg]} -b -r 15 -t "[ Ok ]" -rs + fi + + echo -en "${killer_pause}${killer_box}${killer_out}" + unset killer_out draw_out + + + get_ms timestamp_end + time_left=$((timestamp_start+update_ms-timestamp_end)) + + if ((time_left>1000)); then wait_string=10; time_left=$((time_left-1000)) + elif ((time_left>100)); then wait_string=$((time_left/100)); time_left=0 + else wait_string="0"; time_left=0; fi + + get_key -v keypress -w ${wait_string} + if [[ $(${stty} size) != "$tty_height $tty_width" ]]; then resized; fi + if ((resized>0)); then + calc_sizes; draw_bg quiet; time_left=0; unset killer_out killer_box + fi + + case "$keypress" in + right|shift_tab) if ((selected_int>0)); then ((selected_int--)); else selected_int=$((${#options[@]}-1)); fi ;; + left|tab) if ((selected_int<${#options[@]}-1)); then ((++selected_int)); else selected_int=0; fi ;; + enter) + case "$selected" in + yes) confirmed=1 ;; + no|ok) confirmed=-1 ;; + esac + ;; + q|Q) quit_ ;; + esac + + if ((confirmed<0)); then + unpause_ + break + elif ((confirmed>0)) && [[ -z $status ]]; then + if ${kill} -${sig} ${kill_pid} >/dev/null 2>&1; then + status="success" + else + if ! ps -p ${kill_pid} >/dev/null 2>&1; then + msg="Process not running." + elif [[ $UID != 0 ]]; then + msg="Try restarting with sudo." + else + msg="Unknown error." + fi + status="failed"; fi + fi + + + if ((time_left==0)); then get_ms timestamp_start; unset draw_out; collect_and_draw; fi + if ((resized>=5)); then resized=0; fi + + done + + +} + +get_key() { #? Get one key from standard input and translate key code to readable format + local key key_out wait_time esc ext_out save + + if ((quitting==1)); then quit_; fi + + until (($#==0)); do + case "$1" in + -v|-variable) local -n key_out=$2; ext_out=1; shift;; #* Output variable + -w|-wait) wait_time="$2"; shift;; #* Time to wait for key + -s|-save) save=1;; #* Save key for later processing + esac + shift + done + + if [[ -z $save && -n ${saved_key[0]} ]]; then key="${saved_key[0]}"; unset 'saved_key[0]'; saved_key=("${saved_key[@]}") + else + unset key + + key=$(${stty} -cooked min 0 time ${wait_time:-0} 2>/dev/null; ${dd} bs=1 count=1 2>/dev/null) + if [[ -z ${key:+s} ]]; then + key_out="" + ${stty} isig + if [[ -z $save ]]; then return 0 + else return 1; fi + fi + + #* Read 3 more characters if a leading escape character is detected + if [[ $key == "${enter_key}" ]]; then key="enter" + elif [[ $key == "${ctrl_c}" ]]; then quitting=1; time_left=0 + elif [[ $key == "${ctrl_z}" ]]; then sleepy=1; time_left=0 + elif [[ $key == "${backspace}" || $key == "${backspace_real}" ]]; then key="backspace" + elif [[ $key == "${tab}" ]]; then key="tab" + elif [[ $key == "$esc_character" ]]; then + esc=1; key=$(${stty} -cooked min 0 time 0 2>/dev/null; ${dd} bs=1 count=3 2>/dev/null); fi + if [[ -z $key && $esc -eq 1 ]]; then key="escape" + elif [[ $esc -eq 1 ]]; then + case "${key}" in + '[A'*|'OA'*) key="up" ;; + '[B'*|'OB'*) key="down" ;; + '[D'*|'OD'*) key="left" ;; + '[C'*|'OC'*) key="right" ;; + '[2~') key="insert" ;; + '[3~') key="delete" ;; + '[H'*) key="home" ;; + '[F'*) key="end" ;; + '[5~') key="page_up" ;; + '[6~') key="page_down" ;; + '[Z'*) key="shift_tab" ;; + 'OP'*) key="f1";; + 'OQ'*) key="f2";; + 'OR'*) key="f3";; + 'OS'*) key="f4";; + '[15') key="f5";; + '[17') key="f6";; + '[18') key="f7";; + '[19') key="f8";; + '[20') key="f9";; + '[21') key="f10";; + '[23') key="f11";; + '[24') key="f12";; + *) key="" ;; + esac + fi + + fi + + ${stty} -cooked min 0 time 0 >/dev/null 2>&1; ${dd} bs=512 count=1 >/dev/null 2>&1 + ${stty} isig + if [[ -n $save && -n $key ]]; then saved_key+=("${key}"); return 0; fi + + if [[ -n $ext_out ]]; then key_out="${key}" + else echo -n "${key}"; fi +} + +process_input() { #? Process keypresses for main ui + local wait_time="$1" keypress esc prev_screen anykey filter_change p_height=$((box[processes_height]-3)) + late_update=0 + #* Wait while reading input + get_key -v keypress -w "${wait_time}" + if [[ -z $keypress ]] || [[ -n $failed_pipe ]]; then return; fi + + if [[ -n $input_to_filter ]]; then + filter_change=1 + case "$keypress" in + "enter") unset input_to_filter ;; + "backspace") if [[ ${#filter} -gt 0 ]]; then filter="${filter:: (-1)}"; else unset filter_change; fi ;; + "escape") unset input_to_filter filter ;; + *) if [[ ${#keypress} -eq 1 && $keypress =~ ^[A-Za-z0-9\!\@\#\%\&\/\(\)\[\+\-\_\*\,\;\.\:]$ ]]; then filter+="${keypress//[\\\$\"\']/}"; else unset filter_change; fi ;; + esac + + else + case "$keypress" in + left) #* Move left in processes sorting column + if ((proc[sorting_int]>0)); then ((proc[sorting_int]--)) + else proc[sorting_int]=$((${#sorting[@]}-1)); fi + proc_sorting="${sorting[proc[sorting_int]]}" + if [[ $proc_sorting == "tree" && $use_psutil == true ]]; then + ((proc[sorting_int]--)) + proc_sorting="${sorting[proc[sorting_int]]}" + fi + filter_change=1 + ;; + right) #* Move right in processes sorting column + if ((proc[sorting_int]<${#sorting[@]}-1)); then ((++proc[sorting_int])) + else proc[sorting_int]=0; fi + proc_sorting="${sorting[proc[sorting_int]]}" + if [[ $proc_sorting == "tree" && $use_psutil == true ]]; then + proc[sorting_int]=0 + proc_sorting="${sorting[proc[sorting_int]]}" + fi + filter_change=1 + ;; + n|N) #* Switch to next network device + if ((${#nic_list[@]}>1)); then + if ((nic_int<${#nic_list[@]}-1)); then ((++nic_int)) + else nic_int=0; fi + net[device]="${nic_list[nic_int]}" + net[nic_change]=1 + collect_net init + collect_net + draw_net now + fi + ;; + b|B) #* Switch to previous network device + if ((${#nic_list[@]}>1)); then + if ((nic_int>0)); then ((nic_int--)) + else nic_int=$((${#nic_list[@]}-1)); fi + net[device]="${nic_list[nic_int]}" + net[nic_change]=1 + collect_net init + collect_net + draw_net now + fi + ;; + up|shift_tab) #* Move process selector up one + if ((proc[selected]>1)); then + ((proc[selected]--)) + proc[page_change]=1 + elif ((proc[start]>1)); then + if ((proc[selected]==0)); then proc[selected]=${p_height}; fi + ((proc[start]--)) + proc[page_change]=1 + elif ((proc[start]==1 & proc[selected]==1)); then + proc[selected]=0 + proc[page_change]=1 + fi + ;; + down|tab) #* Move process selector down one + if ((proc[selected]0 & proc[detailed_pid]!=proc[selected_pid])) && ps -p ${proc[selected_pid]} > /dev/null 2>&1; then + proc[detailed]=1 + proc[detailed_change]=1 + proc[detailed_pid]=${proc[selected_pid]} + proc[selected]=0 + unset 'proc[detailed_name]' 'detail_history[@]' 'detail_mem_history[@]' 'proc[detailed_killed]' + calc_sizes + collect_processes now + elif ((proc[detailed]==1 & proc[detailed_pid]!=proc[selected_pid])); then + proc[detailed]=0 + proc[detailed_change]=1 + unset 'proc[detailed_pid]' + calc_sizes + fi + ;; + page_up) #* Move up one page in process box + if ((proc[start]>1)); then + proc[start]=$(( proc[start]-p_height )) + if ((proc[start]<1)); then proc[start]=1; fi + proc[page_change]=1 + elif ((proc[selected]>0)); then + proc[selected]=0 + proc[start]=1 + proc[page_change]=1 + fi + ;; + page_down) #* Move down one page in process box + if ((proc[start]<(${#proc_array[@]}-1)-p_height)); then + if ((proc[start]==1)) && [[ $use_psutil == false ]]; then collect_processes now; fi + proc[start]=$(( proc[start]+p_height )) + if (( proc[start]>(${#proc_array[@]})-p_height )); then proc[start]=$(( (${#proc_array[@]})-p_height )); fi + proc[page_change]=1 + elif ((proc[selected]>0)); then + proc[selected]=$((p_height)) + proc[page_change]=1 + fi + ;; + home) #* Go to first page in process box + proc[start]=1 + proc[page_change]=1 + ;; + end) #* Go to last page in process box + if ((proc[selected]==0)) && [[ $use_psutil == false ]]; then collect_processes now; fi + proc[start]=$(((${#proc_array[@]}-1)-p_height)) + proc[page_change]=1 + ;; + r|R) #* Reverse order of processes sorting column + if [[ -z ${proc[reverse]} ]]; then + proc[reverse]="+" + proc_reversed="true" + else + proc_reversed="false" + unset 'proc[reverse]' + fi + filter_change=1 + ;; + e|E) #* Show processes as a tree + if [[ -z ${proc[tree]} ]]; then + proc[tree]="+" + proc_tree="true" + else + proc_tree="false" + unset 'proc[tree]' + fi + filter_change=1 + ;; + o|O|f2) #* Options + options_ + ;; + +|A|a) #* Add 100ms to update timer + if ((update_ms<86399900)); then + update_ms=$((update_ms+100)) + draw_update_string + fi + ;; + -|S|s) #* Subtract 100ms from update timer + if ((update_ms>100)); then + update_ms=$((update_ms-100)) + draw_update_string + fi + ;; + h|H|f1) #* Show help + help_ + ;; + q|Q) #* Quit + quit_ + ;; + m|M|escape) #* Show main menu + menu_ + ;; + f|F) #* Start process filtering input + input_to_filter=1 + filter_change=1 + if ((proc[selected]>1)); then proc[selected]=1; fi + proc[start]=1 + ;; + c|C) #* Clear process filter + if [[ -n $filter ]]; then + unset input_to_filter filter + filter_change=1 + fi + ;; + t|T|k|K|i|I) #* Send terminate, kill or interrupt signal + if [[ ${proc[selected]} -gt 0 ]]; then + killer_ "$keypress" "${proc[selected_pid]}" + elif [[ ${proc[detailed]} -eq 1 && -z ${proc[detailed_killed]} ]]; then + killer_ "$keypress" "${proc[detailed_pid]}" + fi + ;; + esac + fi + + if [[ -n $filter_change ]]; then + unset filter_change + collect_processes now + proc[filter_change]=1 + draw_processes now + elif [[ ${proc[page_change]} -eq 1 || ${proc[detailed_change]} == 1 ]]; then + if ((proc[selected]==0)); then unset 'proc[selected_pid]'; proc[detailed_change]=1; fi + draw_processes now + fi + + #* Subtract time since input start from time left if timer is interrupted + get_ms timestamp_input_end + time_left=$(( (timestamp_start+update_ms)-timestamp_input_end )) + + return 0 +} + +collect_and_draw() { #? Run all collect and draw functions + local task_int=0 input_runs + for task in processes cpu mem net; do + ((++task_int)) + if [[ -n $pause_screen && -n ${saved_key[0]} ]]; then + return + elif [[ -z $pause_screen ]]; then + input_runs=0 + while [[ -n ${saved_key[0]} ]] && ((time_left>0)) && ((++input_runs<=5)); do + process_input + unset late_update + done + fi + collect_${task} + if get_key -save && [[ -z $pause_screen ]]; then process_input; fi + draw_${task} + if get_key -save && [[ -z $pause_screen ]]; then process_input; fi + draw_clock "$1" + if ((resized>0 & resized0)); then + calc_sizes + draw_bg + fi + + #* Run all collect and draw functions + collect_and_draw now + + #* Reset resized variable if resized and all functions have finished redrawing + if ((resized>=5)); then resized=0 + elif ((resized>0)); then unset draw_out proc_out clock_out; return; fi + + #* Echo everyting out to screen in one command to get a smooth transition between updates + echo -en "${draw_out}${proc_out}${clock_out}" + unset draw_out + + #* Periodically check for new network device if non was found at start or was removed + if ((net[device_check]>10)); then + net[device_check]=0 + get_net_device + elif [[ -n ${net[no_device]} ]]; then + ((++net[device_check])) + fi + + #* Compare timestamps to get exact time needed to wait until next loop + get_ms timestamp_end + time_left=$((timestamp_start+update_ms-timestamp_end)) + if ((time_left>update_ms)); then time_left=$update_ms; fi + if ((time_left>0)); then + + late_update=0 + + #* Divide waiting time in chunks of 500ms and below to keep program responsive while reading input + while ((time_left>0 & resized==0)); do + + #* If NOT waiting for input and time left is greater than 500ms, wait 500ms and loop + if [[ -z $input_to_filter ]] && ((time_left>=500)); then + wait_string="5" + time_left=$((time_left-500)) + + #* If waiting for input and time left is greater than "50 ms", wait 50ms and loop + elif [[ -n $input_to_filter ]] && ((time_left>=100)); then + wait_string="1" + time_left=$((time_left-100)) + + #* Else format wait string with padded zeroes if needed and break loop + else + if ((time_left>=100)); then wait_string=$((time_left/100)); else wait_string=0; fi + time_left=0 + fi + + #* Wait while reading input + process_input "${wait_string}" + if [[ -n $failed_pipe || -n $py_error ]]; then return; fi + + #* Draw clock if set + draw_clock now + + done + + #* If time left is too low to process any input more than five times in succession, add 100ms to update timer + elif ((++late_update==5)); then + update_ms=$((update_ms+100)) + draw_update_string + fi + + unset skip_process_draw skip_net_draw +} + +#? Pre main loop + +#* Read config file or create if non existant +config_dir="${XDG_CONFIG_HOME:-$HOME/.config}/furtop" +if [[ -d "${config_dir}" && -w "${config_dir}" ]] || mkdir -p "${config_dir}"; then + if [[ ! -d "${config_dir}/themes" ]]; then mkdir -p "${config_dir}/themes"; fi + if [[ ! -d "${config_dir}/user_themes" ]]; then mkdir -p "${config_dir}/user_themes"; fi + config_file="${config_dir}/furtop.cfg" + # shellcheck source=/dev/null + if [[ -e "$config_file" ]]; then + source "$config_file" + + #* If current config is from an older version recreate config file and save user changes + if [[ $(get_value -sf "${config_file}" -k "furtop v." -mk 1) != "${version}" ]]; then + create_config + save_config "${save_array[@]}" + fi + else create_config; fi +else + #* If anything goes wrong turn off all writing to filesystem + echo "ERROR: Could not set config dir!" + config_dir="/dev/null" + config_file="/dev/null" + error_logging="false" + unset 'save_array[@]' +fi + +#* Force the use of python psutil if not on Linux +if [[ $system != "Linux" ]]; then use_psutil="true"; fi + +#* Check for python3 and psutil if "use_psutil" is true +if [[ $use_psutil == true ]]; then + if ! command -v python3 >/dev/null 2>&1; then + echo "Error: Missing python3!" + if [[ $system == "Linux" ]]; then + use_psutil="false" + else + exit 1 + fi + elif [[ $use_psutil == true ]] && ! (cd / && python3 -c "import psutil") >/dev/null 2>&1; then + echo "Error: Missing python3 psutil module!" + if [[ $system == "Linux" ]]; then + use_psutil="false" + else + exit 1 + fi + fi +fi + +#* If using bash version 5, set timestamps with EPOCHREALTIME variable +if [[ -n $EPOCHREALTIME ]]; then + get_ms() { #? Set given variable to current epoch millisecond with EPOCHREALTIME varialble + local -n ms_out=$1 + ms_out=$((${EPOCHREALTIME/[.,]/}/1000)) + } + +#* If not, but using psutil, set timestamps with python +elif [[ $use_psutil == true ]]; then + get_ms() { + local -n ms_out=$1 + py_command -v ms_out "get_ms()" + } + +#* Else use date command +else + get_ms() { #? Set given variable to current epoch millisecond with date command + local -n ms_out=$1 + ms_out="" + read ms_out < <(${date} +%s%3N) + } +fi + +#* Setup psutil script +if [[ $use_psutil == true ]]; then + py_command() { + if [[ -n $failed_pipe ]]; then return; fi + local arr var output cmd pyerr=${py_error} ln + case $1 in + "quit") + echo "quit" >&${pycoproc[1]} 2>/dev/null || true + return + ;; + "-v") var=1;; + "-vn") var=1; ln=1;; + "-a") arr=1;; + *) return;; + esac + local -n pyout=$2 + cmd="$3" + + echo "${cmd}" >&${pycoproc[1]} #2>/dev/null || true + if [[ -n $var ]]; then pyout="" + else pyout=(); fi + while IFS= read -r -u ${pycoproc[0]} -t 1 output; do #2>/dev/null + if [[ $output == '/EOL' ]]; then break; fi + if [[ -n $failed_pipe ]]; then py_out=""; return 1; fi + if [[ $output == '/ERROR' ]]; then ((++py_error)); unset arr var; fi + if [[ -n $arr ]]; then pyout+=("${output}") + elif [[ -n $var ]]; then pyout+="${output}${ln:+\n}"; fi + done + if ((py_error>pyerr)); then py_out=""; return 1; fi + if [[ -n $ln ]]; then printf -v pyout "%b" "${pyout}"; fi + return 0 + } + + if ! pytmpdir=$(mktemp -d "${TMPDIR:-/tmp}"/XXXXXXXXXXXX); then + if [[ $system == "Linux" ]]; then + use_psutil="false" + else + echo "ERROR: Failed setting up temp directory for psutil script!" + exit 1 + fi + else + pywrapper="${pytmpdir}/furtop.psutil" + +cat << 'EOF' > "${pywrapper}" +import os, sys, subprocess, re, time, psutil +from datetime import timedelta +from collections import defaultdict +from typing import List, Set, Dict, Tuple, Optional, Union + +system: str +if "linux" in sys.platform: system = "Linux" +elif "bsd" in sys.platform: system = "BSD" +elif "darwin" in sys.platform: system = "MacOS" +else: system = "Other" + +parent_pid: int = psutil.Process(os.getpid()).ppid() + +allowed_commands: Tuple[str] = ( + 'get_proc', + 'get_disks', + 'get_cpu_name', + 'get_cpu_cores', + 'get_nics', + 'get_cpu_cores', + 'get_cpu_usage', + 'get_cpu_freq', + 'get_uptime', + 'get_load_avg', + 'get_mem', + 'get_detailed_names_cmd', + 'get_detailed_mem_time', + 'get_net', + 'get_cmd_out', + 'get_sensors', + 'get_sensors_check', + 'get_ms' + ) +command: str = '' +cpu_count: int = psutil.cpu_count() +disk_hist: Dict = {} + +def cleaned(string: str) -> str: + '''Escape characters not suitable for "echo -e" in bash''' + return string.replace("\\", "\\\\").replace("$", "\\$").replace("\n", "\\n").replace("\t", "\\t").replace("\"", "\\\"").replace("\'", "\\\'") + +def get_cmd_out(cmd: str): + '''Save bash the trouble of creating child processes by running through python instead''' + print(subprocess.check_output(cmd, shell=True, universal_newlines=True).rstrip()) + +def get_ms(): + '''Get current epoch millisecond''' + t = str(time.time()).split(".") + print(f'{t[0]}{t[1][:3]}') + +def get_sensors(): + '''A clone of "sensors" but using psutil''' + temps = psutil.sensors_temperatures() + if not temps: + return + try: + for name, entries in temps.items(): + print(name) + for entry in entries: + print(f'{entry.label or name}: {entry.current}°C (high = {entry.high}°C, crit = {entry.critical}°C)') + print() + except: + pass + +def get_sensors_check(): + '''Check if get_sensors() output contains accepted CPU temperature values''' + if not hasattr(psutil, "sensors_temperatures"): print("false"); return + try: + temps = psutil.sensors_temperatures() + except: + pass + print("false"); return + if not temps: print("false"); return + try: + for _, entries in temps.items(): + for entry in entries: + if entry.label.startswith(('Package', 'Core 0', 'Tdie')): + print("true") + return + except: + pass + print("false") + +def get_cpu_name(): + '''Fetch a suitable CPU identifier from the CPU model name string''' + name: str = "" + command: str = "" + all_info: str = "" + rem_line: str = "" + if system == "Linux": + command = "cat /proc/cpuinfo" + rem_line = "model name" + elif system == "MacOS": + command ="sysctl -n machdep.cpu.brand_string" + elif system == "BSD": + command ="sysctl hw.model" + rem_line = "hw.model" + + all_info = subprocess.check_output("LANG=C " + command, shell=True, universal_newlines=True) + if rem_line: + for line in all_info.split("\n"): + if rem_line in line: + name = re.sub( ".*" + rem_line + ".*:", "", line,1).lstrip() + else: + name = all_info + if "Xeon" in name: + name = name.split(" ") + name = name[name.index("CPU")+1] + elif "Ryzen" in name: + name = name.split(" ") + name = " ".join(name[name.index("Ryzen"):name.index("Ryzen")+3]) + elif "CPU" in name: + name = name.split(" ") + name = name[name.index("CPU")-1] + + print(name) + +def get_cpu_cores(): + '''Get number of CPU cores and threads''' + cores: int = psutil.cpu_count(logical=False) + threads: int = psutil.cpu_count(logical=True) + print(f'{cores} {threads if threads else cores}') + +def get_cpu_usage(): + cpu: float = psutil.cpu_percent(percpu=False) + threads: List[float] = psutil.cpu_percent(percpu=True) + print(f'{cpu:.0f}') + for thread in threads: + print(f'{thread:.0f}') + +def get_cpu_freq(): + '''Get current CPU frequency''' + try: + print(f'{psutil.cpu_freq().current:.0f}') + except: + print(0) + +def get_uptime(): + '''Get current system uptime''' + print(str(timedelta(seconds=round(time.time()-psutil.boot_time(),0)))[:-3]) + +def get_load_avg(): + '''Get CPU load average''' + for lavg in os.getloadavg(): + print(round(lavg, 2), ' ', end='') + print() + +def get_mem(): + '''Get current system memory and swap usage''' + mem = psutil.virtual_memory() + swap = psutil.swap_memory() + try: + cmem = mem.cached>>10 + except: + cmem = mem.active>>10 + print(mem.total>>10, mem.free>>10, mem.available>>10, cmem, swap.total>>10, swap.free>>10) + +def get_nics(): + '''Get a list of all network devices sorted by highest throughput''' + io_all = psutil.net_io_counters(pernic=True) + up_stat = psutil.net_if_stats() + + for nic in sorted(psutil.net_if_addrs(), key=lambda nic: (io_all[nic].bytes_recv + io_all[nic].bytes_sent), reverse=True): + if up_stat[nic].isup is False: + continue + print(nic) + +def get_net(net_dev: str): + '''Emulated /proc/net/dev for selected network device''' + net = psutil.net_io_counters(pernic=True)[net_dev] + print(0,net.bytes_recv,0,0,0,0,0,0,0,net.bytes_sent) + +def get_detailed_names_cmd(pid: int): + '''Get name, parent name, username and arguments for selected pid''' + p = psutil.Process(pid) + pa = psutil.Process(p.ppid()) + with p.oneshot(): + print(p.name()) + print(pa.name()) + print(p.username()) + cmd = ' '.join(p.cmdline()) or '[' + p.name() + ']' + print(cleaned(cmd)) + +def get_detailed_mem_time(pid: int): + '''Get memory usage and runtime for selected pid''' + p = psutil.Process(pid) + with p.oneshot(): + print(p.memory_info().rss) + print(timedelta(seconds=round(time.time()-p.create_time(),0))) + +def get_proc(sorting='cpu lazy', tree=False, prog_len=0, arg_len=0, search='', reverse=True, proc_per_cpu=True, max_lines=0): + '''List all processess with pid, name, arguments, threads, username, memory percent and cpu percent''' + line_count: int = 0 + err: float = 0.0 + reverse = not reverse + + if sorting == 'pid': + sort_cmd = "p.info['pid']" + elif sorting == 'program' or tree and sorting == "arguments": + sort_cmd = "p.info['name']" + reverse = not reverse + elif sorting == 'arguments': + sort_cmd = "' '.join(str(p.info['cmdline'])) or p.info['name']" + reverse = not reverse + elif sorting == 'threads': + sort_cmd = "str(p.info['num_threads'])" + elif sorting == 'user': + sort_cmd = "p.info['username']" + reverse = not reverse + elif sorting == 'memory': + sort_cmd = "str(p.info['memory_percent'])" + elif sorting == 'cpu responsive': + sort_cmd = "p.info['cpu_percent']" if proc_per_cpu else "(p.info['cpu_percent'] / cpu_count)" + else: + sort_cmd = "(sum(p.info['cpu_times'][:2] if not p.info['cpu_times'] == 0.0 else [0.0, 0.0]) * 1000 / (time.time() - p.info['create_time']))" + + if tree: + proc_tree(width=prog_len + arg_len, sorting=sort_cmd, reverse=reverse, max_lines=max_lines, proc_per_cpu=proc_per_cpu, search=search) + return + + + print(f"{'Pid:':>7} {'Program:':<{prog_len}}", f"{'Arguments:':<{arg_len-4}}" if arg_len else '', f"{'Threads:' if arg_len else ' Tr:'} {'User:':<9}Mem%{'Cpu%':>11}", sep='') + + for p in sorted(psutil.process_iter(['pid', 'name', 'cmdline', 'num_threads', 'username', 'memory_percent', 'cpu_percent', 'cpu_times', 'create_time'], err), key=lambda p: eval(sort_cmd), reverse=reverse): + if p.info['name'] == 'idle' or p.info['name'] == err or p.info['pid'] == err: + continue + if p.info['cmdline'] == err: + p.info['cmdline'] = "" + if p.info['username'] == err: + p.info['username'] = "?" + if p.info['num_threads'] == err: + p.info['num_threads'] = 0 + if search: + found = False + for value in [ p.info['name'], ' '.join(p.info['cmdline']), str(p.info['pid']), p.info['username'] ]: + if search in value: + found = True + break + if not found: + continue + + cpu = p.info['cpu_percent'] if proc_per_cpu else (p.info['cpu_percent'] / psutil.cpu_count()) + mem = p.info['memory_percent'] + cmd = ' '.join(p.info['cmdline']) or '[' + p.info['name'] + ']' + print(f"{p.info['pid']:>7} ", + f"{cleaned(p.info['name']):<{prog_len}.{prog_len-1}}", + f"{cleaned(cmd):<{arg_len}.{arg_len-1}}" if arg_len else '', + f"{p.info['num_threads']:>4} " if p.info['num_threads'] < 1000 else '999> ', + f"{p.info['username']:<9.9}" if len(p.info['username']) < 10 else f"{p.info['username'][:8]:<8}+", + f"{mem:>4.1f}" if mem < 100 else f"{mem:>4.0f} ", + f"{cpu:>11.1f} " if cpu < 100 else f"{cpu:>11.0f} ", + sep='') + line_count += 1 + if max_lines and line_count == max_lines: + break + +def proc_tree(width: int, sorting: str = 'cpu lazy', reverse: bool = True, max_lines: int = 0, proc_per_cpu=True, search=''): + '''List all processess in a tree view with pid, name, threads, username, memory percent and cpu percent''' + tree_line_count: int = 0 + err: float = 0.0 + + def create_tree(parent: int, tree, indent: str = '', inindent: str = ' ', found: bool = False): + nonlocal infolist, tree_line_count, max_lines, tree_width, proc_per_cpu, search + cont: bool = True + if max_lines and tree_line_count >= max_lines: + return + try: + name: str = psutil.Process(parent).name() + if name == "idle": return + except psutil.Error: + pass + name: str = '' + try: + getinfo: Dict = infolist[parent] + except: + pass + getinfo: bool = False + if search and not found: + for value in [ name, str(parent), getinfo['username'] if getinfo else '' ]: + if search in value: + found = True + break + if not found: + cont = False + if cont: print(f"{f'{inindent}{parent} {cleaned(name)}':<{tree_width}.{tree_width-1}}", sep='', end='') + if getinfo and cont: + if getinfo['cpu_times'] == err: + getinfo['num_threads'] = 0 + if p.info['username'] == err: + p.info['username'] = "?" + cpu = getinfo['cpu_percent'] if proc_per_cpu else (getinfo['cpu_percent'] / psutil.cpu_count()) + print(f"{getinfo['num_threads']:>4} " if getinfo['num_threads'] < 1000 else '999> ', + f"{getinfo['username']:<9.9}" if len(getinfo['username']) < 10 else f"{getinfo['username'][:8]:<8}+", + f"{getinfo['memory_percent']:>4.1f}" if getinfo['memory_percent'] < 100 else f"{getinfo['memory_percent']:>4.0f} ", + f"{cpu:>11.1f} " if cpu < 100 else f"{cpu:>11.0f} ", + sep='') + elif cont: + print(f"{'':>14}{'0.0':>4}{'0.0':>11} ", sep='') + tree_line_count += 1 + if parent not in tree: + return + children = tree[parent][:-1] + for child in children: + create_tree(child, tree, indent + " │ ", indent + " ├─ ", found=found) + if max_lines and tree_line_count >= max_lines: + break + child = tree[parent][-1] + create_tree(child, tree, indent + " ", indent + " └─ ") + + infolist: Dict = {} + tree: List = defaultdict(list) + for p in sorted(psutil.process_iter(['pid', 'name', 'num_threads', 'username', 'memory_percent', 'cpu_percent', 'cpu_times', 'create_time'], err), key=lambda p: eval(sorting), reverse=reverse): + try: + tree[p.ppid()].append(p.pid) + except (psutil.NoSuchProcess, psutil.ZombieProcess): + pass + else: + infolist[p.pid] = p.info + if 0 in tree and 0 in tree[0]: + tree[0].remove(0) + + tree_width: int = width + 8 + + print(f"{' Tree:':<{tree_width-4}}", 'Threads: ', f"{'User:':<9}Mem%{'Cpu%':>11}", sep='') + create_tree(min(tree), tree) + +def get_disks(exclude: str = None, filtering: str = None): + '''Get stats, current read and current write for all disks''' + global disk_hist + disk_read: int = 0 + disk_write: int = 0 + dev_name: str + disk_name: str + disk_list: List[str] = [] + excludes: List[str] = [] + if exclude: excludes = exclude.split(' ') + if system == "BSD": excludes += ["devfs", "tmpfs", "procfs", "linprocfs", "gvfs", "fusefs"] + if filtering: filtering: Tuple[str] = tuple(filtering.split(' ')) + io_counters = psutil.disk_io_counters(perdisk=True if system == "Linux" else False, nowrap=True) + print("Ignored line") + for disk in psutil.disk_partitions(): + disk_io = None + disk_name = disk.mountpoint.rsplit('/', 1)[-1] if not disk.mountpoint == "/" else "root" + while disk_name in disk_list: disk_name += "_" + disk_list += [disk_name] + if excludes and disk.fstype in excludes or filtering and not disk_name.endswith(filtering): + continue + if system == "MacOS" and disk.mountpoint == "/private/var/vm": + continue + try: + disk_u = psutil.disk_usage(disk.mountpoint) + except: + pass + print(f'{disk.device} {disk_u.total >> 10} {disk_u.used >> 10} {disk_u.free >> 10} {disk_u.percent:.0f} ', end='') + try: + if system == "Linux": + dev_name = os.path.realpath(disk.device).rsplit('/', 1)[-1] + if dev_name.startswith("md"): + try: + dev_name = dev_name[:dev_name.index("p")] + except: + pass + disk_io = io_counters[dev_name] + elif disk.mountpoint == "/": + disk_io = io_counters + else: + raise Exception + disk_read = disk_io.read_bytes + disk_write = disk_io.write_bytes + + disk_read -= disk_hist[disk.device][0] + disk_write -= disk_hist[disk.device][1] + except: + pass + disk_read = 0 + disk_write = 0 + + if disk_io: disk_hist[disk.device] = (disk_io.read_bytes, disk_io.write_bytes) + print(f'{disk_read >> 10} {disk_write >> 10} {disk_name}') + +#* The script takes input over coproc pipes and runs command if in the accepted commands list +while command != 'quit': + if not psutil.pid_exists(parent_pid): + quit() + try: + command = input() + except: + pass + quit() + + if not command or command == 'test': + continue + elif command.startswith(allowed_commands): + try: + exec(command) + except Exception as e: + pass + print() + print('/ERROR') + print(f'PSUTIL ERROR! Command: {command}\n{e}', file=sys.stderr) + else: + continue + print('/EOL') + #print(f'{command}', file=sys.stderr) +EOF + fi +fi + +#* Set up traps for ctrl-c, soft kill, window resize, ctrl-z and resume from ctrl-z +trap 'quitting=1; time_left=0' SIGINT SIGQUIT SIGTERM +trap 'resized=1; time_left=0' SIGWINCH +trap 'sleepy=1; time_left=0' SIGTSTP +trap 'resume_' SIGCONT +trap 'failed_pipe=1; time_left=0' PIPE + +#* Set up error logging to file if enabled +if [[ $error_logging == true ]]; then + set -o errtrace + trap 'traperr' ERR + + #* Remove everything but the last 500 lines of error log if larger than 500 lines + if [[ -e "${config_dir}/error.log" && $(${wc} -l <"${config_dir}/error.log") -gt 500 ]]; then + ${tail} -n 500 "${config_dir}/error.log" > "${config_dir}/tmp" + ${rm} -f "${config_dir}/error.log" + ${mv} -f "${config_dir}/tmp" "${config_dir}/error.log" + fi + ( echo " " ; echo "New instance of furtop version: ${version} Pid: $$" ) >> "${config_dir}/error.log" + exec 2>>"${config_dir}/error.log" + if [[ $1 == "--debug" ]]; then + exec 19>"${config_dir}/tracing.log" + BASH_XTRACEFD=19 + set -x + fi +else + exec 2>/dev/null +fi + +#* If we have been sourced by another shell, quit. Allows sourcing only function definition. +[[ "${#BASH_SOURCE[@]}" -gt 1 ]] && { return 0; } + +#* Call init function +init_ + +if [[ $use_psutil == true ]]; then + coproc pycoproc (python3 ${pywrapper}) + sleep 0.1 + init_ cont +fi + +#* Start infinite loop +until false; do + if [[ $use_psutil == true ]] && [[ -n $failed_pipe ]]; then + if ((++failed_pipes>10)); then + if [[ $system == "Linux" ]]; then + use_psutil="false" + else + quit_ 1 + fi + fi + coproc pycoproc (python3 ${pywrapper}) + sleep 0.1 + unset failed_pipe + fi + if [[ -n $py_error ]]; then + if ((++py_errors>10)); then + if [[ $system == "Linux" ]]; then + use_psutil="false" + else + quit_ 1 + fi + fi + unset py_error + fi + main_loop +done + +#* Quit cleanly even if false starts being true... +quit_ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..24156d6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +psutil==5.7.0 diff --git a/src/furtop.psutil.py b/src/furtop.psutil.py new file mode 100755 index 0000000..ec3cd5b --- /dev/null +++ b/src/furtop.psutil.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 + +'''This is a copy of the python script that furtop starts in a coprocess when using psutil for data collection''' + +import os, sys, subprocess, re, time, psutil +from datetime import timedelta +from collections import defaultdict +from typing import List, Set, Dict, Tuple, Optional, Union + +system: str +if "linux" in sys.platform: system = "Linux" +elif "bsd" in sys.platform: system = "BSD" +elif "darwin" in sys.platform: system = "MacOS" +else: system = "Other" + +parent_pid: int = psutil.Process(os.getpid()).ppid() + +allowed_commands: Tuple[str] = ( + 'get_proc', + 'get_disks', + 'get_cpu_name', + 'get_cpu_cores', + 'get_nics', + 'get_cpu_cores', + 'get_cpu_usage', + 'get_cpu_freq', + 'get_uptime', + 'get_load_avg', + 'get_mem', + 'get_detailed_names_cmd', + 'get_detailed_mem_time', + 'get_net', + 'get_cmd_out', + 'get_sensors', + 'get_sensors_check', + 'get_ms' + ) +command: str = '' +cpu_count: int = psutil.cpu_count() +disk_hist: Dict = {} + +def cleaned(string: str) -> str: + '''Escape characters not suitable for "echo -e" in bash''' + return string.replace("\\", "\\\\").replace("$", "\\$").replace("\n", "\\n").replace("\t", "\\t").replace("\"", "\\\"").replace("\'", "\\\'") + +def get_cmd_out(cmd: str): + '''Save bash the trouble of creating child processes by running through python instead''' + print(subprocess.check_output(cmd, shell=True, universal_newlines=True).rstrip()) + +def get_ms(): + '''Get current epoch millisecond''' + t = str(time.time()).split(".") + print(f'{t[0]}{t[1][:3]}') + +def get_sensors(): + '''A clone of "sensors" but using psutil''' + temps = psutil.sensors_temperatures() + if not temps: + return + try: + for name, entries in temps.items(): + print(name) + for entry in entries: + print(f'{entry.label or name}: {entry.current}°C (high = {entry.high}°C, crit = {entry.critical}°C)') + print() + except: + pass + +def get_sensors_check(): + '''Check if get_sensors() output contains accepted CPU temperature values''' + if not hasattr(psutil, "sensors_temperatures"): print("false"); return + try: + temps = psutil.sensors_temperatures() + except: + pass + print("false"); return + if not temps: print("false"); return + try: + for _, entries in temps.items(): + for entry in entries: + if entry.label.startswith(('Package', 'Core 0', 'Tdie')): + print("true") + return + except: + pass + print("false") + +def get_cpu_name(): + '''Fetch a suitable CPU identifier from the CPU model name string''' + name: str = "" + command: str = "" + all_info: str = "" + rem_line: str = "" + if system == "Linux": + command = "cat /proc/cpuinfo" + rem_line = "model name" + elif system == "MacOS": + command ="sysctl -n machdep.cpu.brand_string" + elif system == "BSD": + command ="sysctl hw.model" + rem_line = "hw.model" + + all_info = subprocess.check_output("LANG=C " + command, shell=True, universal_newlines=True) + if rem_line: + for line in all_info.split("\n"): + if rem_line in line: + name = re.sub( ".*" + rem_line + ".*:", "", line,1).lstrip() + else: + name = all_info + if "Xeon" in name: + name = name.split(" ") + name = name[name.index("CPU")+1] + elif "Ryzen" in name: + name = name.split(" ") + name = " ".join(name[name.index("Ryzen"):name.index("Ryzen")+3]) + elif "CPU" in name: + name = name.split(" ") + name = name[name.index("CPU")-1] + + print(name) + +def get_cpu_cores(): + '''Get number of CPU cores and threads''' + cores: int = psutil.cpu_count(logical=False) + threads: int = psutil.cpu_count(logical=True) + print(f'{cores} {threads if threads else cores}') + +def get_cpu_usage(): + cpu: float = psutil.cpu_percent(percpu=False) + threads: List[float] = psutil.cpu_percent(percpu=True) + print(f'{cpu:.0f}') + for thread in threads: + print(f'{thread:.0f}') + +def get_cpu_freq(): + '''Get current CPU frequency''' + try: + print(f'{psutil.cpu_freq().current:.0f}') + except: + print(0) + +def get_uptime(): + '''Get current system uptime''' + print(str(timedelta(seconds=round(time.time()-psutil.boot_time(),0)))[:-3]) + +def get_load_avg(): + '''Get CPU load average''' + for lavg in os.getloadavg(): + print(round(lavg, 2), ' ', end='') + print() + +def get_mem(): + '''Get current system memory and swap usage''' + mem = psutil.virtual_memory() + swap = psutil.swap_memory() + try: + cmem = mem.cached>>10 + except: + cmem = mem.active>>10 + print(mem.total>>10, mem.free>>10, mem.available>>10, cmem, swap.total>>10, swap.free>>10) + +def get_nics(): + '''Get a list of all network devices sorted by highest throughput''' + io_all = psutil.net_io_counters(pernic=True) + up_stat = psutil.net_if_stats() + + for nic in sorted(psutil.net_if_addrs(), key=lambda nic: (io_all[nic].bytes_recv + io_all[nic].bytes_sent), reverse=True): + if up_stat[nic].isup is False: + continue + print(nic) + +def get_net(net_dev: str): + '''Emulated /proc/net/dev for selected network device''' + net = psutil.net_io_counters(pernic=True)[net_dev] + print(0,net.bytes_recv,0,0,0,0,0,0,0,net.bytes_sent) + +def get_detailed_names_cmd(pid: int): + '''Get name, parent name, username and arguments for selected pid''' + p = psutil.Process(pid) + pa = psutil.Process(p.ppid()) + with p.oneshot(): + print(p.name()) + print(pa.name()) + print(p.username()) + cmd = ' '.join(p.cmdline()) or '[' + p.name() + ']' + print(cleaned(cmd)) + +def get_detailed_mem_time(pid: int): + '''Get memory usage and runtime for selected pid''' + p = psutil.Process(pid) + with p.oneshot(): + print(p.memory_info().rss) + print(timedelta(seconds=round(time.time()-p.create_time(),0))) + +def get_proc(sorting='cpu lazy', tree=False, prog_len=0, arg_len=0, search='', reverse=True, proc_per_cpu=True, max_lines=0): + '''List all processess with pid, name, arguments, threads, username, memory percent and cpu percent''' + line_count: int = 0 + err: float = 0.0 + reverse = not reverse + + if sorting == 'pid': + sort_cmd = "p.info['pid']" + elif sorting == 'program' or tree and sorting == "arguments": + sort_cmd = "p.info['name']" + reverse = not reverse + elif sorting == 'arguments': + sort_cmd = "' '.join(str(p.info['cmdline'])) or p.info['name']" + reverse = not reverse + elif sorting == 'threads': + sort_cmd = "str(p.info['num_threads'])" + elif sorting == 'user': + sort_cmd = "p.info['username']" + reverse = not reverse + elif sorting == 'memory': + sort_cmd = "str(p.info['memory_percent'])" + elif sorting == 'cpu responsive': + sort_cmd = "p.info['cpu_percent']" if proc_per_cpu else "(p.info['cpu_percent'] / cpu_count)" + else: + sort_cmd = "(sum(p.info['cpu_times'][:2] if not p.info['cpu_times'] == 0.0 else [0.0, 0.0]) * 1000 / (time.time() - p.info['create_time']))" + + if tree: + proc_tree(width=prog_len + arg_len, sorting=sort_cmd, reverse=reverse, max_lines=max_lines, proc_per_cpu=proc_per_cpu, search=search) + return + + + print(f"{'Pid:':>7} {'Program:':<{prog_len}}", f"{'Arguments:':<{arg_len-4}}" if arg_len else '', f"{'Threads:' if arg_len else ' Tr:'} {'User:':<9}Mem%{'Cpu%':>11}", sep='') + + for p in sorted(psutil.process_iter(['pid', 'name', 'cmdline', 'num_threads', 'username', 'memory_percent', 'cpu_percent', 'cpu_times', 'create_time'], err), key=lambda p: eval(sort_cmd), reverse=reverse): + if p.info['name'] == 'idle' or p.info['name'] == err or p.info['pid'] == err: + continue + if p.info['cmdline'] == err: + p.info['cmdline'] = "" + if p.info['username'] == err: + p.info['username'] = "?" + if p.info['num_threads'] == err: + p.info['num_threads'] = 0 + if search: + found = False + for value in [ p.info['name'], ' '.join(p.info['cmdline']), str(p.info['pid']), p.info['username'] ]: + if search in value: + found = True + break + if not found: + continue + + cpu = p.info['cpu_percent'] if proc_per_cpu else (p.info['cpu_percent'] / psutil.cpu_count()) + mem = p.info['memory_percent'] + cmd = ' '.join(p.info['cmdline']) or '[' + p.info['name'] + ']' + print(f"{p.info['pid']:>7} ", + f"{cleaned(p.info['name']):<{prog_len}.{prog_len-1}}", + f"{cleaned(cmd):<{arg_len}.{arg_len-1}}" if arg_len else '', + f"{p.info['num_threads']:>4} " if p.info['num_threads'] < 1000 else '999> ', + f"{p.info['username']:<9.9}" if len(p.info['username']) < 10 else f"{p.info['username'][:8]:<8}+", + f"{mem:>4.1f}" if mem < 100 else f"{mem:>4.0f} ", + f"{cpu:>11.1f} " if cpu < 100 else f"{cpu:>11.0f} ", + sep='') + line_count += 1 + if max_lines and line_count == max_lines: + break + +def proc_tree(width: int, sorting: str = 'cpu lazy', reverse: bool = True, max_lines: int = 0, proc_per_cpu=True, search=''): + '''List all processess in a tree view with pid, name, threads, username, memory percent and cpu percent''' + tree_line_count: int = 0 + err: float = 0.0 + + def create_tree(parent: int, tree, indent: str = '', inindent: str = ' ', found: bool = False): + nonlocal infolist, tree_line_count, max_lines, tree_width, proc_per_cpu, search + cont: bool = True + if max_lines and tree_line_count >= max_lines: + return + try: + name: str = psutil.Process(parent).name() + if name == "idle": return + except psutil.Error: + pass + name: str = '' + try: + getinfo: Dict = infolist[parent] + except: + pass + getinfo: bool = False + if search and not found: + for value in [ name, str(parent), getinfo['username'] if getinfo else '' ]: + if search in value: + found = True + break + if not found: + cont = False + if cont: print(f"{f'{inindent}{parent} {cleaned(name)}':<{tree_width}.{tree_width-1}}", sep='', end='') + if getinfo and cont: + if getinfo['cpu_times'] == err: + getinfo['num_threads'] = 0 + if p.info['username'] == err: + p.info['username'] = "?" + cpu = getinfo['cpu_percent'] if proc_per_cpu else (getinfo['cpu_percent'] / psutil.cpu_count()) + print(f"{getinfo['num_threads']:>4} " if getinfo['num_threads'] < 1000 else '999> ', + f"{getinfo['username']:<9.9}" if len(getinfo['username']) < 10 else f"{getinfo['username'][:8]:<8}+", + f"{getinfo['memory_percent']:>4.1f}" if getinfo['memory_percent'] < 100 else f"{getinfo['memory_percent']:>4.0f} ", + f"{cpu:>11.1f} " if cpu < 100 else f"{cpu:>11.0f} ", + sep='') + elif cont: + print(f"{'':>14}{'0.0':>4}{'0.0':>11} ", sep='') + tree_line_count += 1 + if parent not in tree: + return + children = tree[parent][:-1] + for child in children: + create_tree(child, tree, indent + " │ ", indent + " ├─ ", found=found) + if max_lines and tree_line_count >= max_lines: + break + child = tree[parent][-1] + create_tree(child, tree, indent + " ", indent + " └─ ") + + infolist: Dict = {} + tree: List = defaultdict(list) + for p in sorted(psutil.process_iter(['pid', 'name', 'num_threads', 'username', 'memory_percent', 'cpu_percent', 'cpu_times', 'create_time'], err), key=lambda p: eval(sorting), reverse=reverse): + try: + tree[p.ppid()].append(p.pid) + except (psutil.NoSuchProcess, psutil.ZombieProcess): + pass + else: + infolist[p.pid] = p.info + if 0 in tree and 0 in tree[0]: + tree[0].remove(0) + + tree_width: int = width + 8 + + print(f"{' Tree:':<{tree_width-4}}", 'Threads: ', f"{'User:':<9}Mem%{'Cpu%':>11}", sep='') + create_tree(min(tree), tree) + +def get_disks(exclude: str = None, filtering: str = None): + '''Get stats, current read and current write for all disks''' + global disk_hist + disk_read: int = 0 + disk_write: int = 0 + dev_name: str + disk_name: str + disk_list: List[str] = [] + excludes: List[str] = [] + if exclude: excludes = exclude.split(' ') + if system == "BSD": excludes += ["devfs", "tmpfs", "procfs", "linprocfs", "gvfs", "fusefs"] + if filtering: filtering: Tuple[str] = tuple(filtering.split(' ')) + io_counters = psutil.disk_io_counters(perdisk=True if system == "Linux" else False, nowrap=True) + print("Ignored line") + for disk in psutil.disk_partitions(): + disk_io = None + disk_name = disk.mountpoint.rsplit('/', 1)[-1] if not disk.mountpoint == "/" else "root" + while disk_name in disk_list: disk_name += "_" + disk_list += [disk_name] + if excludes and disk.fstype in excludes or filtering and not disk_name.endswith(filtering): + continue + if system == "MacOS" and disk.mountpoint == "/private/var/vm": + continue + try: + disk_u = psutil.disk_usage(disk.mountpoint) + except: + pass + print(f'{disk.device} {disk_u.total >> 10} {disk_u.used >> 10} {disk_u.free >> 10} {disk_u.percent:.0f} ', end='') + try: + if system == "Linux": + dev_name = os.path.realpath(disk.device).rsplit('/', 1)[-1] + if dev_name.startswith("md"): + try: + dev_name = dev_name[:dev_name.index("p")] + except: + pass + disk_io = io_counters[dev_name] + elif disk.mountpoint == "/": + disk_io = io_counters + else: + raise Exception + disk_read = disk_io.read_bytes + disk_write = disk_io.write_bytes + + disk_read -= disk_hist[disk.device][0] + disk_write -= disk_hist[disk.device][1] + except: + pass + disk_read = 0 + disk_write = 0 + + if disk_io: disk_hist[disk.device] = (disk_io.read_bytes, disk_io.write_bytes) + print(f'{disk_read >> 10} {disk_write >> 10} {disk_name}') + +#* The script takes input over coproc pipes and runs command if in the accepted commands list +while command != 'quit': + if not psutil.pid_exists(parent_pid): + quit() + try: + command = input() + except: + pass + quit() + + if not command or command == 'test': + continue + elif command.startswith(allowed_commands): + try: + exec(command) + except Exception as e: + pass + print() + print('/ERROR') + print(f'PSUTIL ERROR! Command: {command}\n{e}', file=sys.stderr) + else: + continue + print('/EOL') + #print(f'{command}', file=sys.stderr) \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..958110a --- /dev/null +++ b/test.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +./test/libs/bats/bin/bats test/*.bats diff --git a/test/basic_test.bats b/test/basic_test.bats new file mode 100644 index 0000000..e159b6f --- /dev/null +++ b/test/basic_test.bats @@ -0,0 +1,17 @@ +#!/usr/bin/env bats + +load 'libs/bats-support/load' +load 'libs/bats-assert/load' + +load test_helper + +@test "Sourcing works, by checking if \$system is set" { + run echo $system + refute_output "" +} + +@test "#get_themes populates themes" { + get_themes + assert_success + assert [ ${#themes[@]} -gt 0 ] +} diff --git a/test/test_helper.bash b/test/test_helper.bash new file mode 100644 index 0000000..63d5af1 --- /dev/null +++ b/test/test_helper.bash @@ -0,0 +1 @@ +source furtop diff --git a/themes/Default.theme b/themes/Default.theme new file mode 100644 index 0000000..e224d50 --- /dev/null +++ b/themes/Default.theme @@ -0,0 +1,89 @@ +#Bashtop theme with nord palette (https://www.nordtheme.com) +#by Justin Zobel + +# Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" +# example for white: "#ffffff", "#ff" or "255 255 255". + +# All graphs and meters can be gradients +# For single color graphs leave "mid" and "end" variable empty. +# Use "start" and "end" variables for two color gradient +# Use "start", "mid" and "end" for three color gradient + +# Main background, empty for terminal default, need to be empty if you want transparent background +theme[main_bg]="" + +# Main text color +theme[main_fg]="#ffccbb" + +# Title color for boxes +theme[title]="#aabbbb" + +# Higlight color for keyboard shortcuts +theme[hi_fg]="#aa77bb" + +# Background color of selected item in processes box +theme[selected_bg]="#775577" + +# Foreground color of selected item in processes box +theme[selected_fgF]="#ffeeee" + +# Color of inactive/disabled text +theme[inactive_fg]="#555577" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#aa77bb" + +# Cpu box outline color +theme[cpu_box]="#555566" + +# Memory/disks box outline color +theme[mem_box]="#336688" + +# Net up/down box outline color +theme[net_box]="#665588" + +# Processes box outline color +theme[proc_box]="#664488" + +# Box divider line and small boxes line color +theme[div_line]="#665599" + +# Temperature graph colors +theme[temp_start]="#aaaadd" +theme[temp_mid]="#88bbcc" +theme[temp_end]="#ffeeff" + +# CPU graph colors +theme[cpu_start]="#aaaadd" +theme[cpu_mid]="#88bbcc" +theme[cpu_end]="#ffeeff" + +# Mem/Disk free meter +theme[free_start]="#aaaadd" +theme[free_mid]="#88bbcc" +theme[free_end]="#ffeeff" + +# Mem/Disk cached meter +theme[cached_start]="#aaaadd" +theme[cached_mid]="#88bbcc" +theme[cached_end]="#ffeeff" + +# Mem/Disk available meter +theme[available_start]="#aaaadd" +theme[available_mid]="#88bbcc" +theme[available_end]="#ffeeff" + +# Mem/Disk used meter +theme[used_start]="#aaaadd" +theme[used_mid]="#88bbcc" +theme[used_end]="#ffeeff" + +# Download graph colors +theme[download_start]="#aaaadd" +theme[download_mid]="#88bbcc" +theme[download_end]="#ffeeff" + +# Upload graph colors +theme[upload_start]="#aaaadd" +theme[upload_mid]="#88bbcc" +theme[upload_end]="#ffeeff"