diff --git a/.editorconfig b/.editorconfig index 61b7846..63202f0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,7 @@ [*.{cpp,h,sh,md,cfg,sample}] indent_style = tab indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fd2bf38..3c937d1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,10 @@ assignees: aristocratos --- +**Read the README.md and search for similar issues before posting a bug report!** + +Any bug that can be solved by just reading the [prerequisites](https://github.com/aristocratos/btop#prerequisites) section of the README will likely be ignored. + **Describe the bug** [A clear and concise description of what the bug is.] @@ -24,7 +28,7 @@ assignees: aristocratos [If applicable, add screenshots to help explain your problem.] **Info (please complete the following information):** - - btop++ version: `bpytop -v` + - btop++ version: `btop -v` - Binary: [self compiled or static binary from release] - (If compiled) Compiler and version: - Architecture: [x86_64, aarch64, etc.] `uname -m` @@ -38,16 +42,18 @@ assignees: aristocratos contents of `~/.config/btop/btop.log` -(try running btop with `--debug` flag if error.log is empty) +(try running btop with `--debug` flag if btop.log is empty) **GDB Backtrace** If btop++ is crashing at start the following steps could be helpful: -1. run `gdb btop` +(Extra helpful if compiled with `make OPTFLAGS="-O0 -g"`) -2. `r` to run, wait for crash and press enter +1. run (linux): `gdb btop` (macos): `lldb btop` -3. `bt` to get backtrace +2. `r` to run, wait for crash and press enter if prompted, CTRL+L to clear screen if needed. + +3. (gdb): `thread apply all bt` (lldb): `bt all` to get backtrace for all threads 4. Copy and paste the backtrace here: diff --git a/.github/workflows/continuous-build-linux.yml b/.github/workflows/continuous-build-linux.yml new file mode 100644 index 0000000..af32d65 --- /dev/null +++ b/.github/workflows/continuous-build-linux.yml @@ -0,0 +1,113 @@ +name: Continuous Build Linux + +on: + workflow_dispatch: + push: + branches: + - main + tags-ignore: + - '*.*' + paths: + - 'src/**' + - '!src/osx/**' + - '!src/freebsd/**' + - 'include/**' + - 'Makefile' + - '.github/workflows/continuous-build.yml' + +jobs: + static-build: + continue-on-error: true + strategy: + matrix: + toolchain: + - aarch64-linux-musl + - aarch64_be-linux-musl + - arm-linux-musleabi + - arm-linux-musleabihf + - armeb-linux-musleabi + - armeb-linux-musleabihf + - armel-linux-musleabi + - armel-linux-musleabihf + - armv5l-linux-musleabi + - armv5l-linux-musleabihf + - armv6-linux-musleabi + - armv6-linux-musleabihf + - armv7l-linux-musleabihf + - armv7m-linux-musleabi + - armv7r-linux-musleabihf + - i486-linux-musl + - i686-linux-musl + - m68k-linux-musl + - mips-linux-musl + - mips-linux-musln32sf + - mips-linux-muslsf + - mips64-linux-musl + - mips64-linux-musln32 + - mips64-linux-musln32sf + - mips64el-linux-musl + - mips64el-linux-musln32 + - mips64el-linux-musln32sf + - mipsel-linux-musl + - mipsel-linux-musln32 + - mipsel-linux-musln32sf + - mipsel-linux-muslsf + - powerpc-linux-musl + - powerpc-linux-muslsf + - powerpc64-linux-musl + - powerpc64le-linux-musl + - powerpcle-linux-musl + - powerpcle-linux-muslsf + - riscv32-linux-musl + - riscv64-linux-musl + - s390x-linux-musl + - x86_64-linux-musl + - x86_64-linux-muslx32 + + # - or1k-linux-musl + # - sh2-linux-musl + # - sh2-linux-muslfdpic + # - sh2eb-linux-musl + # - sh2eb-linux-muslfdpic + # - sh4-linux-musl + # - sh4eb-linux-musl + + runs-on: ubuntu-latest + container: muslcc/x86_64:${{ matrix.toolchain }} + + steps: + - name: Install build tools + run: apk add --no-cache coreutils git make tar zstd + + - name: Fix - Unsafe repository stop + run: git config --global --add safe.directory /__w/btop/btop + + - name: Checkout source + uses: actions/checkout@v2 + + - name: Fix - Stopping at filesystem boundary + run: git init # [fix Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).] + + - name: Build + run: make STATIC=true STRIP=true + + - name: Make executable + run: chmod +x bin/* + + - name: Set up directories + run: | + mkdir .artifacts + mkdir .package + + - name: Create binary atrifacts + run: | + TOOLCHAIN=${{ matrix.toolchain }} + GIT_HASH=$(git rev-parse --short "${{ github.sha }}") + FILENAME=btop-${TOOLCHAIN/linux-musl/}-$GIT_HASH + cp bin/btop .artifacts/$FILENAME + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: btop-${{ matrix.toolchain }} + path: '.artifacts/**' diff --git a/.github/workflows/continuous-build-macos.yml b/.github/workflows/continuous-build-macos.yml new file mode 100644 index 0000000..2cb5e4a --- /dev/null +++ b/.github/workflows/continuous-build-macos.yml @@ -0,0 +1,34 @@ +name: Continuous Build MacOS + +on: + push: + branches: + - main + tags-ignore: + - '*.*' + paths: + - 'src/**' + - '!src/linux/**' + - '!src/freebsd/**' + - 'include/**' + - 'Makefile' + - '.github/workflows/*' + +jobs: + build-osx: + + runs-on: macos-11 + + steps: + - uses: actions/checkout@v2 + - name: Compile + run: | + make CXX=g++-11 ARCH=x86_64 STATIC=true STRIP=true + GIT_HASH=$(git rev-parse --short "$GITHUB_SHA") + mv bin/btop bin/btop-x86_64-BigSur-$GIT_HASH + ls -alh bin + + - uses: actions/upload-artifact@v2 + with: + name: btop-x86_64-macos-BigSur + path: 'bin/*' diff --git a/.gitignore b/.gitignore index 3817f1b..fc1994b 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,8 @@ stage/ build bin btop -.*/ \ No newline at end of file +.*/ + + +#do not ignore .github directory +!.github \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7354364..dacbb53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,291 @@ +## v1.2.8 + +* Added: Support for ZFS pool io stats monitoring, by @simplepad + +* Added: Filtering of kernel processes, by @0xJoeMama + +* Added: New theme everforest-dark-hard, by @iambeingtracked + +* Added: New theme tomorrow-night, by @appuchias + +* Changed: Disable battery monitoring if it fails instead of exiting + +## v1.2.7 + +* Fixed: Disk IO stats for individual partitions instead of whole disk (Linux) + +* Added: Case insensitive process filtering, by @abrasumente233 + +* Added: Include ZFS ARC in cached/available memory on Linux, by @mattico + +* Added: Desktop entry and icons, by @yonatan8070 + +* Fixed: Net sync scale bug + +* Added: tokyo-night & tokyo-storm themes, by @Schievel1 + +## v1.2.6 + +* Fixed: Wrong memory unit when shorten and size is less than 10, by @mohi001 + +* Fixed: Use cpu cores avarage temp if missing cpu package temp for FreeBSD + +* Changed: Enter symbol to a more common variant + +## v1.2.5 + +* Fixed: Fallback to less accurate UTF8 char count if conversion to wstring fails + +* Fixed: Small ui fixes for mem and disks + +* Added: New theme HotPurpleTrafficLight, by @pallebone + +* Fixed: title_left symbol between auto and zero in the net box is not displayed, by @mrdotx + +* Fixed: Mouse mappings for net box + +## v1.2.4 + +* Optimization: Proc::draw() + +* Fixed: Ignore duplicate disks with same mountpoint + +* Changed: Restrict command line for processes to 1000 characters to fix utf8 conversion errors + +* Added: add "g" and "G" to vim keys, by @mohi001 + +## v1.2.3 + +* Changed: floating_humanizer() now show fractions when shortened and value is < 10 + +* Fixed: Process tree not redrawing properly + +* Fixed: string to wstring conversion crash when string is too big + +## v1.2.2 + +* Changed: Reverted uncolor() back to using regex to fix delay in opening menu when compiled with musl + +* Added: Toggle for showing free disk space for privileged or normal users + +* Added: Clarification on signal screen that number can be manually entered + +## v1.2.1 + +* Added: Arrow only after use of "f" when filtering processes, by @NavigationHazard + +* Fixed: Fx::uncolor not removing all escapes + +* Fixed: Text alignment for popup boxes + +* Fixed: Terminal resize warning getting stuck + +* Removed: Unnecessary counter for atomic_lock + +* Added: Percentage progress to Makefile + +* Fixed: Alignment of columns in proc box when wide UTF8 characters are used + +* Fixed: Battery meter draw fix + +## v1.2.0 + +* Added: Support for FreeBSD, by @joske and @aristocratos + +* Fixed (again): Account for system rolling over net speeds in Net::collect() + +* Added: Theme gruvbox_material_dark, by @marcoradocchia + +* Added: Option for base 10 bytes/bits + +## v1.1.5 + +* Fixed: Account for system rolling over net speeds in Net::collect() + +## v1.1.4 + +* Fixed: Create dependency files in build directory when compiling, by @stwnt + +* Fixed: fix CPU temp fallback on macOS, by @joske + +* Changed: From rng::sort() to rng::stable_sort() for more stability + +* Fixed: in_avail() can always be zero, by @pg83 + +## v1.1.3 + +* Added: New theme ayu, by @AlphaNecron + +* Added: New theme gruvbox_dark_v2, by @pietryszak + +* Fixed: Macos cpu coretemp for Intel, by @joske + +* Added: New theme OneDark, by @vtmx + +* Fixed: Fixed network graph scale int rollover + +* Fixed: Suspected possibility of very rare stall in Input::clear() + +## v1.1.2 + +* Fixed: SISEGV on macos Mojave, by @mgradowski + +* Fixed: Small optimizations and fixes to Mem::collect() and Input::get() + +* Fixed: Wrong unit for net_upload and net_download in config menu + +* Fixed: UTF-8 detection on macos + +* Fixed: coretemp iteration due to missing tempX_input, by @KFilipek + +* Fixed: coretemp ordering + +## v1.1.1 + +* Added: Partial static build (libgcc, libstdc++) for macos + +* Changed: Continuous build macos switched to OSX 11.6 (Big Sur) and partial static build + +* Changed: Release binaries for macos switched to OSX 12 (Monterey) and partial static build + +## v1.1.0 + +* Added: Support for OSX, by @joske and @aristocratos + +## v1.0.24 + +* Changed: Collection ordering + +* Fixed: Restore all escape seq mouse modes on exit + +* Fixed: SIGINT not cleaning up on exit + +## v1.0.23 + +* Fixed: Config parser missing first value when not including version header + +* Fixed: Vim keys menu lists selection + +* Fixed: Stall when clearing input queue on exit and queue is >1 + +* Fixed: Inconsistent behaviour of "q" key in the menus + +## v1.0.22 + +* Fixed: Bad values for disks and network on 32-bit + +## v1.0.21 + +* Fixed: Removed extra spaces in cpu name + +* Added: / as alternative bind for filter + +* Fixed: Security issue when running with SUID bit set + +## v1.0.20 + +* Added: Improved cpu sensor detection for Ryzen Mobile, by @adnanpri + +* Changed: Updated makefile + +* Changed: Regex for Fx::uncolor() changed to string search and replace + +* Changed: Removed all use of regex with dedicated string functions + +## v1.0.19 + +* Fixed: Makefile now tests compiler flag compatibility + +## v1.0.18 + +* Fixed: Makefile g++ -dumpmachine failure to get platform on some distros + +## v1.0.17 + +* Changed: Reverted mutexes back to custom atomic bool based locks + +* Added: Static binaries switched to building with musl + more platforms, by @jan-guenter + +* Fixed: Improved battery detection, by @jan-guenter + +* Added: Displayed battery selectable in options menu + +* Fixed: Battery error if non existent battery named is entered + +## v1.0.16 + +* Fixed: atomic_wait() and atomic_lock{} use cpu pause instructions instead of thread sleep + +* Fixed: Swapped from atomic bool spinlocks to mutexes to fix rare deadlock + +* Added: Continuous Build workflow for OSX branch, by @ShrirajHegde + +* Changed: Reverted thread mutex lock to atomic bool with wait and timeout + +* Changed: Removed unnecessary async threads in Runner thread + +* Added: Try to restart secondary thread in case of stall and additional error checks for ifstream in Proc::collect() + +* Fixed: change [k]ill to [K]ill when enabling vim keys, by @jlopezcur + +## v1.0.15 + +* Fixed: Extra "root" partition when running in snap + +* Changed: Limit atomic_wait() to 1000ms to fix rare stall + +* Fixed: Removed unneeded lock in Runner::run() + +* Added: Toggle in options for enabling directional vim keys "h,j,k,l" + +## v1.0.14 + +* Changed: Total system memory is checked at every update instead of once at start + +* Added: Continuous Build workflow, by @ShrirajHegde + +* Fixed: Uid -> User fallback to getpwuid() if failure for non static builds + +* Fixed: snap root disk and changed to compiler flags instead of env variables for detection + +* Added: Development branch for OSX, by @joske + +## v1.0.13 + +* Changed: Graph empty symbol is now regular whitespace + +## v1.0.12 + +* Fixed: Exception handling for faulty net download/upload speed + +* Fixed: Cpu percent formatting if over 10'000 + +## v1.0.11 + +* Changed: atomic_wait to use while loop instead of wait() because of rare stall when a signal handler is triggered while waiting + +* Fixed: Get real / mountpoint when running inside snap + +* Fixed: UTF8 set LANG and LC_ALL to empty before UTF8 search and fixed empty error msg on exit before signal handler init + +* Changed: Init will continue with a warning if UTF-8 locale are detected and it fails to set the locale + +## v1.0.10 + +* Added: Wait for terminal size properties to be available at start + +* Changed: Stop second thread before updating terminal size variables + +* Changed: Moved check for valid terminal dimensions to before platform init + +* Added: Check for empty percentage deques + +* Changed: Cpu temp values check for existing values + +* Fixed: Cpu percent cutting off above 1000 percent and added scaling with "k" prefix above 10'000 + +* Fixed: Crash when rapidly resizing terminal at start + ## v1.0.9 * Added: ifstream check and try-catch for stod() in Tools::system_uptime() diff --git a/Img/icon.png b/Img/icon.png new file mode 100644 index 0000000..cba4890 Binary files /dev/null and b/Img/icon.png differ diff --git a/Img/icon.svg b/Img/icon.svg new file mode 100644 index 0000000..402b842 --- /dev/null +++ b/Img/icon.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Img/logo.png b/Img/logo.png index 9704fb5..7e9cb7b 100644 Binary files a/Img/logo.png and b/Img/logo.png differ diff --git a/Img/logo.svg b/Img/logo.svg new file mode 100644 index 0000000..951858f --- /dev/null +++ b/Img/logo.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Makefile b/Makefile index e6b4d44..d1bc95d 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,18 @@ -#* Btop++ makefile v1.2 +#* Btop++ makefile v1.5 -BANNER = \n \033[38;5;196m██████\033[38;5;240m╗ \033[38;5;196m████████\033[38;5;240m╗ \033[38;5;196m██████\033[38;5;240m╗ \033[38;5;196m██████\033[38;5;240m╗\n \033[38;5;160m██\033[38;5;239m╔══\033[38;5;160m██\033[38;5;239m╗╚══\033[38;5;160m██\033[38;5;239m╔══╝\033[38;5;160m██\033[38;5;239m╔═══\033[38;5;160m██\033[38;5;239m╗\033[38;5;160m██\033[38;5;239m╔══\033[38;5;160m██\033[38;5;239m╗ \033[38;5;160m██\033[38;5;239m╗ \033[38;5;160m██\033[38;5;239m╗\n \033[38;5;124m██████\033[38;5;238m╔╝ \033[38;5;124m██\033[38;5;238m║ \033[38;5;124m██\033[38;5;238m║ \033[38;5;124m██\033[38;5;238m║\033[38;5;124m██████\033[38;5;238m╔╝ \033[38;5;124m██████\033[38;5;238m╗\033[38;5;124m██████\033[38;5;238m╗\n \033[38;5;88m██\033[38;5;237m╔══\033[38;5;88m██\033[38;5;237m╗ \033[38;5;88m██\033[38;5;237m║ \033[38;5;88m██\033[38;5;237m║ \033[38;5;88m██\033[38;5;237m║\033[38;5;88m██\033[38;5;237m╔═══╝ ╚═\033[38;5;88m██\033[38;5;237m╔═╝╚═\033[38;5;88m██\033[38;5;237m╔═╝\n \033[38;5;52m██████\033[38;5;236m╔╝ \033[38;5;52m██\033[38;5;236m║ ╚\033[38;5;52m██████\033[38;5;236m╔╝\033[38;5;52m██\033[38;5;236m║ ╚═╝ ╚═╝\n \033[38;5;235m╚═════╝ ╚═╝ ╚═════╝ ╚═╝ \033[1;3;38;5;240mMakefile v1.2\033[0m +BANNER = \n \033[38;5;196m██████\033[38;5;240m╗ \033[38;5;196m████████\033[38;5;240m╗ \033[38;5;196m██████\033[38;5;240m╗ \033[38;5;196m██████\033[38;5;240m╗\n \033[38;5;160m██\033[38;5;239m╔══\033[38;5;160m██\033[38;5;239m╗╚══\033[38;5;160m██\033[38;5;239m╔══╝\033[38;5;160m██\033[38;5;239m╔═══\033[38;5;160m██\033[38;5;239m╗\033[38;5;160m██\033[38;5;239m╔══\033[38;5;160m██\033[38;5;239m╗ \033[38;5;160m██\033[38;5;239m╗ \033[38;5;160m██\033[38;5;239m╗\n \033[38;5;124m██████\033[38;5;238m╔╝ \033[38;5;124m██\033[38;5;238m║ \033[38;5;124m██\033[38;5;238m║ \033[38;5;124m██\033[38;5;238m║\033[38;5;124m██████\033[38;5;238m╔╝ \033[38;5;124m██████\033[38;5;238m╗\033[38;5;124m██████\033[38;5;238m╗\n \033[38;5;88m██\033[38;5;237m╔══\033[38;5;88m██\033[38;5;237m╗ \033[38;5;88m██\033[38;5;237m║ \033[38;5;88m██\033[38;5;237m║ \033[38;5;88m██\033[38;5;237m║\033[38;5;88m██\033[38;5;237m╔═══╝ ╚═\033[38;5;88m██\033[38;5;237m╔═╝╚═\033[38;5;88m██\033[38;5;237m╔═╝\n \033[38;5;52m██████\033[38;5;236m╔╝ \033[38;5;52m██\033[38;5;236m║ ╚\033[38;5;52m██████\033[38;5;236m╔╝\033[38;5;52m██\033[38;5;236m║ ╚═╝ ╚═╝\n \033[38;5;235m╚═════╝ ╚═╝ ╚═════╝ ╚═╝ \033[1;3;38;5;240mMakefile v1.5\033[0m override BTOP_VERSION := $(shell head -n100 src/btop.cpp 2>/dev/null | grep "Version =" | cut -f2 -d"\"" || echo " unknown") override TIMESTAMP := $(shell date +%s 2>/dev/null || echo "0") +override DATESTAMP := $(shell date '+%Y-%m-%d %H:%M:%S' || echo "5 minutes ago") +ifeq ($(shell command -v gdate >/dev/null; echo $$?),0) + DATE_CMD := gdate +else + DATE_CMD := date +endif ifneq ($(QUIET),true) - override PRE := info + override PRE := info info-quiet override QUIET := false else override PRE := info-quiet @@ -14,33 +20,56 @@ endif PREFIX ?= /usr/local -#? NOTICE! Manually set PLATFORM and ARCH if not compiling for host system +#? Detect PLATFORM and ARCH from uname/gcc if not set PLATFORM ?= $(shell uname -s || echo unknown) -ARCH ?= $(shell uname -m || echo unknown) +ifneq ($(filter unknown Darwin, $(PLATFORM)),) + override PLATFORM := $(shell $(CXX) -dumpmachine | awk -F"-" '{ print (NF==4) ? $$3 : $$2 }') + ifeq ($(PLATFORM),apple) + override PLATFORM := macos + endif +endif +ifeq ($(shell uname -v | grep ARM64 >/dev/null 2>&1; echo $$?),0) + ARCH ?= arm64 +else + ARCH ?= $(shell $(CXX) -dumpmachine | cut -d "-" -f 1) +endif -#? Only enable fcf-protection if on x86_64 -ifeq ($(ARCH),x86_64) - override ADDFLAGS += -fcf-protection +override PLATFORM_LC := $(shell echo $(PLATFORM) | tr '[:upper:]' '[:lower:]') + +#? Any flags added to TESTFLAGS must not contain whitespace for the testing to work +override TESTFLAGS := -fexceptions -fstack-clash-protection -fcf-protection +ifneq ($(PLATFORM) $(ARCH),macos arm64) + override TESTFLAGS += -fstack-protector endif ifeq ($(STATIC),true) - override ADDFLAGS += -static -static-libgcc -static-libstdc++ -endif - -#? Make sure PLATFORM Darwin is OSX and not Darwin -ifeq ($(PLATFORM),Darwin) - ifeq ($(shell sw_vers >/dev/null 2>&1; echo $$?),0) - PLATFORM := OSX + override ADDFLAGS += -static-libgcc -static-libstdc++ + ifneq ($(PLATFORM),macos) + override ADDFLAGS += -DSTATIC_BUILD -static -Wl,--fatal-warnings endif endif +ifeq ($(STRIP),true) + override ADDFLAGS += -s +endif + #? Compiler and Linker -CXX ?= g++ +ifeq ($(shell command -v g++-11 >/dev/null; echo $$?),0) + CXX := g++-11 +else ifeq ($(shell command -v g++11 >/dev/null; echo $$?),0) + CXX := g++11 +else ifeq ($(shell command -v g++ >/dev/null; echo $$?),0) + CXX := g++ +endif override CXX_VERSION := $(shell $(CXX) -dumpfullversion -dumpversion || echo 0) #? Try to make sure we are using GCC/G++ version 11 or later if not instructed to use g++-10 ifeq ($(CXX),g++) - V_MAJOR := $(shell echo $(CXX_VERSION) | cut -f1 -d".") + ifeq ($(shell g++ --version | grep clang >/dev/null 2>&1; echo $$?),0) + V_MAJOR := 0 + else + V_MAJOR := $(shell echo $(CXX_VERSION) | cut -f1 -d".") + endif ifneq ($(shell test $(V_MAJOR) -ge 11; echo $$?),0) ifeq ($(shell command -v g++-11 >/dev/null; echo $$?),0) override CXX := g++-11 @@ -50,21 +79,27 @@ ifeq ($(CXX),g++) endif #? Pull in platform specific source files and get thread count -ifeq ($(PLATFORM),Linux) +ifeq ($(PLATFORM_LC),linux) PLATFORM_DIR := linux THREADS := $(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) -else ifeq ($(PLATFORM),FreeBSD) + SU_GROUP := root +else ifeq ($(PLATFORM_LC),freebsd) PLATFORM_DIR := freebsd THREADS := $(shell getconf NPROCESSORS_ONLN 2>/dev/null || echo 1) -else ifeq ($(PLATFORM),OSX) + SU_GROUP := wheel + override ADDFLAGS += -lstdc++ -lm -lkvm -ldevstat -Wl,-rpath=/usr/local/lib/gcc11 + export MAKE = gmake +else ifeq ($(PLATFORM_LC),macos) PLATFORM_DIR := osx THREADS := $(shell sysctl -n hw.ncpu || echo 1) + override ADDFLAGS += -framework IOKit -framework CoreFoundation -Wno-format-truncation + SU_GROUP := wheel else $(error $(shell printf "\033[1;91mERROR: \033[97mUnsupported platform ($(PLATFORM))\033[0m")) endif #? Use all CPU cores (will only be set if using Make 4.3+) -MAKEFLAGS := --jobs=$(THREADS) +MAKEFLAGS := --jobs=$(THREADS) ifeq ($(THREADS),1) override THREADS := auto endif @@ -78,23 +113,47 @@ SRCEXT := cpp DEPEXT := d OBJEXT := o +#? Filter out unsupported compiler flags +override GOODFLAGS := $(foreach flag,$(TESTFLAGS),$(strip $(shell echo "int main() {}" | $(CXX) -o /dev/null $(flag) -x c++ - >/dev/null 2>&1 && echo $(flag) || true))) + #? Flags, Libraries and Includes override REQFLAGS := -std=c++20 WARNFLAGS := -Wall -Wextra -pedantic -OPTFLAGS ?= -O2 -ftree-loop-vectorize -flto=$(THREADS) -LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector -fstack-clash-protection $(ADDFLAGS) +OPTFLAGS := -O2 -ftree-loop-vectorize -flto=$(THREADS) +LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS $(GOODFLAGS) $(ADDFLAGS) override CXXFLAGS += $(REQFLAGS) $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) override LDFLAGS += $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) INC := -I$(INCDIR) -I$(SRCDIR) SU_USER := root -SU_GROUP := root -SOURCES := $(shell find $(SRCDIR) -maxdepth 1 -type f -name *.$(SRCEXT)) +ifdef DEBUG + override OPTFLAGS := -O0 -g +endif -SOURCES += $(shell find $(SRCDIR)/$(PLATFORM_DIR) -type f -name *.$(SRCEXT)) +SOURCES := $(sort $(shell find $(SRCDIR) -maxdepth 1 -type f -name *.$(SRCEXT))) + +SOURCES += $(sort $(shell find $(SRCDIR)/$(PLATFORM_DIR) -type f -name *.$(SRCEXT))) + +#? Setup percentage progress +SOURCE_COUNT := $(words $(SOURCES)) OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT))) +ifeq ($(shell find $(BUILDDIR) -type f -newermt "$(DATESTAMP)" -name *.o >/dev/null 2>&1; echo $$?),0) + ifneq ($(wildcard $(BUILDDIR)/.*),) + SKIPPED_SOURCES := $(foreach fname,$(SOURCES),$(shell find $(BUILDDIR) -type f -newer $(fname) -name *.o | grep "$(basename $(notdir $(fname))).o" 2>/dev/null)) + override SOURCE_COUNT := $(shell expr $(SOURCE_COUNT) - $(words $(SKIPPED_SOURCES))) + ifeq ($(SOURCE_COUNT),0) + override SOURCE_COUNT = $(words $(SOURCES)) + endif + endif + PROGRESS = expr $$(find $(BUILDDIR) -type f -newermt "$(DATESTAMP)" -name *.o | wc -l || echo 1) '*' 90 / $(SOURCE_COUNT) | cut -c1-2 +else + PROGRESS = expr $$(find $(BUILDDIR) -type f -name *.o | wc -l || echo 1) '*' 90 / $(SOURCE_COUNT) | cut -c1-2 +endif + +P := %% + #? Default Make all: $(PRE) directories btop @@ -111,10 +170,8 @@ info: @printf "\033[1;95mCXXFLAGS \033[1;92m+| \033[0;37m\$$(\033[92mREQFLAGS\033[37m) \$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m)\n" @printf "\033[1;95mLDFLAGS \033[1;92m+| \033[0;37m\$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m)\n" - @printf "\n\033[1;92mBuilding btop++ \033[93m(\033[97mv$(BTOP_VERSION)\033[93m)\033[0m\n" - info-quiet: - + @sleep 0.1 2>/dev/null || true @printf "\n\033[1;92mBuilding btop++ \033[91m(\033[97mv$(BTOP_VERSION)\033[91m) \033[93m$(PLATFORM) \033[96m$(ARCH)\033[0m\n" help: @@ -128,6 +185,7 @@ help: @printf " install Install btop++ to \$$PREFIX ($(PREFIX))\n" @printf " setuid Set installed binary owner/group to \$$SU_USER/\$$SU_GROUP ($(SU_USER)/$(SU_GROUP)) and set SUID bit\n" @printf " uninstall Uninstall btop++ from \$$PREFIX\n" + @printf " info Display information about Environment,compiler and linker flags\n" #? Make the Directories directories: @@ -154,6 +212,16 @@ install: @cp -p README.md $(DESTDIR)$(PREFIX)/share/btop @printf "\033[1;92mInstalling themes to: \033[1;97m$(DESTDIR)$(PREFIX)/share/btop/themes\033[0m\n" @cp -pr themes $(DESTDIR)$(PREFIX)/share/btop + @printf "\033[1;92mInstalling desktop entry to: \033[1;97m$(DESTDIR)$(PREFIX)/share/applications/btop.desktop\n" + @mkdir -p $(DESTDIR)$(PREFIX)/share/applications/ + @cp -p btop.desktop $(DESTDIR)$(PREFIX)/share/applications/btop.desktop + @printf "\033[1;92mInstalling PNG icon to: \033[1;97m$(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/btop.png\n" + @mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps + @cp -p Img/icon.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/btop.png + @printf "\033[1;92mInstalling SVG icon to: \033[1;97m$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/btop.svg\n" + @mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps + @cp -p Img/icon.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/btop.svg + #? Set SUID bit for btop as $SU_USER in $SU_GROUP setuid: @@ -168,6 +236,12 @@ uninstall: @rm -rf $(DESTDIR)$(PREFIX)/bin/btop @printf "\033[1;91mRemoving: \033[1;97m$(DESTDIR)$(PREFIX)/share/btop\033[0m\n" @rm -rf $(DESTDIR)$(PREFIX)/share/btop + @printf "\033[1;91mRemoving: \033[1;97m$(DESTDIR)$(PREFIX)/share/applications/btop.desktop\033[0m\n" + @rm -rf $(DESTDIR)$(PREFIX)/share/applications/btop.desktop + @printf "\033[1;91mRemoving: \033[1;97m$(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/btop.png\033[0m\n" + @rm -rf $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/btop.png + @printf "\033[1;91mRemoving: \033[1;97m$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/btop.svg\033[0m\n" + @rm -rf $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/btop.svg #? Pull in dependency info for *existing* .o files -include $(OBJECTS:.$(OBJEXT)=.$(DEPEXT)) @@ -175,26 +249,21 @@ uninstall: #? Link .ONESHELL: btop: $(OBJECTS) - @sleep 0.1 2>/dev/null || true + @sleep 0.2 2>/dev/null || true @TSTAMP=$$(date +%s 2>/dev/null || echo "0") @$(QUIET) || printf "\n\033[1;92mLinking and optimizing binary\033[37m...\033[0m\n" @$(CXX) -o $(TARGETDIR)/btop $^ $(LDFLAGS) || exit 1 - @printf "\033[1;92m-> \033[1;37m$(TARGETDIR)/btop \033[100D\033[35C\033[1;93m(\033[1;97m$$(du -ah $(TARGETDIR)/btop | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" - printf "\n\033[1;92mBuild complete in \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n" + @printf "\033[1;92m100$(P) -> \033[1;37m$(TARGETDIR)/btop \033[100D\033[38C\033[1;93m(\033[1;97m$$(du -ah $(TARGETDIR)/btop | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$($(DATE_CMD) -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" + @printf "\n\033[1;92mBuild complete in \033[92m(\033[97m$$($(DATE_CMD) -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n" #? Compile .ONESHELL: $(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT) - @sleep 0.1 2>/dev/null || true + @sleep 0.3 2>/dev/null || true @TSTAMP=$$(date +%s 2>/dev/null || echo "0") @$(QUIET) || printf "\033[1;97mCompiling $<\033[0m\n" - @$(CXX) $(CXXFLAGS) $(INC) -c -o $@ $< || exit 1 - @$(CXX) $(CXXFLAGS) $(INC) -MM $(SRCDIR)/$*.$(SRCEXT) > $(BUILDDIR)/$*.$(DEPEXT) >/dev/null || exit 1 - @cp -f $(BUILDDIR)/$*.$(DEPEXT) $(BUILDDIR)/$*.$(DEPEXT).tmp - @sed -e 's|.*:|$(BUILDDIR)/$*.$(OBJEXT):|' < $(BUILDDIR)/$*.$(DEPEXT).tmp > $(BUILDDIR)/$*.$(DEPEXT) - @sed -e 's/.*://' -e 's/\\$$//' < $(BUILDDIR)/$*.$(DEPEXT).tmp | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $(BUILDDIR)/$*.$(DEPEXT) - @rm -f $(BUILDDIR)/$*.$(DEPEXT).tmp - @printf "\033[1;92m-> \033[1;37m$@ \033[100D\033[35C\033[1;93m(\033[1;97m$$(du -ah $@ | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$(date -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" + @$(CXX) $(CXXFLAGS) $(INC) -MMD -c -o $@ $< || exit 1 + @printf "\033[1;92m$$($(PROGRESS))$(P)\033[10D\033[5C-> \033[1;37m$@ \033[100D\033[38C\033[1;93m(\033[1;97m$$(du -ah $@ | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$($(DATE_CMD) -d @$$(expr $$($(DATE_CMD) +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" #? Non-File Targets .PHONY: all msg help pre diff --git a/README.md b/README.md index 596dc42..3520962 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,20 @@ ![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-yellow) ![c++20](https://img.shields.io/badge/cpp-c%2B%2B20-green) -![btop_version](https://img.shields.io/github/v/tag/aristocratos/btop?label=version) +![latest_release](https://img.shields.io/github/v/tag/aristocratos/btop?label=release) [![Donate](https://img.shields.io/badge/-Donate-yellow?logo=paypal)](https://paypal.me/aristocratos) [![Sponsor](https://img.shields.io/badge/-Sponsor-red?logo=github)](https://github.com/sponsors/aristocratos) [![Coffee](https://img.shields.io/badge/-Buy%20me%20a%20Coffee-grey?logo=Ko-fi)](https://ko-fi.com/aristocratos) [![btop](https://snapcraft.io/btop/badge.svg)](https://snapcraft.io/btop) +[![Continuous Build Linux](https://github.com/aristocratos/btop/actions/workflows/continuous-build-linux.yml/badge.svg)](https://github.com/aristocratos/btop/actions/workflows/continuous-build-linux.yml) +[![Continuous Build MacOS](https://github.com/aristocratos/btop/actions/workflows/continuous-build-macos.yml/badge.svg)](https://github.com/aristocratos/btop/actions/workflows/continuous-build-macos.yml) + ## Index - - * [News](#news) * [Documents](#documents) * [Description](#description) @@ -25,15 +28,67 @@ * [Prerequisites](#prerequisites) (Read this if you are having issues!) * [Screenshots](#screenshots) * [Keybindings](#help-menu) -* [Installation](#installation) -* [Manual compilation](#compilation) -* [Install the snap](#install-the-snap) +* [Installation Linux/OSX](#installation) +* [Compilation Linux](#compilation-linux) +* [Compilation OSX](#compilation-osx) +* [Compilation FreeBSD](#compilation-freebsd) +* [Installing the snap](#installing-the-snap) * [Configurability](#configurability) * [License](#license) ## News -### Under development +##### 16 January 2022 + +Release v1.2.0 with FreeBSD support. No release binaries for FreeBSD provided as of yet. + +Again a big thanks to [@joske](https://github.com/joske) for his porting efforts! + +Since compatibility with Linux, MacOS and FreeBSD are done, the focus going forward will be on new features like GPU monitoring. + +##### 13 November 2021 + +Release v1.1.0 with OSX support. Binaries in [continuous-build-macos](https://github.com/aristocratos/btop/actions/workflows/continuous-build-macos.yml) are only x86 for now. +Macos binaries + installer are included for both x86 and ARM64 (Apple Silicon) in the releases. + +Big thank you to [@joske](https://github.com/joske) who wrote the vast majority of the implementation! + +
+More... + +##### 30 October 2021 + +Work on the OSX and FreeBSD branches, both initiated and mostly worked on by [@joske](https://github.com/joske), will likely be completed in the coming weeks. +The OSX branch has some memory leaks that needs to be sorted out and both have some issues with the processes cpu usage calculation and other smaller issues that needs fixing. + +If you want to help out, test for bugs/fix bugs or just try out the branches: + +**OSX** +```bash +# Install and use Homebrew or MacPorts package managers for easy dependency installation +brew install coreutils make gcc@11 +git clone https://github.com/aristocratos/btop.git +cd btop +git checkout OSX +gmake +``` + +**FreeBSD** +```bash +sudo pkg install gmake gcc11 coreutils git +git clone https://github.com/aristocratos/btop.git +cd btop +git checkout freebsd +gmake +``` + +Note that GNU make (`gmake`) is recommended but not required for OSX but it is required on FreeBSD. + + +##### 6 October 2021 + +OsX development have been started by [@joske](https://github.com/joske), big thanks :) +See branch [OSX](https://github.com/aristocratos/btop/tree/OSX) for current progress. ##### 18 September 2021 @@ -59,6 +114,8 @@ Windows support is not in the plans as of now, but if anyone else wants to take This project is gonna take some time until it has complete feature parity with bpytop, since all system information gathering will have to be written from scratch without any external libraries. And will need some help in the form of code contributions to get complete support for BSD and OSX. +
+ ## Documents **[CHANGELOG.md](CHANGELOG.md)** @@ -114,7 +171,7 @@ Any support is greatly appreciated! For best experience, a terminal with support for: -* 24-bit truecolor ([See list of terminals with truecolor support](https://gist.github.com/XVilka/8346728)) +* 24-bit truecolor ([See list of terminals with truecolor support](https://github.com//termstandard/colors)) * 256-color terminals are supported through 24-bit to 256-color conversion when setting "truecolor" to False in the options or with "-lc/--low-color" arguments. * 16 color TTY mode will be activated if a real tty device is detected. Can be forced with "-t/--tty_on" arguments. * Wide characters (Are sometimes problematic in web-based terminals) @@ -167,9 +224,11 @@ Also needs a UTF8 locale and a font that covers: ## Installation -**Binary release (statically compiled, for kernel 3.2.0 and newer)** +**Binaries for Linux are statically compiled with musl and works on kernel 2.6.39 and newer** -1. **Download btop-(VERSION)-(PLATFORM)-(ARCH).tbz from [latest release](https://github.com/aristocratos/btop/releases/latest) and unpack to a new folder** +1. **Download btop-(VERSION)-(ARCH)-(PLATFORM).tbz from [latest release](https://github.com/aristocratos/btop/releases/latest) and unpack to a new folder** + + **Notice! Use x86_64 for 64-bit x86 systems, i486 and i686 are 32-bit!** 2. **Install (from created folder)** @@ -207,17 +266,37 @@ Also needs a UTF8 locale and a font that covers: make help ``` -## Compilation +**Binary release (from native os repo)** + +* **openSUSE** + * **Tumbleweed:** + ```bash + sudo zypper in btop + ``` + * For all other versions, see [openSUSE Software: btop](https://software.opensuse.org/package/btop) + + +**Binary release on Homebrew (macOS (x86_64 & ARM64) / Linux (x86_64))** + +* **[Homebrew](https://formulae.brew.sh/formula/btop)** + ```bash + brew install btop + ``` + +## Compilation Linux Needs GCC 10 or higher, (GCC 11 or above strongly recommended for better CPU efficiency in the compiled binary). The makefile also needs GNU coreutils and `sed` (should already be installed on any modern distribution). + For a `cmake` based build alternative see the [fork](https://github.com/jan-guenter/btop/tree/main) by @jan-guenter + 1. **Install dependencies (example for Ubuntu 21.04 Hirsute)** + Use gcc-10 g++-10 if gcc-11 isn't available + ``` bash sudo apt install coreutils sed git build-essential gcc-11 g++-11 - # use gcc-10 g++-10 if gcc-11 isn't available ``` 2. **Clone repository** @@ -231,13 +310,20 @@ Also needs a UTF8 locale and a font that covers: Append `STATIC=true` to `make` command for static compilation. + Notice! If using LDAP Authentication, usernames will show as UID number for LDAP users if compiling statically with glibc. + Append `QUIET=true` for less verbose output. - Notice! Manually set `$ARCH` variable if cross-compiling + Append `STRIP=true` to force stripping of debug symbols (adds `-s` linker flag). + + Append `ARCH=` to manually set the target architecture. + If omitted the makefile uses the machine triple (output of `-dumpmachine` compiler parameter) to detect the target system. Use `ADDFLAGS` variable for appending flags to both compiler and linker. - For example: `make ADDFLAGS=-march=native` might give a performance boost if compiling only for your own system. + For example: `ADDFLAGS=-march=native` might give a performance boost if compiling only for your own system. + + If `g++` is linked to an older version of gcc on your system specify the correct version by appending `CXX=g++-10` or `CXX=g++-11`. ``` bash make @@ -245,9 +331,11 @@ Also needs a UTF8 locale and a font that covers: 4. **Install** + Append `PREFIX=/target/dir` to set target, default: `/usr/local` + + Notice! Only use "sudo" when installing to a NON user owned directory. + ``` bash - # use "make install PREFIX=/target/dir" to set target, default: /usr/local - # only use "sudo" when installing to a NON user owned directory sudo make install ``` @@ -255,9 +343,11 @@ Also needs a UTF8 locale and a font that covers: No need for `sudo` to enable signal sending to any process and to prevent /proc read permissions problems on some systems. + Run after make install and use same PREFIX if any was used at install. + + Set `SU_USER` and `SU_GROUP` to select user and group, default is `root` and `root` + ``` bash - # run after make install and use same PREFIX if any was used at install - # set SU_USER and SU_GROUP to select user and group, default is root:root sudo make setuid ``` @@ -285,7 +375,177 @@ Also needs a UTF8 locale and a font that covers: make help ``` -## Install the snap +## Compilation OSX + + Needs GCC 10 or higher, (GCC 11 or above strongly recommended for better CPU efficiency in the compiled binary). + + The makefile also needs GNU coreutils and `sed`. + + Install and use Homebrew or MacPorts package managers for easy dependency installation + +1. **Install dependencies (example for Homebrew)** + + ``` bash + brew install coreutils make gcc@11 + ``` + +2. **Clone repository** + + ``` bash + git clone https://github.com/aristocratos/btop.git + cd btop + ``` + +3. **Compile** + + Append `STATIC=true` to `make` command for static compilation (only libgcc and libstdc++ will be static!). + + Append `QUIET=true` for less verbose output. + + Append `STRIP=true` to force stripping of debug symbols (adds `-s` linker flag). + + Append `ARCH=` to manually set the target architecture. + If omitted the makefile uses the machine triple (output of `-dumpmachine` compiler parameter) to detect the target system. + + Use `ADDFLAGS` variable for appending flags to both compiler and linker. + + For example: `ADDFLAGS=-march=native` might give a performance boost if compiling only for your own system. + + ``` bash + gmake + ``` + +4. **Install** + + Append `PREFIX=/target/dir` to set target, default: `/usr/local` + + Notice! Only use "sudo" when installing to a NON user owned directory. + + ``` bash + sudo gmake install + ``` + +5. **(Recommended) Set suid bit to make btop always run as root (or other user)** + + No need for `sudo` to see information for non user owned processes and to enable signal sending to any process. + + Run after make install and use same PREFIX if any was used at install. + + Set `SU_USER` and `SU_GROUP` to select user and group, default is `root` and `wheel` + + ``` bash + sudo gmake setuid + ``` + +* **Uninstall** + + ``` bash + sudo gmake uninstall + ``` + +* **Remove any object files from source dir** + + ```bash + gmake clean + ``` + +* **Remove all object files, binaries and created directories in source dir** + + ```bash + gmake distclean + ``` + +* **Show help** + + ```bash + gmake help + ``` + +## Compilation FreeBSD + + Needs GCC 10 or higher, (GCC 11 or above strongly recommended for better CPU efficiency in the compiled binary). + + Note that GNU make (`gmake`) is required to compile on FreeBSD. + +1. **Install dependencies** + + ``` bash + sudo pkg install gmake gcc11 coreutils git + ``` + +2. **Clone repository** + + ``` bash + git clone https://github.com/aristocratos/btop.git + cd btop + ``` + +3. **Compile** + + Append `STATIC=true` to `make` command for static compilation. + + Append `QUIET=true` for less verbose output. + + Append `STRIP=true` to force stripping of debug symbols (adds `-s` linker flag). + + Append `ARCH=` to manually set the target architecture. + If omitted the makefile uses the machine triple (output of `-dumpmachine` compiler parameter) to detect the target system. + + Use `ADDFLAGS` variable for appending flags to both compiler and linker. + + For example: `ADDFLAGS=-march=native` might give a performance boost if compiling only for your own system. + + ``` bash + gmake + ``` + +4. **Install** + + Append `PREFIX=/target/dir` to set target, default: `/usr/local` + + Notice! Only use "sudo" when installing to a NON user owned directory. + + ``` bash + sudo gmake install + ``` + +5. **(Recommended) Set suid bit to make btop always run as root (or other user)** + + No need for `sudo` to see information for non user owned processes and to enable signal sending to any process. + + Run after make install and use same PREFIX if any was used at install. + + Set `SU_USER` and `SU_GROUP` to select user and group, default is `root` and `wheel` + + ``` bash + sudo gmake setuid + ``` + +* **Uninstall** + + ``` bash + sudo gmake uninstall + ``` + +* **Remove any object files from source dir** + + ```bash + gmake clean + ``` + +* **Remove all object files, binaries and created directories in source dir** + + ```bash + gmake distclean + ``` + +* **Show help** + + ```bash + gmake help + ``` + +## Installing the snap [![btop](https://snapcraft.io/btop/badge.svg)](https://snapcraft.io/btop) * **Install the snap** @@ -297,7 +557,7 @@ Also needs a UTF8 locale and a font that covers: ``` sudo snap install btop --edge ``` - + * **Connect the interface** ```bash @@ -310,17 +570,17 @@ Also needs a UTF8 locale and a font that covers: All options changeable from within UI. Config and log files stored in `$XDG_CONFIG_HOME/btop` or `$HOME/.config/btop` folder -#### btop.cfg: (auto generated if not found) +#### btop.conf: (auto generated if not found) ```bash -#? Config file for btop v. 0.1.0 +#? Config file for btop v. 1.2.2 #* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. #* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" color_theme = "Default" #* If the theme set background should be shown, set to False if you want terminal background transparency. -theme_background = False +theme_background = True #* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. truecolor = True @@ -330,11 +590,15 @@ truecolor = True force_tty = False #* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. -#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positons, G=graph symbol to use for box. -#* Use withespace " " as seprator between different presets. +#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. +#* Use withespace " " as separator between different presets. #* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" +#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. +#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. +vim_keys = False + #* Rounded corners on boxes, is ignored if TTY mode is ON. rounded_corners = True @@ -358,10 +622,10 @@ graph_symbol_net = "default" graph_symbol_proc = "default" #* Manually set which boxes to show. Available values are "cpu mem net proc", separate values with whitespace. -shown_boxes = "cpu mem net proc" +shown_boxes = "proc cpu mem net" #* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. -update_ms = 2000 +update_ms = 1500 #* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive", #* "cpu lazy" sorts top process over time (easier to follow), "cpu responsive" updates top process directly. @@ -429,6 +693,9 @@ cpu_core_map = "" #* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". temp_scale = "celsius" +#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. +base_10_sizes = False + #* Show CPU frequency. show_cpu_freq = True @@ -467,6 +734,9 @@ only_physical = True #* Read disks list from /etc/fstab. This also disables only_physical. use_fstab = False +#* Set to true to show available disk space for privileged users. +disk_free_priv = False + #* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. show_io_stat = True @@ -497,6 +767,9 @@ net_iface = "br0" #* Show battery stats in top right if battery is present. show_battery = True +#* Which battery to use if multiple are present. "Auto" for auto detection. +selected_battery = "Auto" + #* Set loglevel for "~/.config/btop/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". #* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. log_level = "DEBUG" diff --git a/btop.desktop b/btop.desktop new file mode 100644 index 0000000..3771b21 --- /dev/null +++ b/btop.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=btop++ +GenericName=System Monitor +GenericName[it]=Monitor di sistema +Comment=Resource monitor that shows usage and stats for processor, memory, disks, network and processes +Comment[it]=Monitoraggio delle risorse: mostra utilizzo e statistiche per CPU, dischi, rete e processi +Icon=btop +Exec=btop +Terminal=true +Categories=System;Monitor;ConsoleOnly; +Keywords=system;process;task diff --git a/include/robin_hood.h b/include/robin_hood.h index 511a308..0af031f 100644 --- a/include/robin_hood.h +++ b/include/robin_hood.h @@ -36,7 +36,7 @@ // see https://semver.org/ #define ROBIN_HOOD_VERSION_MAJOR 3 // for incompatible API changes #define ROBIN_HOOD_VERSION_MINOR 11 // for adding functionality in a backwards-compatible manner -#define ROBIN_HOOD_VERSION_PATCH 3 // for backwards-compatible bug fixes +#define ROBIN_HOOD_VERSION_PATCH 5 // for backwards-compatible bug fixes #include #include @@ -1820,6 +1820,12 @@ public: InsertionState::key_found != idxAndState.second); } + template + iterator emplace_hint(const_iterator position, Args&&... args) { + (void)position; + return emplace(std::forward(args)...).first; + } + template std::pair try_emplace(const key_type& key, Args&&... args) { return try_emplace_impl(key, std::forward(args)...); @@ -1831,16 +1837,15 @@ public: } template - std::pair try_emplace(const_iterator hint, const key_type& key, - Args&&... args) { + iterator try_emplace(const_iterator hint, const key_type& key, Args&&... args) { (void)hint; - return try_emplace_impl(key, std::forward(args)...); + return try_emplace_impl(key, std::forward(args)...).first; } template - std::pair try_emplace(const_iterator hint, key_type&& key, Args&&... args) { + iterator try_emplace(const_iterator hint, key_type&& key, Args&&... args) { (void)hint; - return try_emplace_impl(std::move(key), std::forward(args)...); + return try_emplace_impl(std::move(key), std::forward(args)...).first; } template @@ -1854,16 +1859,15 @@ public: } template - std::pair insert_or_assign(const_iterator hint, const key_type& key, - Mapped&& obj) { + iterator insert_or_assign(const_iterator hint, const key_type& key, Mapped&& obj) { (void)hint; - return insertOrAssignImpl(key, std::forward(obj)); + return insertOrAssignImpl(key, std::forward(obj)).first; } template - std::pair insert_or_assign(const_iterator hint, key_type&& key, Mapped&& obj) { + iterator insert_or_assign(const_iterator hint, key_type&& key, Mapped&& obj) { (void)hint; - return insertOrAssignImpl(std::move(key), std::forward(obj)); + return insertOrAssignImpl(std::move(key), std::forward(obj)).first; } std::pair insert(const value_type& keyval) { @@ -1871,10 +1875,20 @@ public: return emplace(keyval); } + iterator insert(const_iterator hint, const value_type& keyval) { + (void)hint; + return emplace(keyval).first; + } + std::pair insert(value_type&& keyval) { return emplace(std::move(keyval)); } + iterator insert(const_iterator hint, value_type&& keyval) { + (void)hint; + return emplace(std::move(keyval)).first; + } + // Returns 1 if key is found, 0 otherwise. size_t count(const key_type& key) const { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) @@ -2308,13 +2322,14 @@ private: auto const numElementsWithBuffer = calcNumElementsWithBuffer(max_elements); - // calloc also zeroes everything + // malloc & zero mInfo. Faster than calloc everything. auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer); ROBIN_HOOD_LOG("std::calloc " << numBytesTotal << " = calcNumBytesTotal(" << numElementsWithBuffer << ")") mKeyVals = reinterpret_cast( - detail::assertNotNull(std::calloc(1, numBytesTotal))); + detail::assertNotNull(std::malloc(numBytesTotal))); mInfo = reinterpret_cast(mKeyVals + numElementsWithBuffer); + std::memset(mInfo, 0, numBytesTotal - numElementsWithBuffer * sizeof(Node)); // set sentinel mInfo[numElementsWithBuffer] = 1; diff --git a/include/widechar_width.hpp b/include/widechar_width.hpp new file mode 100644 index 0000000..a5b4c85 --- /dev/null +++ b/include/widechar_width.hpp @@ -0,0 +1,1491 @@ +/** + * widechar_width.h, generated on 2022-02-11. + * See https://github.com/ridiculousfish/widecharwidth/ + * + * SHA1 file hashes: + * UnicodeData.txt: 8a5c26bfb27df8cfab23cf2c34c62d8d3075ae4d + * EastAsianWidth.txt: 8ec36ccac3852bf0c2f02e37c6151551cd14db72 + * emoji-data.txt: 3f0ec08c001c4bc6df0b07d01068fc73808bfb4c + */ + +#ifndef WIDECHAR_WIDTH_H +#define WIDECHAR_WIDTH_H + +#include +#include +#include +#include + +namespace utf8 { + +/* Special width values */ +enum { + widechar_nonprint = 0, // The character is not printable. + widechar_combining = 0, // The character is a zero-width combiner. + widechar_ambiguous = 1, // The character is East-Asian ambiguous width. + widechar_private_use = 1, // The character is for private use. + widechar_unassigned = 0, // The character is unassigned. + widechar_widened_in_9 = 2, // Width is 1 in Unicode 8, 2 in Unicode 9+. + widechar_non_character = 0 // The character is a noncharacter. +}; + +/* An inclusive range of characters. */ +struct widechar_range { + uint32_t lo; + uint32_t hi; +}; + +/* Simple ASCII characters - used a lot, so we check them first. */ +static const struct widechar_range widechar_ascii_table[] = { + {0x00020, 0x0007E} +}; + +/* Private usage range. */ +static const struct widechar_range widechar_private_table[] = { + {0x0E000, 0x0F8FF}, + {0xF0000, 0xFFFFD}, + {0x100000, 0x10FFFD} +}; + +/* Nonprinting characters. */ +static const struct widechar_range widechar_nonprint_table[] = { + {0x00000, 0x0001F}, + {0x0007F, 0x0009F}, + {0x000AD, 0x000AD}, + {0x00600, 0x00605}, + {0x0061C, 0x0061C}, + {0x006DD, 0x006DD}, + {0x0070F, 0x0070F}, + {0x00890, 0x00891}, + {0x008E2, 0x008E2}, + {0x0180E, 0x0180E}, + {0x0200B, 0x0200F}, + {0x02028, 0x0202E}, + {0x02060, 0x02064}, + {0x02066, 0x0206F}, + {0x0D800, 0x0DFFF}, + {0x0FEFF, 0x0FEFF}, + {0x0FFF9, 0x0FFFB}, + {0x110BD, 0x110BD}, + {0x110CD, 0x110CD}, + {0x13430, 0x13438}, + {0x1BCA0, 0x1BCA3}, + {0x1D173, 0x1D17A}, + {0xE0001, 0xE0001}, + {0xE0020, 0xE007F} +}; + +/* Width 0 combining marks. */ +static const struct widechar_range widechar_combining_table[] = { + {0x00300, 0x0036F}, + {0x00483, 0x00489}, + {0x00591, 0x005BD}, + {0x005BF, 0x005BF}, + {0x005C1, 0x005C2}, + {0x005C4, 0x005C5}, + {0x005C7, 0x005C7}, + {0x00610, 0x0061A}, + {0x0064B, 0x0065F}, + {0x00670, 0x00670}, + {0x006D6, 0x006DC}, + {0x006DF, 0x006E4}, + {0x006E7, 0x006E8}, + {0x006EA, 0x006ED}, + {0x00711, 0x00711}, + {0x00730, 0x0074A}, + {0x007A6, 0x007B0}, + {0x007EB, 0x007F3}, + {0x007FD, 0x007FD}, + {0x00816, 0x00819}, + {0x0081B, 0x00823}, + {0x00825, 0x00827}, + {0x00829, 0x0082D}, + {0x00859, 0x0085B}, + {0x00898, 0x0089F}, + {0x008CA, 0x008E1}, + {0x008E3, 0x00903}, + {0x0093A, 0x0093C}, + {0x0093E, 0x0094F}, + {0x00951, 0x00957}, + {0x00962, 0x00963}, + {0x00981, 0x00983}, + {0x009BC, 0x009BC}, + {0x009BE, 0x009C4}, + {0x009C7, 0x009C8}, + {0x009CB, 0x009CD}, + {0x009D7, 0x009D7}, + {0x009E2, 0x009E3}, + {0x009FE, 0x009FE}, + {0x00A01, 0x00A03}, + {0x00A3C, 0x00A3C}, + {0x00A3E, 0x00A42}, + {0x00A47, 0x00A48}, + {0x00A4B, 0x00A4D}, + {0x00A51, 0x00A51}, + {0x00A70, 0x00A71}, + {0x00A75, 0x00A75}, + {0x00A81, 0x00A83}, + {0x00ABC, 0x00ABC}, + {0x00ABE, 0x00AC5}, + {0x00AC7, 0x00AC9}, + {0x00ACB, 0x00ACD}, + {0x00AE2, 0x00AE3}, + {0x00AFA, 0x00AFF}, + {0x00B01, 0x00B03}, + {0x00B3C, 0x00B3C}, + {0x00B3E, 0x00B44}, + {0x00B47, 0x00B48}, + {0x00B4B, 0x00B4D}, + {0x00B55, 0x00B57}, + {0x00B62, 0x00B63}, + {0x00B82, 0x00B82}, + {0x00BBE, 0x00BC2}, + {0x00BC6, 0x00BC8}, + {0x00BCA, 0x00BCD}, + {0x00BD7, 0x00BD7}, + {0x00C00, 0x00C04}, + {0x00C3C, 0x00C3C}, + {0x00C3E, 0x00C44}, + {0x00C46, 0x00C48}, + {0x00C4A, 0x00C4D}, + {0x00C55, 0x00C56}, + {0x00C62, 0x00C63}, + {0x00C81, 0x00C83}, + {0x00CBC, 0x00CBC}, + {0x00CBE, 0x00CC4}, + {0x00CC6, 0x00CC8}, + {0x00CCA, 0x00CCD}, + {0x00CD5, 0x00CD6}, + {0x00CE2, 0x00CE3}, + {0x00D00, 0x00D03}, + {0x00D3B, 0x00D3C}, + {0x00D3E, 0x00D44}, + {0x00D46, 0x00D48}, + {0x00D4A, 0x00D4D}, + {0x00D57, 0x00D57}, + {0x00D62, 0x00D63}, + {0x00D81, 0x00D83}, + {0x00DCA, 0x00DCA}, + {0x00DCF, 0x00DD4}, + {0x00DD6, 0x00DD6}, + {0x00DD8, 0x00DDF}, + {0x00DF2, 0x00DF3}, + {0x00E31, 0x00E31}, + {0x00E34, 0x00E3A}, + {0x00E47, 0x00E4E}, + {0x00EB1, 0x00EB1}, + {0x00EB4, 0x00EBC}, + {0x00EC8, 0x00ECD}, + {0x00F18, 0x00F19}, + {0x00F35, 0x00F35}, + {0x00F37, 0x00F37}, + {0x00F39, 0x00F39}, + {0x00F3E, 0x00F3F}, + {0x00F71, 0x00F84}, + {0x00F86, 0x00F87}, + {0x00F8D, 0x00F97}, + {0x00F99, 0x00FBC}, + {0x00FC6, 0x00FC6}, + {0x0102B, 0x0103E}, + {0x01056, 0x01059}, + {0x0105E, 0x01060}, + {0x01062, 0x01064}, + {0x01067, 0x0106D}, + {0x01071, 0x01074}, + {0x01082, 0x0108D}, + {0x0108F, 0x0108F}, + {0x0109A, 0x0109D}, + {0x0135D, 0x0135F}, + {0x01712, 0x01715}, + {0x01732, 0x01734}, + {0x01752, 0x01753}, + {0x01772, 0x01773}, + {0x017B4, 0x017D3}, + {0x017DD, 0x017DD}, + {0x0180B, 0x0180D}, + {0x0180F, 0x0180F}, + {0x01885, 0x01886}, + {0x018A9, 0x018A9}, + {0x01920, 0x0192B}, + {0x01930, 0x0193B}, + {0x01A17, 0x01A1B}, + {0x01A55, 0x01A5E}, + {0x01A60, 0x01A7C}, + {0x01A7F, 0x01A7F}, + {0x01AB0, 0x01ACE}, + {0x01B00, 0x01B04}, + {0x01B34, 0x01B44}, + {0x01B6B, 0x01B73}, + {0x01B80, 0x01B82}, + {0x01BA1, 0x01BAD}, + {0x01BE6, 0x01BF3}, + {0x01C24, 0x01C37}, + {0x01CD0, 0x01CD2}, + {0x01CD4, 0x01CE8}, + {0x01CED, 0x01CED}, + {0x01CF4, 0x01CF4}, + {0x01CF7, 0x01CF9}, + {0x01DC0, 0x01DFF}, + {0x020D0, 0x020F0}, + {0x02CEF, 0x02CF1}, + {0x02D7F, 0x02D7F}, + {0x02DE0, 0x02DFF}, + {0x0302A, 0x0302F}, + {0x03099, 0x0309A}, + {0x0A66F, 0x0A672}, + {0x0A674, 0x0A67D}, + {0x0A69E, 0x0A69F}, + {0x0A6F0, 0x0A6F1}, + {0x0A802, 0x0A802}, + {0x0A806, 0x0A806}, + {0x0A80B, 0x0A80B}, + {0x0A823, 0x0A827}, + {0x0A82C, 0x0A82C}, + {0x0A880, 0x0A881}, + {0x0A8B4, 0x0A8C5}, + {0x0A8E0, 0x0A8F1}, + {0x0A8FF, 0x0A8FF}, + {0x0A926, 0x0A92D}, + {0x0A947, 0x0A953}, + {0x0A980, 0x0A983}, + {0x0A9B3, 0x0A9C0}, + {0x0A9E5, 0x0A9E5}, + {0x0AA29, 0x0AA36}, + {0x0AA43, 0x0AA43}, + {0x0AA4C, 0x0AA4D}, + {0x0AA7B, 0x0AA7D}, + {0x0AAB0, 0x0AAB0}, + {0x0AAB2, 0x0AAB4}, + {0x0AAB7, 0x0AAB8}, + {0x0AABE, 0x0AABF}, + {0x0AAC1, 0x0AAC1}, + {0x0AAEB, 0x0AAEF}, + {0x0AAF5, 0x0AAF6}, + {0x0ABE3, 0x0ABEA}, + {0x0ABEC, 0x0ABED}, + {0x0FB1E, 0x0FB1E}, + {0x0FE00, 0x0FE0F}, + {0x0FE20, 0x0FE2F}, + {0x101FD, 0x101FD}, + {0x102E0, 0x102E0}, + {0x10376, 0x1037A}, + {0x10A01, 0x10A03}, + {0x10A05, 0x10A06}, + {0x10A0C, 0x10A0F}, + {0x10A38, 0x10A3A}, + {0x10A3F, 0x10A3F}, + {0x10AE5, 0x10AE6}, + {0x10D24, 0x10D27}, + {0x10EAB, 0x10EAC}, + {0x10F46, 0x10F50}, + {0x10F82, 0x10F85}, + {0x11000, 0x11002}, + {0x11038, 0x11046}, + {0x11070, 0x11070}, + {0x11073, 0x11074}, + {0x1107F, 0x11082}, + {0x110B0, 0x110BA}, + {0x110C2, 0x110C2}, + {0x11100, 0x11102}, + {0x11127, 0x11134}, + {0x11145, 0x11146}, + {0x11173, 0x11173}, + {0x11180, 0x11182}, + {0x111B3, 0x111C0}, + {0x111C9, 0x111CC}, + {0x111CE, 0x111CF}, + {0x1122C, 0x11237}, + {0x1123E, 0x1123E}, + {0x112DF, 0x112EA}, + {0x11300, 0x11303}, + {0x1133B, 0x1133C}, + {0x1133E, 0x11344}, + {0x11347, 0x11348}, + {0x1134B, 0x1134D}, + {0x11357, 0x11357}, + {0x11362, 0x11363}, + {0x11366, 0x1136C}, + {0x11370, 0x11374}, + {0x11435, 0x11446}, + {0x1145E, 0x1145E}, + {0x114B0, 0x114C3}, + {0x115AF, 0x115B5}, + {0x115B8, 0x115C0}, + {0x115DC, 0x115DD}, + {0x11630, 0x11640}, + {0x116AB, 0x116B7}, + {0x1171D, 0x1172B}, + {0x1182C, 0x1183A}, + {0x11930, 0x11935}, + {0x11937, 0x11938}, + {0x1193B, 0x1193E}, + {0x11940, 0x11940}, + {0x11942, 0x11943}, + {0x119D1, 0x119D7}, + {0x119DA, 0x119E0}, + {0x119E4, 0x119E4}, + {0x11A01, 0x11A0A}, + {0x11A33, 0x11A39}, + {0x11A3B, 0x11A3E}, + {0x11A47, 0x11A47}, + {0x11A51, 0x11A5B}, + {0x11A8A, 0x11A99}, + {0x11C2F, 0x11C36}, + {0x11C38, 0x11C3F}, + {0x11C92, 0x11CA7}, + {0x11CA9, 0x11CB6}, + {0x11D31, 0x11D36}, + {0x11D3A, 0x11D3A}, + {0x11D3C, 0x11D3D}, + {0x11D3F, 0x11D45}, + {0x11D47, 0x11D47}, + {0x11D8A, 0x11D8E}, + {0x11D90, 0x11D91}, + {0x11D93, 0x11D97}, + {0x11EF3, 0x11EF6}, + {0x16AF0, 0x16AF4}, + {0x16B30, 0x16B36}, + {0x16F4F, 0x16F4F}, + {0x16F51, 0x16F87}, + {0x16F8F, 0x16F92}, + {0x16FE4, 0x16FE4}, + {0x16FF0, 0x16FF1}, + {0x1BC9D, 0x1BC9E}, + {0x1CF00, 0x1CF2D}, + {0x1CF30, 0x1CF46}, + {0x1D165, 0x1D169}, + {0x1D16D, 0x1D172}, + {0x1D17B, 0x1D182}, + {0x1D185, 0x1D18B}, + {0x1D1AA, 0x1D1AD}, + {0x1D242, 0x1D244}, + {0x1DA00, 0x1DA36}, + {0x1DA3B, 0x1DA6C}, + {0x1DA75, 0x1DA75}, + {0x1DA84, 0x1DA84}, + {0x1DA9B, 0x1DA9F}, + {0x1DAA1, 0x1DAAF}, + {0x1E000, 0x1E006}, + {0x1E008, 0x1E018}, + {0x1E01B, 0x1E021}, + {0x1E023, 0x1E024}, + {0x1E026, 0x1E02A}, + {0x1E130, 0x1E136}, + {0x1E2AE, 0x1E2AE}, + {0x1E2EC, 0x1E2EF}, + {0x1E8D0, 0x1E8D6}, + {0x1E944, 0x1E94A}, + {0xE0100, 0xE01EF} +}; + +/* Width 0 combining letters. */ +static const struct widechar_range widechar_combiningletters_table[] = { + {0x01160, 0x011FF}, + {0x0D7B0, 0x0D7FF} +}; + +/* Width 2 characters. */ +static const struct widechar_range widechar_doublewide_table[] = { + {0x01100, 0x0115F}, + {0x02329, 0x0232A}, + {0x02E80, 0x02E99}, + {0x02E9B, 0x02EF3}, + {0x02F00, 0x02FD5}, + {0x02FF0, 0x02FFB}, + {0x03000, 0x0303E}, + {0x03041, 0x03096}, + {0x03099, 0x030FF}, + {0x03105, 0x0312F}, + {0x03131, 0x0318E}, + {0x03190, 0x031E3}, + {0x031F0, 0x0321E}, + {0x03220, 0x03247}, + {0x03250, 0x04DBF}, + {0x04E00, 0x0A48C}, + {0x0A490, 0x0A4C6}, + {0x0A960, 0x0A97C}, + {0x0AC00, 0x0D7A3}, + {0x0F900, 0x0FAFF}, + {0x0FE10, 0x0FE19}, + {0x0FE30, 0x0FE52}, + {0x0FE54, 0x0FE66}, + {0x0FE68, 0x0FE6B}, + {0x0FF01, 0x0FF60}, + {0x0FFE0, 0x0FFE6}, + {0x16FE0, 0x16FE4}, + {0x16FF0, 0x16FF1}, + {0x17000, 0x187F7}, + {0x18800, 0x18CD5}, + {0x18D00, 0x18D08}, + {0x1AFF0, 0x1AFF3}, + {0x1AFF5, 0x1AFFB}, + {0x1AFFD, 0x1AFFE}, + {0x1B000, 0x1B122}, + {0x1B150, 0x1B152}, + {0x1B164, 0x1B167}, + {0x1B170, 0x1B2FB}, + {0x1F200, 0x1F200}, + {0x1F202, 0x1F202}, + {0x1F210, 0x1F219}, + {0x1F21B, 0x1F22E}, + {0x1F230, 0x1F231}, + {0x1F237, 0x1F237}, + {0x1F23B, 0x1F23B}, + {0x1F240, 0x1F248}, + {0x1F260, 0x1F265}, + {0x1F57A, 0x1F57A}, + {0x1F5A4, 0x1F5A4}, + {0x1F6D1, 0x1F6D2}, + {0x1F6D5, 0x1F6D7}, + {0x1F6DD, 0x1F6DF}, + {0x1F6F4, 0x1F6FC}, + {0x1F7E0, 0x1F7EB}, + {0x1F7F0, 0x1F7F0}, + {0x1F90C, 0x1F90F}, + {0x1F919, 0x1F93A}, + {0x1F93C, 0x1F945}, + {0x1F947, 0x1F97F}, + {0x1F985, 0x1F9BF}, + {0x1F9C1, 0x1F9FF}, + {0x1FA70, 0x1FA74}, + {0x1FA78, 0x1FA7C}, + {0x1FA80, 0x1FA86}, + {0x1FA90, 0x1FAAC}, + {0x1FAB0, 0x1FABA}, + {0x1FAC0, 0x1FAC5}, + {0x1FAD0, 0x1FAD9}, + {0x1FAE0, 0x1FAE7}, + {0x1FAF0, 0x1FAF6}, + {0x20000, 0x2FFFD}, + {0x30000, 0x3FFFD} +}; + +/* Ambiguous-width characters. */ +static const struct widechar_range widechar_ambiguous_table[] = { + {0x000A1, 0x000A1}, + {0x000A4, 0x000A4}, + {0x000A7, 0x000A8}, + {0x000AA, 0x000AA}, + {0x000AD, 0x000AE}, + {0x000B0, 0x000B4}, + {0x000B6, 0x000BA}, + {0x000BC, 0x000BF}, + {0x000C6, 0x000C6}, + {0x000D0, 0x000D0}, + {0x000D7, 0x000D8}, + {0x000DE, 0x000E1}, + {0x000E6, 0x000E6}, + {0x000E8, 0x000EA}, + {0x000EC, 0x000ED}, + {0x000F0, 0x000F0}, + {0x000F2, 0x000F3}, + {0x000F7, 0x000FA}, + {0x000FC, 0x000FC}, + {0x000FE, 0x000FE}, + {0x00101, 0x00101}, + {0x00111, 0x00111}, + {0x00113, 0x00113}, + {0x0011B, 0x0011B}, + {0x00126, 0x00127}, + {0x0012B, 0x0012B}, + {0x00131, 0x00133}, + {0x00138, 0x00138}, + {0x0013F, 0x00142}, + {0x00144, 0x00144}, + {0x00148, 0x0014B}, + {0x0014D, 0x0014D}, + {0x00152, 0x00153}, + {0x00166, 0x00167}, + {0x0016B, 0x0016B}, + {0x001CE, 0x001CE}, + {0x001D0, 0x001D0}, + {0x001D2, 0x001D2}, + {0x001D4, 0x001D4}, + {0x001D6, 0x001D6}, + {0x001D8, 0x001D8}, + {0x001DA, 0x001DA}, + {0x001DC, 0x001DC}, + {0x00251, 0x00251}, + {0x00261, 0x00261}, + {0x002C4, 0x002C4}, + {0x002C7, 0x002C7}, + {0x002C9, 0x002CB}, + {0x002CD, 0x002CD}, + {0x002D0, 0x002D0}, + {0x002D8, 0x002DB}, + {0x002DD, 0x002DD}, + {0x002DF, 0x002DF}, + {0x00300, 0x0036F}, + {0x00391, 0x003A1}, + {0x003A3, 0x003A9}, + {0x003B1, 0x003C1}, + {0x003C3, 0x003C9}, + {0x00401, 0x00401}, + {0x00410, 0x0044F}, + {0x00451, 0x00451}, + {0x02010, 0x02010}, + {0x02013, 0x02016}, + {0x02018, 0x02019}, + {0x0201C, 0x0201D}, + {0x02020, 0x02022}, + {0x02024, 0x02027}, + {0x02030, 0x02030}, + {0x02032, 0x02033}, + {0x02035, 0x02035}, + {0x0203B, 0x0203B}, + {0x0203E, 0x0203E}, + {0x02074, 0x02074}, + {0x0207F, 0x0207F}, + {0x02081, 0x02084}, + {0x020AC, 0x020AC}, + {0x02103, 0x02103}, + {0x02105, 0x02105}, + {0x02109, 0x02109}, + {0x02113, 0x02113}, + {0x02116, 0x02116}, + {0x02121, 0x02122}, + {0x02126, 0x02126}, + {0x0212B, 0x0212B}, + {0x02153, 0x02154}, + {0x0215B, 0x0215E}, + {0x02160, 0x0216B}, + {0x02170, 0x02179}, + {0x02189, 0x02189}, + {0x02190, 0x02199}, + {0x021B8, 0x021B9}, + {0x021D2, 0x021D2}, + {0x021D4, 0x021D4}, + {0x021E7, 0x021E7}, + {0x02200, 0x02200}, + {0x02202, 0x02203}, + {0x02207, 0x02208}, + {0x0220B, 0x0220B}, + {0x0220F, 0x0220F}, + {0x02211, 0x02211}, + {0x02215, 0x02215}, + {0x0221A, 0x0221A}, + {0x0221D, 0x02220}, + {0x02223, 0x02223}, + {0x02225, 0x02225}, + {0x02227, 0x0222C}, + {0x0222E, 0x0222E}, + {0x02234, 0x02237}, + {0x0223C, 0x0223D}, + {0x02248, 0x02248}, + {0x0224C, 0x0224C}, + {0x02252, 0x02252}, + {0x02260, 0x02261}, + {0x02264, 0x02267}, + {0x0226A, 0x0226B}, + {0x0226E, 0x0226F}, + {0x02282, 0x02283}, + {0x02286, 0x02287}, + {0x02295, 0x02295}, + {0x02299, 0x02299}, + {0x022A5, 0x022A5}, + {0x022BF, 0x022BF}, + {0x02312, 0x02312}, + {0x02460, 0x024E9}, + {0x024EB, 0x0254B}, + {0x02550, 0x02573}, + {0x02580, 0x0258F}, + {0x02592, 0x02595}, + {0x025A0, 0x025A1}, + {0x025A3, 0x025A9}, + {0x025B2, 0x025B3}, + {0x025B6, 0x025B7}, + {0x025BC, 0x025BD}, + {0x025C0, 0x025C1}, + {0x025C6, 0x025C8}, + {0x025CB, 0x025CB}, + {0x025CE, 0x025D1}, + {0x025E2, 0x025E5}, + {0x025EF, 0x025EF}, + {0x02605, 0x02606}, + {0x02609, 0x02609}, + {0x0260E, 0x0260F}, + {0x0261C, 0x0261C}, + {0x0261E, 0x0261E}, + {0x02640, 0x02640}, + {0x02642, 0x02642}, + {0x02660, 0x02661}, + {0x02663, 0x02665}, + {0x02667, 0x0266A}, + {0x0266C, 0x0266D}, + {0x0266F, 0x0266F}, + {0x0269E, 0x0269F}, + {0x026BF, 0x026BF}, + {0x026C6, 0x026CD}, + {0x026CF, 0x026D3}, + {0x026D5, 0x026E1}, + {0x026E3, 0x026E3}, + {0x026E8, 0x026E9}, + {0x026EB, 0x026F1}, + {0x026F4, 0x026F4}, + {0x026F6, 0x026F9}, + {0x026FB, 0x026FC}, + {0x026FE, 0x026FF}, + {0x0273D, 0x0273D}, + {0x02776, 0x0277F}, + {0x02B56, 0x02B59}, + {0x03248, 0x0324F}, + {0x0E000, 0x0F8FF}, + {0x0FE00, 0x0FE0F}, + {0x0FFFD, 0x0FFFD}, + {0x1F100, 0x1F10A}, + {0x1F110, 0x1F12D}, + {0x1F130, 0x1F169}, + {0x1F170, 0x1F18D}, + {0x1F18F, 0x1F190}, + {0x1F19B, 0x1F1AC}, + {0xE0100, 0xE01EF}, + {0xF0000, 0xFFFFD}, + {0x100000, 0x10FFFD} +}; + +/* Unassigned characters. */ +static const struct widechar_range widechar_unassigned_table[] = { + {0x00378, 0x00379}, + {0x00380, 0x00383}, + {0x0038B, 0x0038B}, + {0x0038D, 0x0038D}, + {0x003A2, 0x003A2}, + {0x00530, 0x00530}, + {0x00557, 0x00558}, + {0x0058B, 0x0058C}, + {0x00590, 0x00590}, + {0x005C8, 0x005CF}, + {0x005EB, 0x005EE}, + {0x005F5, 0x005FF}, + {0x0070E, 0x0070E}, + {0x0074B, 0x0074C}, + {0x007B2, 0x007BF}, + {0x007FB, 0x007FC}, + {0x0082E, 0x0082F}, + {0x0083F, 0x0083F}, + {0x0085C, 0x0085D}, + {0x0085F, 0x0085F}, + {0x0086B, 0x0086F}, + {0x0088F, 0x0088F}, + {0x00892, 0x00897}, + {0x00984, 0x00984}, + {0x0098D, 0x0098E}, + {0x00991, 0x00992}, + {0x009A9, 0x009A9}, + {0x009B1, 0x009B1}, + {0x009B3, 0x009B5}, + {0x009BA, 0x009BB}, + {0x009C5, 0x009C6}, + {0x009C9, 0x009CA}, + {0x009CF, 0x009D6}, + {0x009D8, 0x009DB}, + {0x009DE, 0x009DE}, + {0x009E4, 0x009E5}, + {0x009FF, 0x00A00}, + {0x00A04, 0x00A04}, + {0x00A0B, 0x00A0E}, + {0x00A11, 0x00A12}, + {0x00A29, 0x00A29}, + {0x00A31, 0x00A31}, + {0x00A34, 0x00A34}, + {0x00A37, 0x00A37}, + {0x00A3A, 0x00A3B}, + {0x00A3D, 0x00A3D}, + {0x00A43, 0x00A46}, + {0x00A49, 0x00A4A}, + {0x00A4E, 0x00A50}, + {0x00A52, 0x00A58}, + {0x00A5D, 0x00A5D}, + {0x00A5F, 0x00A65}, + {0x00A77, 0x00A80}, + {0x00A84, 0x00A84}, + {0x00A8E, 0x00A8E}, + {0x00A92, 0x00A92}, + {0x00AA9, 0x00AA9}, + {0x00AB1, 0x00AB1}, + {0x00AB4, 0x00AB4}, + {0x00ABA, 0x00ABB}, + {0x00AC6, 0x00AC6}, + {0x00ACA, 0x00ACA}, + {0x00ACE, 0x00ACF}, + {0x00AD1, 0x00ADF}, + {0x00AE4, 0x00AE5}, + {0x00AF2, 0x00AF8}, + {0x00B00, 0x00B00}, + {0x00B04, 0x00B04}, + {0x00B0D, 0x00B0E}, + {0x00B11, 0x00B12}, + {0x00B29, 0x00B29}, + {0x00B31, 0x00B31}, + {0x00B34, 0x00B34}, + {0x00B3A, 0x00B3B}, + {0x00B45, 0x00B46}, + {0x00B49, 0x00B4A}, + {0x00B4E, 0x00B54}, + {0x00B58, 0x00B5B}, + {0x00B5E, 0x00B5E}, + {0x00B64, 0x00B65}, + {0x00B78, 0x00B81}, + {0x00B84, 0x00B84}, + {0x00B8B, 0x00B8D}, + {0x00B91, 0x00B91}, + {0x00B96, 0x00B98}, + {0x00B9B, 0x00B9B}, + {0x00B9D, 0x00B9D}, + {0x00BA0, 0x00BA2}, + {0x00BA5, 0x00BA7}, + {0x00BAB, 0x00BAD}, + {0x00BBA, 0x00BBD}, + {0x00BC3, 0x00BC5}, + {0x00BC9, 0x00BC9}, + {0x00BCE, 0x00BCF}, + {0x00BD1, 0x00BD6}, + {0x00BD8, 0x00BE5}, + {0x00BFB, 0x00BFF}, + {0x00C0D, 0x00C0D}, + {0x00C11, 0x00C11}, + {0x00C29, 0x00C29}, + {0x00C3A, 0x00C3B}, + {0x00C45, 0x00C45}, + {0x00C49, 0x00C49}, + {0x00C4E, 0x00C54}, + {0x00C57, 0x00C57}, + {0x00C5B, 0x00C5C}, + {0x00C5E, 0x00C5F}, + {0x00C64, 0x00C65}, + {0x00C70, 0x00C76}, + {0x00C8D, 0x00C8D}, + {0x00C91, 0x00C91}, + {0x00CA9, 0x00CA9}, + {0x00CB4, 0x00CB4}, + {0x00CBA, 0x00CBB}, + {0x00CC5, 0x00CC5}, + {0x00CC9, 0x00CC9}, + {0x00CCE, 0x00CD4}, + {0x00CD7, 0x00CDC}, + {0x00CDF, 0x00CDF}, + {0x00CE4, 0x00CE5}, + {0x00CF0, 0x00CF0}, + {0x00CF3, 0x00CFF}, + {0x00D0D, 0x00D0D}, + {0x00D11, 0x00D11}, + {0x00D45, 0x00D45}, + {0x00D49, 0x00D49}, + {0x00D50, 0x00D53}, + {0x00D64, 0x00D65}, + {0x00D80, 0x00D80}, + {0x00D84, 0x00D84}, + {0x00D97, 0x00D99}, + {0x00DB2, 0x00DB2}, + {0x00DBC, 0x00DBC}, + {0x00DBE, 0x00DBF}, + {0x00DC7, 0x00DC9}, + {0x00DCB, 0x00DCE}, + {0x00DD5, 0x00DD5}, + {0x00DD7, 0x00DD7}, + {0x00DE0, 0x00DE5}, + {0x00DF0, 0x00DF1}, + {0x00DF5, 0x00E00}, + {0x00E3B, 0x00E3E}, + {0x00E5C, 0x00E80}, + {0x00E83, 0x00E83}, + {0x00E85, 0x00E85}, + {0x00E8B, 0x00E8B}, + {0x00EA4, 0x00EA4}, + {0x00EA6, 0x00EA6}, + {0x00EBE, 0x00EBF}, + {0x00EC5, 0x00EC5}, + {0x00EC7, 0x00EC7}, + {0x00ECE, 0x00ECF}, + {0x00EDA, 0x00EDB}, + {0x00EE0, 0x00EFF}, + {0x00F48, 0x00F48}, + {0x00F6D, 0x00F70}, + {0x00F98, 0x00F98}, + {0x00FBD, 0x00FBD}, + {0x00FCD, 0x00FCD}, + {0x00FDB, 0x00FFF}, + {0x010C6, 0x010C6}, + {0x010C8, 0x010CC}, + {0x010CE, 0x010CF}, + {0x01249, 0x01249}, + {0x0124E, 0x0124F}, + {0x01257, 0x01257}, + {0x01259, 0x01259}, + {0x0125E, 0x0125F}, + {0x01289, 0x01289}, + {0x0128E, 0x0128F}, + {0x012B1, 0x012B1}, + {0x012B6, 0x012B7}, + {0x012BF, 0x012BF}, + {0x012C1, 0x012C1}, + {0x012C6, 0x012C7}, + {0x012D7, 0x012D7}, + {0x01311, 0x01311}, + {0x01316, 0x01317}, + {0x0135B, 0x0135C}, + {0x0137D, 0x0137F}, + {0x0139A, 0x0139F}, + {0x013F6, 0x013F7}, + {0x013FE, 0x013FF}, + {0x0169D, 0x0169F}, + {0x016F9, 0x016FF}, + {0x01716, 0x0171E}, + {0x01737, 0x0173F}, + {0x01754, 0x0175F}, + {0x0176D, 0x0176D}, + {0x01771, 0x01771}, + {0x01774, 0x0177F}, + {0x017DE, 0x017DF}, + {0x017EA, 0x017EF}, + {0x017FA, 0x017FF}, + {0x0181A, 0x0181F}, + {0x01879, 0x0187F}, + {0x018AB, 0x018AF}, + {0x018F6, 0x018FF}, + {0x0191F, 0x0191F}, + {0x0192C, 0x0192F}, + {0x0193C, 0x0193F}, + {0x01941, 0x01943}, + {0x0196E, 0x0196F}, + {0x01975, 0x0197F}, + {0x019AC, 0x019AF}, + {0x019CA, 0x019CF}, + {0x019DB, 0x019DD}, + {0x01A1C, 0x01A1D}, + {0x01A5F, 0x01A5F}, + {0x01A7D, 0x01A7E}, + {0x01A8A, 0x01A8F}, + {0x01A9A, 0x01A9F}, + {0x01AAE, 0x01AAF}, + {0x01ACF, 0x01AFF}, + {0x01B4D, 0x01B4F}, + {0x01B7F, 0x01B7F}, + {0x01BF4, 0x01BFB}, + {0x01C38, 0x01C3A}, + {0x01C4A, 0x01C4C}, + {0x01C89, 0x01C8F}, + {0x01CBB, 0x01CBC}, + {0x01CC8, 0x01CCF}, + {0x01CFB, 0x01CFF}, + {0x01F16, 0x01F17}, + {0x01F1E, 0x01F1F}, + {0x01F46, 0x01F47}, + {0x01F4E, 0x01F4F}, + {0x01F58, 0x01F58}, + {0x01F5A, 0x01F5A}, + {0x01F5C, 0x01F5C}, + {0x01F5E, 0x01F5E}, + {0x01F7E, 0x01F7F}, + {0x01FB5, 0x01FB5}, + {0x01FC5, 0x01FC5}, + {0x01FD4, 0x01FD5}, + {0x01FDC, 0x01FDC}, + {0x01FF0, 0x01FF1}, + {0x01FF5, 0x01FF5}, + {0x01FFF, 0x01FFF}, + {0x02065, 0x02065}, + {0x02072, 0x02073}, + {0x0208F, 0x0208F}, + {0x0209D, 0x0209F}, + {0x020C1, 0x020CF}, + {0x020F1, 0x020FF}, + {0x0218C, 0x0218F}, + {0x02427, 0x0243F}, + {0x0244B, 0x0245F}, + {0x02B74, 0x02B75}, + {0x02B96, 0x02B96}, + {0x02CF4, 0x02CF8}, + {0x02D26, 0x02D26}, + {0x02D28, 0x02D2C}, + {0x02D2E, 0x02D2F}, + {0x02D68, 0x02D6E}, + {0x02D71, 0x02D7E}, + {0x02D97, 0x02D9F}, + {0x02DA7, 0x02DA7}, + {0x02DAF, 0x02DAF}, + {0x02DB7, 0x02DB7}, + {0x02DBF, 0x02DBF}, + {0x02DC7, 0x02DC7}, + {0x02DCF, 0x02DCF}, + {0x02DD7, 0x02DD7}, + {0x02DDF, 0x02DDF}, + {0x02E5E, 0x02E7F}, + {0x02E9A, 0x02E9A}, + {0x02EF4, 0x02EFF}, + {0x02FD6, 0x02FEF}, + {0x02FFC, 0x02FFF}, + {0x03040, 0x03040}, + {0x03097, 0x03098}, + {0x03100, 0x03104}, + {0x03130, 0x03130}, + {0x0318F, 0x0318F}, + {0x031E4, 0x031EF}, + {0x0321F, 0x0321F}, + {0x03401, 0x04DBE}, + {0x04E01, 0x09FFE}, + {0x0A48D, 0x0A48F}, + {0x0A4C7, 0x0A4CF}, + {0x0A62C, 0x0A63F}, + {0x0A6F8, 0x0A6FF}, + {0x0A7CB, 0x0A7CF}, + {0x0A7D2, 0x0A7D2}, + {0x0A7D4, 0x0A7D4}, + {0x0A7DA, 0x0A7F1}, + {0x0A82D, 0x0A82F}, + {0x0A83A, 0x0A83F}, + {0x0A878, 0x0A87F}, + {0x0A8C6, 0x0A8CD}, + {0x0A8DA, 0x0A8DF}, + {0x0A954, 0x0A95E}, + {0x0A97D, 0x0A97F}, + {0x0A9CE, 0x0A9CE}, + {0x0A9DA, 0x0A9DD}, + {0x0A9FF, 0x0A9FF}, + {0x0AA37, 0x0AA3F}, + {0x0AA4E, 0x0AA4F}, + {0x0AA5A, 0x0AA5B}, + {0x0AAC3, 0x0AADA}, + {0x0AAF7, 0x0AB00}, + {0x0AB07, 0x0AB08}, + {0x0AB0F, 0x0AB10}, + {0x0AB17, 0x0AB1F}, + {0x0AB27, 0x0AB27}, + {0x0AB2F, 0x0AB2F}, + {0x0AB6C, 0x0AB6F}, + {0x0ABEE, 0x0ABEF}, + {0x0ABFA, 0x0ABFF}, + {0x0AC01, 0x0D7A2}, + {0x0D7A4, 0x0D7AF}, + {0x0D7C7, 0x0D7CA}, + {0x0D7FC, 0x0D7FF}, + {0x0FA6E, 0x0FA6F}, + {0x0FADA, 0x0FAFF}, + {0x0FB07, 0x0FB12}, + {0x0FB18, 0x0FB1C}, + {0x0FB37, 0x0FB37}, + {0x0FB3D, 0x0FB3D}, + {0x0FB3F, 0x0FB3F}, + {0x0FB42, 0x0FB42}, + {0x0FB45, 0x0FB45}, + {0x0FBC3, 0x0FBD2}, + {0x0FD90, 0x0FD91}, + {0x0FDC8, 0x0FDCE}, + {0x0FE1A, 0x0FE1F}, + {0x0FE53, 0x0FE53}, + {0x0FE67, 0x0FE67}, + {0x0FE6C, 0x0FE6F}, + {0x0FE75, 0x0FE75}, + {0x0FEFD, 0x0FEFE}, + {0x0FF00, 0x0FF00}, + {0x0FFBF, 0x0FFC1}, + {0x0FFC8, 0x0FFC9}, + {0x0FFD0, 0x0FFD1}, + {0x0FFD8, 0x0FFD9}, + {0x0FFDD, 0x0FFDF}, + {0x0FFE7, 0x0FFE7}, + {0x0FFEF, 0x0FFF8}, + {0x1000C, 0x1000C}, + {0x10027, 0x10027}, + {0x1003B, 0x1003B}, + {0x1003E, 0x1003E}, + {0x1004E, 0x1004F}, + {0x1005E, 0x1007F}, + {0x100FB, 0x100FF}, + {0x10103, 0x10106}, + {0x10134, 0x10136}, + {0x1018F, 0x1018F}, + {0x1019D, 0x1019F}, + {0x101A1, 0x101CF}, + {0x101FE, 0x1027F}, + {0x1029D, 0x1029F}, + {0x102D1, 0x102DF}, + {0x102FC, 0x102FF}, + {0x10324, 0x1032C}, + {0x1034B, 0x1034F}, + {0x1037B, 0x1037F}, + {0x1039E, 0x1039E}, + {0x103C4, 0x103C7}, + {0x103D6, 0x103FF}, + {0x1049E, 0x1049F}, + {0x104AA, 0x104AF}, + {0x104D4, 0x104D7}, + {0x104FC, 0x104FF}, + {0x10528, 0x1052F}, + {0x10564, 0x1056E}, + {0x1057B, 0x1057B}, + {0x1058B, 0x1058B}, + {0x10593, 0x10593}, + {0x10596, 0x10596}, + {0x105A2, 0x105A2}, + {0x105B2, 0x105B2}, + {0x105BA, 0x105BA}, + {0x105BD, 0x105FF}, + {0x10737, 0x1073F}, + {0x10756, 0x1075F}, + {0x10768, 0x1077F}, + {0x10786, 0x10786}, + {0x107B1, 0x107B1}, + {0x107BB, 0x107FF}, + {0x10806, 0x10807}, + {0x10809, 0x10809}, + {0x10836, 0x10836}, + {0x10839, 0x1083B}, + {0x1083D, 0x1083E}, + {0x10856, 0x10856}, + {0x1089F, 0x108A6}, + {0x108B0, 0x108DF}, + {0x108F3, 0x108F3}, + {0x108F6, 0x108FA}, + {0x1091C, 0x1091E}, + {0x1093A, 0x1093E}, + {0x10940, 0x1097F}, + {0x109B8, 0x109BB}, + {0x109D0, 0x109D1}, + {0x10A04, 0x10A04}, + {0x10A07, 0x10A0B}, + {0x10A14, 0x10A14}, + {0x10A18, 0x10A18}, + {0x10A36, 0x10A37}, + {0x10A3B, 0x10A3E}, + {0x10A49, 0x10A4F}, + {0x10A59, 0x10A5F}, + {0x10AA0, 0x10ABF}, + {0x10AE7, 0x10AEA}, + {0x10AF7, 0x10AFF}, + {0x10B36, 0x10B38}, + {0x10B56, 0x10B57}, + {0x10B73, 0x10B77}, + {0x10B92, 0x10B98}, + {0x10B9D, 0x10BA8}, + {0x10BB0, 0x10BFF}, + {0x10C49, 0x10C7F}, + {0x10CB3, 0x10CBF}, + {0x10CF3, 0x10CF9}, + {0x10D28, 0x10D2F}, + {0x10D3A, 0x10E5F}, + {0x10E7F, 0x10E7F}, + {0x10EAA, 0x10EAA}, + {0x10EAE, 0x10EAF}, + {0x10EB2, 0x10EFF}, + {0x10F28, 0x10F2F}, + {0x10F5A, 0x10F6F}, + {0x10F8A, 0x10FAF}, + {0x10FCC, 0x10FDF}, + {0x10FF7, 0x10FFF}, + {0x1104E, 0x11051}, + {0x11076, 0x1107E}, + {0x110C3, 0x110CC}, + {0x110CE, 0x110CF}, + {0x110E9, 0x110EF}, + {0x110FA, 0x110FF}, + {0x11135, 0x11135}, + {0x11148, 0x1114F}, + {0x11177, 0x1117F}, + {0x111E0, 0x111E0}, + {0x111F5, 0x111FF}, + {0x11212, 0x11212}, + {0x1123F, 0x1127F}, + {0x11287, 0x11287}, + {0x11289, 0x11289}, + {0x1128E, 0x1128E}, + {0x1129E, 0x1129E}, + {0x112AA, 0x112AF}, + {0x112EB, 0x112EF}, + {0x112FA, 0x112FF}, + {0x11304, 0x11304}, + {0x1130D, 0x1130E}, + {0x11311, 0x11312}, + {0x11329, 0x11329}, + {0x11331, 0x11331}, + {0x11334, 0x11334}, + {0x1133A, 0x1133A}, + {0x11345, 0x11346}, + {0x11349, 0x1134A}, + {0x1134E, 0x1134F}, + {0x11351, 0x11356}, + {0x11358, 0x1135C}, + {0x11364, 0x11365}, + {0x1136D, 0x1136F}, + {0x11375, 0x113FF}, + {0x1145C, 0x1145C}, + {0x11462, 0x1147F}, + {0x114C8, 0x114CF}, + {0x114DA, 0x1157F}, + {0x115B6, 0x115B7}, + {0x115DE, 0x115FF}, + {0x11645, 0x1164F}, + {0x1165A, 0x1165F}, + {0x1166D, 0x1167F}, + {0x116BA, 0x116BF}, + {0x116CA, 0x116FF}, + {0x1171B, 0x1171C}, + {0x1172C, 0x1172F}, + {0x11747, 0x117FF}, + {0x1183C, 0x1189F}, + {0x118F3, 0x118FE}, + {0x11907, 0x11908}, + {0x1190A, 0x1190B}, + {0x11914, 0x11914}, + {0x11917, 0x11917}, + {0x11936, 0x11936}, + {0x11939, 0x1193A}, + {0x11947, 0x1194F}, + {0x1195A, 0x1199F}, + {0x119A8, 0x119A9}, + {0x119D8, 0x119D9}, + {0x119E5, 0x119FF}, + {0x11A48, 0x11A4F}, + {0x11AA3, 0x11AAF}, + {0x11AF9, 0x11BFF}, + {0x11C09, 0x11C09}, + {0x11C37, 0x11C37}, + {0x11C46, 0x11C4F}, + {0x11C6D, 0x11C6F}, + {0x11C90, 0x11C91}, + {0x11CA8, 0x11CA8}, + {0x11CB7, 0x11CFF}, + {0x11D07, 0x11D07}, + {0x11D0A, 0x11D0A}, + {0x11D37, 0x11D39}, + {0x11D3B, 0x11D3B}, + {0x11D3E, 0x11D3E}, + {0x11D48, 0x11D4F}, + {0x11D5A, 0x11D5F}, + {0x11D66, 0x11D66}, + {0x11D69, 0x11D69}, + {0x11D8F, 0x11D8F}, + {0x11D92, 0x11D92}, + {0x11D99, 0x11D9F}, + {0x11DAA, 0x11EDF}, + {0x11EF9, 0x11FAF}, + {0x11FB1, 0x11FBF}, + {0x11FF2, 0x11FFE}, + {0x1239A, 0x123FF}, + {0x1246F, 0x1246F}, + {0x12475, 0x1247F}, + {0x12544, 0x12F8F}, + {0x12FF3, 0x12FFF}, + {0x1342F, 0x1342F}, + {0x13439, 0x143FF}, + {0x14647, 0x167FF}, + {0x16A39, 0x16A3F}, + {0x16A5F, 0x16A5F}, + {0x16A6A, 0x16A6D}, + {0x16ABF, 0x16ABF}, + {0x16ACA, 0x16ACF}, + {0x16AEE, 0x16AEF}, + {0x16AF6, 0x16AFF}, + {0x16B46, 0x16B4F}, + {0x16B5A, 0x16B5A}, + {0x16B62, 0x16B62}, + {0x16B78, 0x16B7C}, + {0x16B90, 0x16E3F}, + {0x16E9B, 0x16EFF}, + {0x16F4B, 0x16F4E}, + {0x16F88, 0x16F8E}, + {0x16FA0, 0x16FDF}, + {0x16FE5, 0x16FEF}, + {0x16FF2, 0x16FFF}, + {0x17001, 0x187F6}, + {0x187F8, 0x187FF}, + {0x18CD6, 0x18CFF}, + {0x18D01, 0x18D07}, + {0x18D09, 0x1AFEF}, + {0x1AFF4, 0x1AFF4}, + {0x1AFFC, 0x1AFFC}, + {0x1AFFF, 0x1AFFF}, + {0x1B123, 0x1B14F}, + {0x1B153, 0x1B163}, + {0x1B168, 0x1B16F}, + {0x1B2FC, 0x1BBFF}, + {0x1BC6B, 0x1BC6F}, + {0x1BC7D, 0x1BC7F}, + {0x1BC89, 0x1BC8F}, + {0x1BC9A, 0x1BC9B}, + {0x1BCA4, 0x1CEFF}, + {0x1CF2E, 0x1CF2F}, + {0x1CF47, 0x1CF4F}, + {0x1CFC4, 0x1CFFF}, + {0x1D0F6, 0x1D0FF}, + {0x1D127, 0x1D128}, + {0x1D1EB, 0x1D1FF}, + {0x1D246, 0x1D2DF}, + {0x1D2F4, 0x1D2FF}, + {0x1D357, 0x1D35F}, + {0x1D379, 0x1D3FF}, + {0x1D455, 0x1D455}, + {0x1D49D, 0x1D49D}, + {0x1D4A0, 0x1D4A1}, + {0x1D4A3, 0x1D4A4}, + {0x1D4A7, 0x1D4A8}, + {0x1D4AD, 0x1D4AD}, + {0x1D4BA, 0x1D4BA}, + {0x1D4BC, 0x1D4BC}, + {0x1D4C4, 0x1D4C4}, + {0x1D506, 0x1D506}, + {0x1D50B, 0x1D50C}, + {0x1D515, 0x1D515}, + {0x1D51D, 0x1D51D}, + {0x1D53A, 0x1D53A}, + {0x1D53F, 0x1D53F}, + {0x1D545, 0x1D545}, + {0x1D547, 0x1D549}, + {0x1D551, 0x1D551}, + {0x1D6A6, 0x1D6A7}, + {0x1D7CC, 0x1D7CD}, + {0x1DA8C, 0x1DA9A}, + {0x1DAA0, 0x1DAA0}, + {0x1DAB0, 0x1DEFF}, + {0x1DF1F, 0x1DFFF}, + {0x1E007, 0x1E007}, + {0x1E019, 0x1E01A}, + {0x1E022, 0x1E022}, + {0x1E025, 0x1E025}, + {0x1E02B, 0x1E0FF}, + {0x1E12D, 0x1E12F}, + {0x1E13E, 0x1E13F}, + {0x1E14A, 0x1E14D}, + {0x1E150, 0x1E28F}, + {0x1E2AF, 0x1E2BF}, + {0x1E2FA, 0x1E2FE}, + {0x1E300, 0x1E7DF}, + {0x1E7E7, 0x1E7E7}, + {0x1E7EC, 0x1E7EC}, + {0x1E7EF, 0x1E7EF}, + {0x1E7FF, 0x1E7FF}, + {0x1E8C5, 0x1E8C6}, + {0x1E8D7, 0x1E8FF}, + {0x1E94C, 0x1E94F}, + {0x1E95A, 0x1E95D}, + {0x1E960, 0x1EC70}, + {0x1ECB5, 0x1ED00}, + {0x1ED3E, 0x1EDFF}, + {0x1EE04, 0x1EE04}, + {0x1EE20, 0x1EE20}, + {0x1EE23, 0x1EE23}, + {0x1EE25, 0x1EE26}, + {0x1EE28, 0x1EE28}, + {0x1EE33, 0x1EE33}, + {0x1EE38, 0x1EE38}, + {0x1EE3A, 0x1EE3A}, + {0x1EE3C, 0x1EE41}, + {0x1EE43, 0x1EE46}, + {0x1EE48, 0x1EE48}, + {0x1EE4A, 0x1EE4A}, + {0x1EE4C, 0x1EE4C}, + {0x1EE50, 0x1EE50}, + {0x1EE53, 0x1EE53}, + {0x1EE55, 0x1EE56}, + {0x1EE58, 0x1EE58}, + {0x1EE5A, 0x1EE5A}, + {0x1EE5C, 0x1EE5C}, + {0x1EE5E, 0x1EE5E}, + {0x1EE60, 0x1EE60}, + {0x1EE63, 0x1EE63}, + {0x1EE65, 0x1EE66}, + {0x1EE6B, 0x1EE6B}, + {0x1EE73, 0x1EE73}, + {0x1EE78, 0x1EE78}, + {0x1EE7D, 0x1EE7D}, + {0x1EE7F, 0x1EE7F}, + {0x1EE8A, 0x1EE8A}, + {0x1EE9C, 0x1EEA0}, + {0x1EEA4, 0x1EEA4}, + {0x1EEAA, 0x1EEAA}, + {0x1EEBC, 0x1EEEF}, + {0x1EEF2, 0x1EFFF}, + {0x1F02C, 0x1F02F}, + {0x1F094, 0x1F09F}, + {0x1F0AF, 0x1F0B0}, + {0x1F0C0, 0x1F0C0}, + {0x1F0D0, 0x1F0D0}, + {0x1F0F6, 0x1F0FF}, + {0x1F1AE, 0x1F1E5}, + {0x1F203, 0x1F20F}, + {0x1F23C, 0x1F23F}, + {0x1F249, 0x1F24F}, + {0x1F252, 0x1F25F}, + {0x1F266, 0x1F2FF}, + {0x1F6D8, 0x1F6DC}, + {0x1F6ED, 0x1F6EF}, + {0x1F6FD, 0x1F6FF}, + {0x1F774, 0x1F77F}, + {0x1F7D9, 0x1F7DF}, + {0x1F7EC, 0x1F7EF}, + {0x1F7F1, 0x1F7FF}, + {0x1F80C, 0x1F80F}, + {0x1F848, 0x1F84F}, + {0x1F85A, 0x1F85F}, + {0x1F888, 0x1F88F}, + {0x1F8AE, 0x1F8AF}, + {0x1F8B2, 0x1F8FF}, + {0x1FA54, 0x1FA5F}, + {0x1FA6E, 0x1FA6F}, + {0x1FA75, 0x1FA77}, + {0x1FA7D, 0x1FA7F}, + {0x1FA87, 0x1FA8F}, + {0x1FAAD, 0x1FAAF}, + {0x1FABB, 0x1FABF}, + {0x1FAC6, 0x1FACF}, + {0x1FADA, 0x1FADF}, + {0x1FAE8, 0x1FAEF}, + {0x1FAF7, 0x1FAFF}, + {0x1FB93, 0x1FB93}, + {0x1FBCB, 0x1FBEF}, + {0x1FBFA, 0x1FFFD}, + {0x20001, 0x2A6DE}, + {0x2A6E0, 0x2A6FF}, + {0x2A701, 0x2B737}, + {0x2B739, 0x2B73F}, + {0x2B741, 0x2B81C}, + {0x2B81E, 0x2B81F}, + {0x2B821, 0x2CEA0}, + {0x2CEA2, 0x2CEAF}, + {0x2CEB1, 0x2EBDF}, + {0x2EBE1, 0x2F7FF}, + {0x2FA1E, 0x2FFFD}, + {0x30001, 0x31349}, + {0x3134B, 0x3FFFD}, + {0x40000, 0x4FFFD}, + {0x50000, 0x5FFFD}, + {0x60000, 0x6FFFD}, + {0x70000, 0x7FFFD}, + {0x80000, 0x8FFFD}, + {0x90000, 0x9FFFD}, + {0xA0000, 0xAFFFD}, + {0xB0000, 0xBFFFD}, + {0xC0000, 0xCFFFD}, + {0xD0000, 0xDFFFD}, + {0xE0000, 0xE0000}, + {0xE0002, 0xE001F}, + {0xE0080, 0xE00FF}, + {0xE01F0, 0xEFFFD} +}; + +/* Non-characters. */ +static const struct widechar_range widechar_nonchar_table[] = { + {0x0FDD0, 0x0FDEF}, + {0x0FFFE, 0x0FFFF}, + {0x1FFFE, 0x1FFFF}, + {0x2FFFE, 0x2FFFF}, + {0x3FFFE, 0x3FFFF}, + {0x4FFFE, 0x4FFFF}, + {0x5FFFE, 0x5FFFF}, + {0x6FFFE, 0x6FFFF}, + {0x7FFFE, 0x7FFFF}, + {0x8FFFE, 0x8FFFF}, + {0x9FFFE, 0x9FFFF}, + {0xAFFFE, 0xAFFFF}, + {0xBFFFE, 0xBFFFF}, + {0xCFFFE, 0xCFFFF}, + {0xDFFFE, 0xDFFFF}, + {0xEFFFE, 0xEFFFF}, + {0xFFFFE, 0xFFFFF}, + {0x10FFFE, 0x10FFFF} +}; + +/* Characters that were widened from width 1 to 2 in Unicode 9. */ +static const struct widechar_range widechar_widened_table[] = { + {0x0231A, 0x0231B}, + {0x023E9, 0x023EC}, + {0x023F0, 0x023F0}, + {0x023F3, 0x023F3}, + {0x025FD, 0x025FE}, + {0x02614, 0x02615}, + {0x02648, 0x02653}, + {0x0267F, 0x0267F}, + {0x02693, 0x02693}, + {0x026A1, 0x026A1}, + {0x026AA, 0x026AB}, + {0x026BD, 0x026BE}, + {0x026C4, 0x026C5}, + {0x026CE, 0x026CE}, + {0x026D4, 0x026D4}, + {0x026EA, 0x026EA}, + {0x026F2, 0x026F3}, + {0x026F5, 0x026F5}, + {0x026FA, 0x026FA}, + {0x026FD, 0x026FD}, + {0x02705, 0x02705}, + {0x0270A, 0x0270B}, + {0x02728, 0x02728}, + {0x0274C, 0x0274C}, + {0x0274E, 0x0274E}, + {0x02753, 0x02755}, + {0x02757, 0x02757}, + {0x02795, 0x02797}, + {0x027B0, 0x027B0}, + {0x027BF, 0x027BF}, + {0x02B1B, 0x02B1C}, + {0x02B50, 0x02B50}, + {0x02B55, 0x02B55}, + {0x1F004, 0x1F004}, + {0x1F0CF, 0x1F0CF}, + {0x1F18E, 0x1F18E}, + {0x1F191, 0x1F19A}, + {0x1F201, 0x1F201}, + {0x1F21A, 0x1F21A}, + {0x1F22F, 0x1F22F}, + {0x1F232, 0x1F236}, + {0x1F238, 0x1F23A}, + {0x1F250, 0x1F251}, + {0x1F300, 0x1F320}, + {0x1F32D, 0x1F335}, + {0x1F337, 0x1F37C}, + {0x1F37E, 0x1F393}, + {0x1F3A0, 0x1F3CA}, + {0x1F3CF, 0x1F3D3}, + {0x1F3E0, 0x1F3F0}, + {0x1F3F4, 0x1F3F4}, + {0x1F3F8, 0x1F43E}, + {0x1F440, 0x1F440}, + {0x1F442, 0x1F4FC}, + {0x1F4FF, 0x1F53D}, + {0x1F54B, 0x1F54E}, + {0x1F550, 0x1F567}, + {0x1F595, 0x1F596}, + {0x1F5FB, 0x1F64F}, + {0x1F680, 0x1F6C5}, + {0x1F6CC, 0x1F6CC}, + {0x1F6D0, 0x1F6D0}, + {0x1F6EB, 0x1F6EC}, + {0x1F910, 0x1F918}, + {0x1F980, 0x1F984}, + {0x1F9C0, 0x1F9C0} +}; + +template +bool widechar_in_table(const Collection &arr, uint32_t c) { + auto where = std::lower_bound(std::begin(arr), std::end(arr), c, + [](widechar_range p, uint32_t c) { return p.hi < c; }); + return where != std::end(arr) && where->lo <= c; +} + +/* Return the width of character c, or a special negative value. */ +int wcwidth(uint32_t c) { + if (widechar_in_table(widechar_ascii_table, c)) + return 1; + if (widechar_in_table(widechar_private_table, c)) + return widechar_private_use; + if (widechar_in_table(widechar_nonprint_table, c)) + return widechar_nonprint; + if (widechar_in_table(widechar_nonchar_table, c)) + return widechar_non_character; + if (widechar_in_table(widechar_combining_table, c)) + return widechar_combining; + if (widechar_in_table(widechar_combiningletters_table, c)) + return widechar_combining; + if (widechar_in_table(widechar_doublewide_table, c)) + return 2; + if (widechar_in_table(widechar_ambiguous_table, c)) + return widechar_ambiguous; + if (widechar_in_table(widechar_unassigned_table, c)) + return widechar_unassigned; + if (widechar_in_table(widechar_widened_table, c)) + return widechar_widened_in_9; + return 1; +} + +} // namespace +#endif // WIDECHAR_WIDTH_H diff --git a/snap/gui/btop.desktop b/snap/gui/btop.desktop new file mode 100644 index 0000000..0135a93 --- /dev/null +++ b/snap/gui/btop.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=btop++ +GenericName=System Monitor +Comment=Resource monitor that shows usage and stats for processor, memory, disks, network and processes +Icon=${SNAP}/meta/gui/icon.svg +Exec=btop +Terminal=true +Categories=System;Monitor;ConsoleOnly; +Keywords=system;process;task diff --git a/snap/gui/icon.svg b/snap/gui/icon.svg new file mode 100644 index 0000000..402b842 --- /dev/null +++ b/snap/gui/icon.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 860b103..e000e9a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -4,7 +4,6 @@ summary: Resource monitor that shows usage and stats description: | Resource monitor that shows usage and stats for processor, memory, disks, network and processes. C++ version and continuation of bashtop and bpytop. - license: Apache-2.0 base: core20 @@ -26,6 +25,8 @@ package-repositories: apps: btop: command: usr/local/bin/btop + extensions: + - gnome-3-38 environment: LC_ALL: C.UTF-8 LANG: C.UTF-8 @@ -38,6 +39,11 @@ apps: - network-observe - home - removable-media + - desktop + - desktop-legacy + - x11 + - wayland + - unity7 parts: btop: diff --git a/src/btop.cpp b/src/btop.cpp index af2ffcf..484a0df 100644 --- a/src/btop.cpp +++ b/src/btop.cpp @@ -17,10 +17,12 @@ tab-size = 4 */ #include +#include #include +#ifdef __FreeBSD__ + #include +#endif #include -#include -#include #include #include #include @@ -30,6 +32,11 @@ tab-size = 4 #include #include #include +#ifdef __APPLE__ + #include + #include + #include +#endif #include #include @@ -40,7 +47,7 @@ tab-size = 4 #include using std::string, std::string_view, std::vector, std::atomic, std::endl, std::cout, std::min, std::flush, std::endl; -using std::string_literals::operator""s, std::to_string, std::future, std::async, std::bitset, std::future_status; +using std::string_literals::operator""s, std::to_string; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; @@ -55,7 +62,7 @@ namespace Global { {"#801414", "██████╔╝ ██║ ╚██████╔╝██║ ╚═╝ ╚═╝"}, {"#000000", "╚═════╝ ╚═╝ ╚═════╝ ╚═╝"}, }; - const string Version = "1.0.9"; + const string Version = "1.2.8"; int coreCount; string overlay; @@ -66,6 +73,7 @@ namespace Global { string fg_green = "\x1b[1;92m"; string fg_red = "\x1b[0;91m"; + uid_t real_uid, set_uid; fs::path self_path; @@ -79,8 +87,9 @@ namespace Global { uint64_t start_time; atomic resized (false); - atomic resizing (false); atomic quitting (false); + atomic should_quit (false); + atomic should_sleep (false); atomic _runner_started (false); bool arg_tty = false; @@ -88,7 +97,6 @@ namespace Global { int arg_preset = -1; } - //* A simple argument parser void argumentParser(const int& argc, char **argv) { for(int i = 1; i < argc; i++) { @@ -150,9 +158,14 @@ void argumentParser(const int& argc, char **argv) { //* Handler for SIGWINCH and general resizing events, does nothing if terminal hasn't been resized unless force=true void term_resize(bool force) { - if (Global::resizing) return; - atomic_lock lck(Global::resizing); - if (auto refreshed = Term::refresh(); refreshed or force) { + static atomic resizing (false); + if (Input::polling) { + Global::resized = true; + Input::interrupt = true; + return; + } + atomic_lock lck(resizing, true); + if (auto refreshed = Term::refresh(true); refreshed or force) { if (force and refreshed) force = false; } else return; @@ -160,6 +173,7 @@ void term_resize(bool force) { static const array all_boxes = {"cpu", "mem", "net", "proc"}; Global::resized = true; if (Runner::active) Runner::stop(); + Term::refresh(); Config::unlock(); auto boxes = Config::getS("shown_boxes"); @@ -175,11 +189,12 @@ void term_resize(bool force) { << Mv::to((Term::height / 2) + 1, (Term::width / 2) - 12) << Global::fg_white << "Needed for current config:" << Mv::to((Term::height / 2) + 2, (Term::width / 2) - 10) << "Width = " << min_size.at(0) << " Height = " << min_size.at(1) << flush; - while (not Term::refresh() and not Input::poll()) sleep_ms(10); - if (Input::poll()) { + bool got_key = false; + for (; not Term::refresh() and not got_key; got_key = Input::poll(10)); + if (got_key) { auto key = Input::get(); if (key == "q") - exit(0); + clean_quit(0); else if (is_in(key, "1", "2", "3", "4")) { Config::current_preset = -1; Config::toggle_box(all_boxes.at(std::stoi(key) - 1)); @@ -199,19 +214,26 @@ void clean_quit(int sig) { if (Global::quitting) return; Global::quitting = true; Runner::stop(); - if (Global::_runner_started and pthread_join(Runner::runner_id, NULL) != 0) { - Logger::error("Failed to join _runner thread!"); + if (Global::_runner_started) { + #ifdef __APPLE__ + if (pthread_join(Runner::runner_id, NULL) != 0) { + Logger::warning("Failed to join _runner thread on exit!"); + pthread_cancel(Runner::runner_id); + } + #else + struct timespec ts; + ts.tv_sec = 5; + if (pthread_timedjoin_np(Runner::runner_id, NULL, &ts) != 0) { + Logger::warning("Failed to join _runner thread on exit!"); + pthread_cancel(Runner::runner_id); + } + #endif } Config::write(); - Input::clear(); - - //? Wait for any remaining Tools::atomic_lock destructors to finish for max 1000ms - for (int i = 0; Tools::active_locks > 0 and i < 100; i++) { - sleep_ms(10); - } if (Term::initialized) { + Input::clear(); Term::restore(); } @@ -222,12 +244,13 @@ void clean_quit(int sig) { } Logger::info("Quitting! Runtime: " + sec_to_dhms(time_s() - Global::start_time)); - //? Assume error if still not cleaned up and call quick_exit to avoid a segfault from Tools::atomic_lock destructor - if (Tools::active_locks > 0) { - quick_exit((sig != -1 ? sig : 0)); - } + const auto excode = (sig != -1 ? sig : 0); - if (sig != -1) exit(sig); +#ifdef __APPLE__ + _Exit(excode); +#else + quick_exit(excode); +#endif } //* Handler for SIGTSTP; stops threads, restores terminal and sends SIGSTOP @@ -250,10 +273,24 @@ void _exit_handler() { void _signal_handler(const int sig) { switch (sig) { case SIGINT: - clean_quit(0); + if (Runner::active) { + Global::should_quit = true; + Runner::stopping = true; + Input::interrupt = true; + } + else { + clean_quit(0); + } break; case SIGTSTP: - _sleep(); + if (Runner::active) { + Global::should_sleep = true; + Runner::stopping = true; + Input::interrupt = true; + } + else { + _sleep(); + } break; case SIGCONT: _resume(); @@ -291,10 +328,22 @@ namespace Runner { pthread_mutex_t& pt_mutex; public: int status; - thread_lock(pthread_mutex_t& mtx) : pt_mutex(mtx) { status = pthread_mutex_lock(&pt_mutex); } + thread_lock(pthread_mutex_t& mtx) : pt_mutex(mtx) { pthread_mutex_init(&pt_mutex, NULL); status = pthread_mutex_lock(&pt_mutex); } ~thread_lock() { if (status == 0) pthread_mutex_unlock(&pt_mutex); } }; + //* Wrapper for raising priviliges when using SUID bit + class gain_priv { + int status = -1; + public: + gain_priv() { + if (Global::real_uid != Global::set_uid) this->status = seteuid(Global::set_uid); + } + ~gain_priv() { + if (status == 0) status = seteuid(Global::real_uid); + } + }; + string output; string empty_bg; bool pause_output = false; @@ -302,20 +351,6 @@ namespace Runner { pthread_t runner_id; pthread_mutex_t mtx; - const unordered_flat_map box_bits = { - {"proc", 0b0000'0001}, - {"net", 0b0000'0100}, - {"mem", 0b0001'0000}, - {"cpu", 0b0100'0000}, - }; - - enum bit_pos { - proc_present, proc_running, - net_present, net_running, - mem_present, mem_running, - cpu_present, cpu_running - }; - enum debug_actions { collect_begin, draw_begin, @@ -327,16 +362,11 @@ namespace Runner { draw }; - const uint_fast8_t proc_done = 0b0000'0011; - const uint_fast8_t net_done = 0b0000'1100; - const uint_fast8_t mem_done = 0b0011'0000; - const uint_fast8_t cpu_done = 0b1100'0000; - string debug_bg; unordered_flat_map> debug_times; struct runner_conf { - bitset<8> box_mask; + vector boxes; bool no_update; bool force_redraw; bool background_update; @@ -366,10 +396,10 @@ namespace Runner { //? ------------------------------- Secondary thread: async launcher and drawing ---------------------------------- void * _runner(void * _) { (void)_; - //? Block all signals in this thread to avoid deadlock from any signal handlers trying to stop this thread + //? Block some signals in this thread to avoid deadlock from any signal handlers trying to stop this thread sigemptyset(&mask); - sigaddset(&mask, SIGINT); - sigaddset(&mask, SIGTSTP); + // sigaddset(&mask, SIGINT); + // sigaddset(&mask, SIGTSTP); sigaddset(&mask, SIGWINCH); sigaddset(&mask, SIGTERM); pthread_sigmask(SIG_BLOCK, &mask, NULL); @@ -386,13 +416,24 @@ namespace Runner { //* ----------------------------------------------- THREAD LOOP ----------------------------------------------- while (not Global::quitting) { thread_wait(); + atomic_wait_for(active, true, 5000); + if (active) { + Global::exit_error_msg = "Runner thread failed to get active lock!"; + Global::thread_exception = true; + Input::interrupt = true; + stopping = true; + } if (stopping or Global::resized) { + sleep_ms(1); continue; } //? Atomic lock used for blocking non thread-safe actions in main thread atomic_lock lck(active); + //? Set effective user if SUID bit is set + gain_priv powers{}; + auto& conf = current_conf; //! DEBUG stats @@ -404,128 +445,85 @@ namespace Runner { output.clear(); - //* Start collection functions for all boxes in async threads and draw in this thread when finished - //? Starting order below based on mean time to finish + //* Run collection and draw functions for all boxes try { - future cpu; - future mem; - future net; - future&> proc; + //? CPU + if (v_contains(conf.boxes, "cpu")) { + try { + if (Global::debug) debug_timer("cpu", collect_begin); - //? Loop until all box flags present in bitmask have been zeroed - while (conf.box_mask.count() > 0) { - if (stopping) break; + //? Start collect + auto cpu = Cpu::collect(conf.no_update); - //? PROC - if (conf.box_mask.test(proc_present)) { - if (not conf.box_mask.test(proc_running)) { - if (Global::debug) debug_timer("proc", collect_begin); + if (Global::debug) debug_timer("cpu", draw_begin); - //? Start async collect - proc = async(Proc::collect, conf.no_update); - conf.box_mask.set(proc_running); - } - else if (not proc.valid()) - throw std::runtime_error("Proc::collect() future not valid."); + //? Draw box + if (not pause_output) output += Cpu::draw(cpu, conf.force_redraw, conf.no_update); - else if (proc.wait_for(10us) == future_status::ready) { - try { - if (Global::debug) debug_timer("proc", draw_begin); - - //? Draw box - if (not pause_output) output += Proc::draw(proc.get(), conf.force_redraw, conf.no_update); - - if (Global::debug) debug_timer("proc", draw_done); - } - catch (const std::exception& e) { - throw std::runtime_error("Proc:: -> " + (string)e.what()); - } - conf.box_mask ^= proc_done; - } + if (Global::debug) debug_timer("cpu", draw_done); } - - //? NET - if (conf.box_mask.test(net_present)) { - if (not conf.box_mask.test(net_running)) { - if (Global::debug) debug_timer("net", collect_begin); - - //? Start async collect - net = async(Net::collect, conf.no_update); - conf.box_mask.set(net_running); - } - else if (not net.valid()) - throw std::runtime_error("Net::collect() future not valid."); - - else if (net.wait_for(10us) == future_status::ready) { - try { - if (Global::debug) debug_timer("net", draw_begin); - - //? Draw box - if (not pause_output) output += Net::draw(net.get(), conf.force_redraw, conf.no_update); - - if (Global::debug) debug_timer("net", draw_done); - } - catch (const std::exception& e) { - throw std::runtime_error("Net:: -> " + (string)e.what()); - } - conf.box_mask ^= net_done; - } + catch (const std::exception& e) { + throw std::runtime_error("Cpu:: -> " + (string)e.what()); } + } - //? MEM - if (conf.box_mask.test(mem_present)) { - if (not conf.box_mask.test(mem_running)) { - if (Global::debug) debug_timer("mem", collect_begin); + //? MEM + if (v_contains(conf.boxes, "mem")) { + try { + if (Global::debug) debug_timer("mem", collect_begin); - //? Start async collect - mem = async(Mem::collect, conf.no_update); - conf.box_mask.set(mem_running); - } - else if (not mem.valid()) - throw std::runtime_error("Mem::collect() future not valid."); + //? Start collect + auto mem = Mem::collect(conf.no_update); - else if (mem.wait_for(10us) == future_status::ready) { - try { - if (Global::debug) debug_timer("mem", draw_begin); + if (Global::debug) debug_timer("mem", draw_begin); - //? Draw box - if (not pause_output) output += Mem::draw(mem.get(), conf.force_redraw, conf.no_update); + //? Draw box + if (not pause_output) output += Mem::draw(mem, conf.force_redraw, conf.no_update); - if (Global::debug) debug_timer("mem", draw_done); - } - catch (const std::exception& e) { - throw std::runtime_error("Mem:: -> " + (string)e.what()); - } - conf.box_mask ^= mem_done; - } + if (Global::debug) debug_timer("mem", draw_done); } + catch (const std::exception& e) { + throw std::runtime_error("Mem:: -> " + (string)e.what()); + } + } - //? CPU - if (conf.box_mask.test(cpu_present)) { - if (not conf.box_mask.test(cpu_running)) { - if (Global::debug) debug_timer("cpu", collect_begin); + //? NET + if (v_contains(conf.boxes, "net")) { + try { + if (Global::debug) debug_timer("net", collect_begin); - //? Start async collect - cpu = async(Cpu::collect, conf.no_update); - conf.box_mask.set(cpu_running); - } - else if (not cpu.valid()) - throw std::runtime_error("Cpu::collect() future not valid."); + //? Start collect + auto net = Net::collect(conf.no_update); - else if (cpu.wait_for(10us) == future_status::ready) { - try { - if (Global::debug) debug_timer("cpu", draw_begin); + if (Global::debug) debug_timer("net", draw_begin); - //? Draw box - if (not pause_output) output += Cpu::draw(cpu.get(), conf.force_redraw, conf.no_update); + //? Draw box + if (not pause_output) output += Net::draw(net, conf.force_redraw, conf.no_update); - if (Global::debug) debug_timer("cpu", draw_done); - } - catch (const std::exception& e) { - throw std::runtime_error("Cpu:: -> " + (string)e.what()); - } - conf.box_mask ^= cpu_done; - } + if (Global::debug) debug_timer("net", draw_done); + } + catch (const std::exception& e) { + throw std::runtime_error("Net:: -> " + (string)e.what()); + } + } + + //? PROC + if (v_contains(conf.boxes, "proc")) { + try { + if (Global::debug) debug_timer("proc", collect_begin); + + //? Start collect + auto proc = Proc::collect(conf.no_update); + + if (Global::debug) debug_timer("proc", draw_begin); + + //? Draw box + if (not pause_output) output += Proc::draw(proc, conf.force_redraw, conf.no_update); + + if (Global::debug) debug_timer("proc", draw_done); + } + catch (const std::exception& e) { + throw std::runtime_error("Proc:: -> " + (string)e.what()); } } } @@ -581,15 +579,23 @@ namespace Runner { << Term::sync_end << flush; } //* ----------------------------------------------- THREAD LOOP ----------------------------------------------- - pthread_exit(NULL); } //? ------------------------------------------ Secondary thread end ----------------------------------------------- //* Runs collect and draw in a secondary thread, unlocks and locks config to update cached values void run(const string& box, const bool no_update, const bool force_redraw) { - atomic_lock lck(waiting); - atomic_wait(active); + atomic_wait_for(active, true, 5000); + if (active) { + Logger::error("Stall in Runner thread, restarting!"); + active = false; + // exit(1); + pthread_cancel(Runner::runner_id); + if (pthread_create(&Runner::runner_id, NULL, &Runner::_runner, NULL) != 0) { + Global::exit_error_msg = "Failed to re-create _runner thread!"; + clean_quit(1); + } + } if (stopping or Global::resized) return; if (box == "overlay") { @@ -602,21 +608,21 @@ namespace Runner { Config::unlock(); Config::lock(); - //? Setup bitmask for selected boxes and pass to _runner thread - bitset<8> box_mask; - for (const auto& box : (box == "all" ? Config::current_boxes : vector{box})) { - box_mask |= box_bits.at(box); - } - - current_conf = {box_mask, no_update, force_redraw, (not Config::getB("tty_mode") and Config::getB("background_update")), Global::overlay, Global::clock}; + current_conf = { + (box == "all" ? Config::current_boxes : vector{box}), + no_update, force_redraw, + (not Config::getB("tty_mode") and Config::getB("background_update")), + Global::overlay, + Global::clock + }; if (Menu::active and not current_conf.background_update) Global::overlay.clear(); thread_trigger(); - - //? Wait for _runner thread to be active before returning - for (int i = 0; not active and i < 10; i++) sleep_ms(1); + atomic_wait_for(active, false, 10); } + + } //* Stops any work being done in runner thread and checks for thread errors @@ -626,12 +632,23 @@ namespace Runner { if (ret != EBUSY and not Global::quitting) { if (active) active = false; Global::exit_error_msg = "Runner thread died unexpectedly!"; - exit(1); + clean_quit(1); } else if (ret == EBUSY) { - atomic_wait(active); + atomic_wait_for(active, true, 5000); + if (active) { + active = false; + if (Global::quitting) { + return; + } + else { + Global::exit_error_msg = "No response from Runner thread, quitting!"; + clean_quit(1); + } + } thread_trigger(); - sleep_ms(1); + atomic_wait_for(active, false, 100); + atomic_wait_for(active, true, 100); } stopping = false; } @@ -646,16 +663,20 @@ int main(int argc, char **argv) { Global::start_time = time_s(); + //? Save real and effective userid's and drop priviliges until needed if running with SUID bit set + Global::real_uid = getuid(); + Global::set_uid = geteuid(); + if (Global::real_uid != Global::set_uid) { + if (seteuid(Global::real_uid) != 0) { + Global::real_uid = Global::set_uid; + Global::exit_error_msg = "Failed to change effective user ID. Unset btop SUID bit to ensure security on this system. Quitting!"; + clean_quit(1); + } + } + //? Call argument parser if launched with arguments if (argc > 1) argumentParser(argc, argv); - //? Setup signal handlers for CTRL-C, CTRL-Z, resume and terminal resize - std::atexit(_exit_handler); - std::signal(SIGINT, _signal_handler); - std::signal(SIGTSTP, _signal_handler); - std::signal(SIGCONT, _signal_handler); - std::signal(SIGWINCH, _signal_handler); - //? Setup paths for config, log and user themes for (const auto& env : {"XDG_CONFIG_HOME", "HOME"}) { if (std::getenv(env) != NULL and access(std::getenv(env), W_OK) != -1) { @@ -680,10 +701,17 @@ int main(int argc, char **argv) { } } //? Try to find global btop theme path relative to binary path -#if defined(__linux__) +#ifdef __linux__ { std::error_code ec; Global::self_path = fs::read_symlink("/proc/self/exe", ec).remove_filename(); } +#elif __APPLE__ + { + char buf [PATH_MAX]; + uint32_t bufsize = PATH_MAX; + if(!_NSGetExecutablePath(buf, &bufsize)) + Global::self_path = fs::path(buf).remove_filename(); + } #endif if (std::error_code ec; not Global::self_path.empty()) { Theme::theme_dir = fs::canonical(Global::self_path / "../share/btop/themes", ec); @@ -718,44 +746,76 @@ int main(int argc, char **argv) { } //? Try to find and set a UTF-8 locale - if (bool found = false; std::setlocale(LC_ALL, NULL) == NULL or not str_to_upper(s_replace((string)std::setlocale(LC_ALL, NULL), "-", "")).ends_with("UTF8")) { - if (std::getenv("LANG") != NULL and str_to_upper(s_replace((string)std::getenv("LANG"), "-", "")).ends_with("UTF8")) { - if (std::setlocale(LC_ALL, std::getenv("LANG")) != NULL) { - found = true; + if (std::setlocale(LC_ALL, "") != NULL and not s_contains((string)std::setlocale(LC_ALL, ""), ";") + and str_to_upper(s_replace((string)std::setlocale(LC_ALL, ""), "-", "")).ends_with("UTF8")) { + Logger::debug("Using locale " + (string)std::setlocale(LC_ALL, "")); + } + else { + string found; + bool set_failure = false; + for (const auto loc_env : array{"LANG", "LC_ALL"}) { + if (std::getenv(loc_env) != NULL and str_to_upper(s_replace((string)std::getenv(loc_env), "-", "")).ends_with("UTF8")) { + found = std::getenv(loc_env); + if (std::setlocale(LC_ALL, found.c_str()) == NULL) { + set_failure = true; + Logger::warning("Failed to set locale " + found + " continuing anyway."); + } } } - else { - if (setenv("LANG", "", 1) == 0) { + if (found.empty()) { + if (setenv("LC_ALL", "", 1) == 0 and setenv("LANG", "", 1) == 0) { try { if (const auto loc = std::locale("").name(); not loc.empty() and loc != "*") { for (auto& l : ssplit(loc, ';')) { if (str_to_upper(s_replace(l, "-", "")).ends_with("UTF8")) { - if (std::setlocale(LC_ALL, l.substr(l.find('=') + 1).c_str()) != NULL) { - found = true; + found = l.substr(l.find('=') + 1); + if (std::setlocale(LC_ALL, found.c_str()) != NULL) { + break; } - break; } } } } - catch (...) { found = false; } + catch (...) { found.clear(); } } } - if (not found and Global::utf_force) - Logger::warning("No UTF-8 locale detected! Forcing start with --utf-force argument."); - else if (not found) { - Global::exit_error_msg = "No UTF-8 locale detected!\nUse --utf-force argument to force start if you're sure your terminal can handle it."; - exit(1); + #ifdef __APPLE__ + if (found.empty()) { + CFLocaleRef cflocale = CFLocaleCopyCurrent(); + CFStringRef id_value = (CFStringRef)CFLocaleGetValue(cflocale, kCFLocaleIdentifier); + auto loc_id = CFStringGetCStringPtr(id_value, kCFStringEncodingUTF8); + CFRelease(cflocale); + std::string cur_locale = (loc_id != nullptr ? loc_id : ""); + if (cur_locale.empty()) { + Logger::warning("No UTF-8 locale detected! Some symbols might not display correctly."); + } + else if (std::setlocale(LC_ALL, string(cur_locale + ".UTF-8").c_str()) != NULL) { + Logger::debug("Setting LC_ALL=" + cur_locale + ".UTF-8"); + } + else if(std::setlocale(LC_ALL, "en_US.UTF-8") != NULL) { + Logger::debug("Setting LC_ALL=en_US.UTF-8"); + } + else { + Logger::warning("Failed to set macos locale, continuing anyway."); + } } - else - Logger::debug("Setting LC_ALL=" + (string)std::setlocale(LC_ALL, NULL)); + #else + if (found.empty() and Global::utf_force) + Logger::warning("No UTF-8 locale detected! Forcing start with --utf-force argument."); + else if (found.empty()) { + Global::exit_error_msg = "No UTF-8 locale detected!\nUse --utf-force argument to force start if you're sure your terminal can handle it."; + clean_quit(1); + } + #endif + else if (not set_failure) + Logger::debug("Setting LC_ALL=" + found); } //? Initialize terminal and set options if (not Term::init()) { Global::exit_error_msg = "No tty detected!\nbtop++ needs an interactive shell to run."; - exit(1); + clean_quit(1); } if (Term::current_tty != "unknown") Logger::info("Running on " + Term::current_tty); @@ -763,10 +823,25 @@ int main(int argc, char **argv) { Config::set("tty_mode", true); Logger::info("Forcing tty mode: setting 16 color mode and using tty friendly graph symbols"); } +#ifndef __APPLE__ else if (not Global::arg_tty and Term::current_tty.starts_with("/dev/tty")) { Config::set("tty_mode", true); Logger::info("Real tty detected: setting 16 color mode and using tty friendly graph symbols"); } +#endif + + //? Check for valid terminal dimensions + { + int t_count = 0; + while (Term::width <= 0 or Term::width > 10000 or Term::height <= 0 or Term::height > 10000) { + sleep_ms(10); + Term::refresh(); + if (++t_count == 100) { + Global::exit_error_msg = "Failed to get size of terminal!"; + clean_quit(1); + } + } + } //? Platform dependent init and error check try { @@ -774,18 +849,25 @@ int main(int argc, char **argv) { } catch (const std::exception& e) { Global::exit_error_msg = "Exception in Shared::init() -> " + (string)e.what(); - exit(1); + clean_quit(1); } //? Update list of available themes and generate the selected theme Theme::updateThemes(); Theme::setTheme(); + //? Setup signal handlers for CTRL-C, CTRL-Z, resume and terminal resize + std::atexit(_exit_handler); + std::signal(SIGINT, _signal_handler); + std::signal(SIGTSTP, _signal_handler); + std::signal(SIGCONT, _signal_handler); + std::signal(SIGWINCH, _signal_handler); + //? Start runner thread Runner::thread_sem_init(); if (pthread_create(&Runner::runner_id, NULL, &Runner::_runner, NULL) != 0) { Global::exit_error_msg = "Failed to create _runner thread!"; - exit(1); + clean_quit(1); } else { Global::_runner_started = true; @@ -808,8 +890,9 @@ int main(int argc, char **argv) { } - //? Print out box outlines Draw::calcSizes(); + + //? Print out box outlines cout << Term::sync_start << Cpu::box << Mem::box << Net::box << Proc::box << Term::sync_end << flush; @@ -821,10 +904,12 @@ int main(int argc, char **argv) { try { while (not true not_eq not false) { //? Check for exceptions in secondary thread and exit with fail signal if true - if (Global::thread_exception) exit(1); + if (Global::thread_exception) clean_quit(1); + else if (Global::should_quit) clean_quit(0); + else if (Global::should_sleep) { Global::should_sleep = false; _sleep(); } //? Make sure terminal size hasn't changed (in case of SIGWINCH not working properly) - term_resize(); + term_resize(Global::resized); //? Trigger secondary thread to redraw if terminal has been resized if (Global::resized) { @@ -833,7 +918,7 @@ int main(int argc, char **argv) { Global::resized = false; if (Menu::active) Menu::process(); else Runner::run("all", true, true); - atomic_wait(Runner::active); + atomic_wait_for(Runner::active, true, 1000); } //? Update clock if needed @@ -876,7 +961,7 @@ int main(int argc, char **argv) { } catch (const std::exception& e) { Global::exit_error_msg = "Exception in main loop -> " + (string)e.what(); - exit(1); + clean_quit(1); } } diff --git a/src/btop_config.cpp b/src/btop_config.cpp index 2a1bbc0..479101a 100644 --- a/src/btop_config.cpp +++ b/src/btop_config.cpp @@ -50,10 +50,13 @@ namespace Config { "#* Will force 16-color mode and TTY theme, set all graph symbols to \"tty\" and swap out other non tty friendly symbols."}, {"presets", "#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.\n" - "#* Format: \"box_name:P:G,box_name:P:G\" P=(0 or 1) for alternate positons, G=graph symbol to use for box.\n" - "#* Use withespace \" \" as seprator between different presets.\n" + "#* Format: \"box_name:P:G,box_name:P:G\" P=(0 or 1) for alternate positions, G=graph symbol to use for box.\n" + "#* Use withespace \" \" as separator between different presets.\n" "#* Example: \"cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty\""}, + {"vim_keys", "#* Set to True to enable \"h,j,k,l,g,G\" keys for directional control in lists.\n" + "#* Conflicting keys for h:\"help\" and k:\"kill\" is accessible while holding shift."}, + {"rounded_corners", "#* Rounded corners on boxes, is ignored if TTY mode is ON."}, {"graph_symbol", "#* Default symbols to use for graph creation, \"braille\", \"block\" or \"tty\".\n" @@ -74,8 +77,8 @@ namespace Config { {"update_ms", "#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs."}, - {"proc_sorting", "#* Processes sorting, \"pid\" \"program\" \"arguments\" \"threads\" \"user\" \"memory\" \"cpu lazy\" \"cpu responsive\",\n" - "#* \"cpu lazy\" sorts top process over time (easier to follow), \"cpu responsive\" updates top process directly."}, + {"proc_sorting", "#* Processes sorting, \"pid\" \"program\" \"arguments\" \"threads\" \"user\" \"memory\" \"cpu lazy\" \"cpu direct\",\n" + "#* \"cpu lazy\" sorts top process over time (easier to follow), \"cpu direct\" updates top process directly."}, {"proc_reversed", "#* Reverse sorting order, True or False."}, @@ -89,10 +92,14 @@ namespace Config { {"proc_mem_bytes", "#* Show process memory as bytes instead of percent."}, + {"proc_cpu_graphs", "#* Show cpu graph for each process."}, + {"proc_info_smaps", "#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate)"}, {"proc_left", "#* Show proc box on left side of screen instead of right."}, + {"proc_filter_kernel", "#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop)."}, + {"cpu_graph_upper", "#* Sets the CPU stat shown in upper half of the CPU graph, \"total\" is always available.\n" "#* Select from a list of detected attributes from the options menu."}, @@ -120,6 +127,8 @@ namespace Config { {"temp_scale", "#* Which temperature scale to use, available values: \"celsius\", \"fahrenheit\", \"kelvin\" and \"rankine\"."}, + {"base_10_sizes", "#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024."}, + {"show_cpu_freq", "#* Show CPU frequency."}, {"clock_format", "#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.\n" @@ -136,6 +145,8 @@ namespace Config { {"mem_below_net", "#* Show mem box below net box instead of above."}, + {"zfs_arc_cached", "#* Count ZFS ARC in cached and available memory."}, + {"show_swap", "#* If swap memory should be shown in memory box."}, {"swap_disk", "#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk."}, @@ -146,6 +157,10 @@ namespace Config { {"use_fstab", "#* Read disks list from /etc/fstab. This also disables only_physical."}, + {"zfs_hide_datasets", "#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool)"}, + + {"disk_free_priv", "#* Set to true to show available disk space for privileged users."}, + {"show_io_stat", "#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view."}, {"io_mode", "#* Toggles io mode for disks, showing big graphs for disk read/write speeds."}, @@ -167,6 +182,8 @@ namespace Config { {"show_battery", "#* Show battery stats in top right if battery is present."}, + {"selected_battery", "#* Which battery to use if multiple are present. \"Auto\" for auto detection."}, + {"log_level", "#* Set loglevel for \"~/.config/btop/btop.log\" levels are: \"ERROR\" \"WARNING\" \"INFO\" \"DEBUG\".\n" "#* The level set includes all lower levels, i.e. \"DEBUG\" will show all logging info."} }; @@ -184,6 +201,7 @@ namespace Config { {"cpu_graph_upper", "total"}, {"cpu_graph_lower", "total"}, {"cpu_sensor", "Auto"}, + {"selected_battery", "Auto"}, {"cpu_core_map", ""}, {"temp_scale", "celsius"}, {"clock_format", "%X"}, @@ -206,10 +224,12 @@ namespace Config { {"proc_tree", false}, {"proc_colors", true}, {"proc_gradient", true}, - {"proc_per_core", true}, + {"proc_per_core", false}, {"proc_mem_bytes", true}, + {"proc_cpu_graphs", true}, {"proc_info_smaps", false}, {"proc_left", false}, + {"proc_filter_kernel", false}, {"cpu_invert_lower", true}, {"cpu_single_graph", false}, {"cpu_bottom", false}, @@ -220,18 +240,23 @@ namespace Config { {"background_update", true}, {"mem_graphs", true}, {"mem_below_net", false}, + {"zfs_arc_cached", true}, {"show_swap", true}, {"swap_disk", true}, {"show_disks", true}, {"only_physical", true}, {"use_fstab", true}, + {"zfs_hide_datasets", false}, {"show_io_stat", true}, {"io_mode", false}, + {"base_10_sizes", false}, {"io_graph_combined", false}, {"net_auto", true}, - {"net_sync", false}, + {"net_sync", true}, {"show_battery", true}, + {"vim_keys", false}, {"tty_mode", false}, + {"disk_free_priv", false}, {"force_tty", false}, {"lowcolor", false}, {"show_detailed", false}, @@ -245,6 +270,7 @@ namespace Config { {"net_upload", 100}, {"detailed_pid", 0}, {"selected_pid", 0}, + {"selected_depth", 0}, {"proc_start", 0}, {"proc_selected", 0}, {"proc_last_selected", 0}, @@ -252,7 +278,7 @@ namespace Config { unordered_flat_map intsTmp; bool _locked(const string& name) { - atomic_wait(writelock); + atomic_wait(writelock, true); if (not write_new and rng::find_if(descriptions, [&name](const auto& a) { return a.at(0) == name; }) != descriptions.end()) write_new = true; return locked.load(); @@ -261,6 +287,8 @@ namespace Config { fs::path conf_dir; fs::path conf_file; + vector available_batteries = {"Auto"}; + vector current_boxes; vector preset_list = {"cpu:0:default,mem:0:default,net:0:default,proc:0:default"}; int current_preset = -1; @@ -444,13 +472,14 @@ namespace Config { void unlock() { if (not locked) return; atomic_wait(Runner::active); - atomic_lock lck(writelock); + atomic_lock lck(writelock, true); try { if (Proc::shown) { ints.at("selected_pid") = Proc::selected_pid; strings.at("selected_name") = Proc::selected_name; ints.at("proc_start") = Proc::start; ints.at("proc_selected") = Proc::selected; + ints.at("selected_depth") = Proc::selected_depth; } for (auto& item : stringsTmp) { @@ -470,7 +499,7 @@ namespace Config { } catch (const std::exception& e) { Global::exit_error_msg = "Exception during Config::unlock() : " + (string)e.what(); - exit(1); + clean_quit(1); } locked = false; @@ -521,9 +550,7 @@ namespace Config { vector valid_names; for (auto &n : descriptions) valid_names.push_back(n[0]); - string v_string; - getline(cread, v_string, '\n'); - if (not s_contains(v_string, Global::Version)) + if (string v_string; cread.peek() != '#' or (getline(cread, v_string, '\n') and not s_contains(v_string, Global::Version))) write_new = true; while (not cread.eof()) { cread >> std::ws; @@ -580,6 +607,7 @@ namespace Config { void write() { if (conf_file.empty() or not write_new) return; Logger::debug("Writing new config file"); + if (geteuid() != Global::real_uid and seteuid(Global::real_uid) != 0) return; std::ofstream cwrite(conf_file, std::ios::trunc); if (cwrite.good()) { cwrite << "#? Config file for btop v. " << Global::Version; @@ -595,4 +623,4 @@ namespace Config { } } } -} \ No newline at end of file +} diff --git a/src/btop_config.hpp b/src/btop_config.hpp index fd08d92..9d2f63c 100644 --- a/src/btop_config.hpp +++ b/src/btop_config.hpp @@ -45,9 +45,10 @@ namespace Config { extern vector current_boxes; extern vector preset_list; + extern vector available_batteries; extern int current_preset; - //* Check if string only contains space seperated valid names for boxes + //* Check if string only contains space separated valid names for boxes bool check_boxes(const string& boxes); //* Toggle box and update config string shown_boxes diff --git a/src/btop_draw.cpp b/src/btop_draw.cpp index 91a239a..3603406 100644 --- a/src/btop_draw.cpp +++ b/src/btop_draw.cpp @@ -43,42 +43,42 @@ namespace Symbols { const unordered_flat_map> graph_symbols = { { "braille_up", { - " ", "⢀", "⢠", "⢰", "⢸", + " ", "⢀", "⢠", "⢰", "⢸", "⡀", "⣀", "⣠", "⣰", "⣸", "⡄", "⣄", "⣤", "⣴", "⣼", "⡆", "⣆", "⣦", "⣶", "⣾", "⡇", "⣇", "⣧", "⣷", "⣿" }}, {"braille_down", { - " ", "⠈", "⠘", "⠸", "⢸", + " ", "⠈", "⠘", "⠸", "⢸", "⠁", "⠉", "⠙", "⠹", "⢹", "⠃", "⠋", "⠛", "⠻", "⢻", "⠇", "⠏", "⠟", "⠿", "⢿", "⡇", "⡏", "⡟", "⡿", "⣿" }}, {"block_up", { - " ", "▗", "▗", "▐", "▐", + " ", "▗", "▗", "▐", "▐", "▖", "▄", "▄", "▟", "▟", "▖", "▄", "▄", "▟", "▟", "▌", "▙", "▙", "█", "█", "▌", "▙", "▙", "█", "█" }}, {"block_down", { - " ", "▝", "▝", "▐", "▐", + " ", "▝", "▝", "▐", "▐", "▘", "▀", "▀", "▜", "▜", "▘", "▀", "▀", "▜", "▜", "▌", "▛", "▛", "█", "█", "▌", "▛", "▛", "█", "█" }}, {"tty_up", { - " ", "░", "░", "▒", "▒", + " ", "░", "░", "▒", "▒", "░", "░", "▒", "▒", "█", "░", "▒", "▒", "▒", "█", "▒", "▒", "▒", "█", "█", "▒", "█", "█", "█", "█" }}, {"tty_down", { - " ", "░", "░", "▒", "▒", + " ", "░", "░", "▒", "▒", "░", "░", "▒", "▒", "█", "░", "▒", "▒", "▒", "█", "▒", "▒", "▒", "█", "█", @@ -111,7 +111,7 @@ namespace Draw { } for (size_t i = 0; i < line[1].size(); i += 3) { if (line[1][i] == ' ') { - letter = ' '; + letter = Mv::r(1); i -= 2; } else @@ -388,15 +388,22 @@ namespace Draw { } } //? Generate graph symbol from 5x5 2D vector - graphs.at(current).at(horizon) += (height == 1 and result.at(0) + result.at(1) == 0) ? Mv::r(1) : graph_symbol.at((result.at(0) * 5 + result.at(1))); + if (height == 1) { + if (result.at(0) + result.at(1) == 0) graphs.at(current).at(horizon) += Mv::r(1); + else { + if (not color_gradient.empty()) graphs.at(current).at(horizon) += Theme::g(color_gradient).at(clamp(max(last, data_value), 0ll, 100ll)); + graphs.at(current).at(horizon) += graph_symbol.at((result.at(0) * 5 + result.at(1))); + } + } + else graphs.at(current).at(horizon) += graph_symbol.at((result.at(0) * 5 + result.at(1))); } if (mult and i >= 0) last = data_value; } last = data_value; out.clear(); if (height == 1) { - if (not color_gradient.empty()) - out += (last < 1 and not color_gradient.empty() ? Theme::c("inactive_fg") : Theme::g(color_gradient).at(clamp(last, 0ll, 100ll))); + //if (not color_gradient.empty()) + // out += (last < 1 ? Theme::c("inactive_fg") : Theme::g(color_gradient).at(clamp(last, 0ll, 100ll))); out += graphs.at(current).at(0); } else { @@ -431,7 +438,7 @@ namespace Draw { //? Populate the two switching graph vectors and fill empty space if data size < width for (const int& i : iota(0, height * 2)) { if (tty_mode and i % 2 != current) continue; - graphs[(i % 2 != 0)].push_back((value_width < width) ? ((height == 1) ? Mv::r(1) : " "s) * (width - value_width) : ""); + graphs[(i % 2 != 0)].push_back((value_width < width) ? ((height == 1) ? Mv::r(1) : " "s) * (width - value_width) : ""); } if (data.size() == 0) return; this->_create(data, data_offset); @@ -443,7 +450,11 @@ namespace Draw { //? Make room for new characters on graph if (not tty_mode) current = not current; for (const int& i : iota(0, height)) { - if (graphs.at(current).at(i).at(1) == '[') graphs.at(current).at(i).erase(0, 4); + if (height == 1 and graphs.at(current).at(i).at(1) == '[') { + if (graphs.at(current).at(i).at(3) == 'C') graphs.at(current).at(i).erase(0, 4); + else graphs.at(current).at(i).erase(0, graphs.at(current).at(i).find_first_of('m') + 4); + } + else if (graphs.at(current).at(i).at(0) == ' ') graphs.at(current).at(i).erase(0, 1); else graphs.at(current).at(i).erase(0, 3); } this->_create(data, (int)data.size() - 1); @@ -472,21 +483,6 @@ namespace Cpu { vector core_graphs; vector temp_graphs; - unsigned long fastrand(void) { - static unsigned long x=123456789, y=362436069, z=521288629; - unsigned long t; - x ^= x << 16; - x ^= x >> 5; - x ^= x << 1; - - t = x; - x = y; - y = z; - z = t ^ x ^ y; - - return z; - } - string draw(const cpu_info& cpu, const bool force_redraw, const bool data_same) { if (Runner::stopping) return ""; if (force_redraw) redraw = true; @@ -504,6 +500,7 @@ namespace Cpu { const string& title_left = Theme::c("cpu_box") + (cpu_bottom ? Symbols::title_left_down : Symbols::title_left); const string& title_right = Theme::c("cpu_box") + (cpu_bottom ? Symbols::title_right_down : Symbols::title_right); static int bat_pos = 0, bat_len = 0; + if (cpu.cpu_percent.at("total").empty() or cpu.core_percent.at(0).empty() or (show_temps and cpu.temp.at(0).empty())) return ""; string out; out.reserve(width * height); @@ -541,15 +538,15 @@ namespace Cpu { if (b_column_size > 0 or extra_width > 0) { core_graphs.clear(); for (const auto& core_data : cpu.core_percent) { - core_graphs.emplace_back(5 * b_column_size + extra_width, 1, "", core_data, graph_symbol); + core_graphs.emplace_back(5 * b_column_size + extra_width, 1, "cpu", core_data, graph_symbol); } } if (show_temps) { temp_graphs.clear(); - temp_graphs.emplace_back(5, 1, "", cpu.temp.at(0), graph_symbol, false, false, cpu.temp_max, -23); + temp_graphs.emplace_back(5, 1, "temp", cpu.temp.at(0), graph_symbol, false, false, cpu.temp_max, -23); if (not hide_cores and b_column_size > 1) { for (const auto& i : iota((size_t)1, cpu.temp.size())) { - temp_graphs.emplace_back(5, 1, "", cpu.temp.at(i), graph_symbol, false, false, cpu.temp_max, -23); + temp_graphs.emplace_back(5, 1, "temp", cpu.temp.at(i), graph_symbol, false, false, cpu.temp_max, -23); } } } @@ -587,7 +584,7 @@ namespace Cpu { out += Mv::to(y, bat_pos) + title_left + Theme::c("title") + Fx::b + "BAT" + bat_symbol + ' ' + str_percent + (Term::width >= 100 ? Fx::ub + ' ' + bat_meter(percent) + Fx::b : "") - + (not str_time.empty() ? ' ' + Theme::c("title") + str_time : "") + Fx::ub + title_right; + + (not str_time.empty() ? ' ' + Theme::c("title") + str_time : " ") + Fx::ub + title_right; } } else if (bat_pos > 0) { @@ -639,16 +636,16 @@ namespace Cpu { + ljust(to_string(n), core_width); if (b_column_size > 0 or extra_width > 0) out += Theme::c("inactive_fg") + graph_bg * (5 * b_column_size + extra_width) + Mv::l(5 * b_column_size + extra_width) - + Theme::g("cpu").at(clamp(cpu.core_percent.at(n).back(), 0ll, 100ll)) + core_graphs.at(n)(cpu.core_percent.at(n), data_same or redraw); - else - out += Theme::g("cpu").at(clamp(cpu.core_percent.at(n).back(), 0ll, 100ll)); + + core_graphs.at(n)(cpu.core_percent.at(n), data_same or redraw); + + out += Theme::g("cpu").at(clamp(cpu.core_percent.at(n).back(), 0ll, 100ll)); out += rjust(to_string(cpu.core_percent.at(n).back()), (b_column_size < 2 ? 3 : 4)) + Theme::c("main_fg") + '%'; if (show_temps and not hide_cores) { const auto [temp, unit] = celsius_to(cpu.temp.at(n+1).back(), temp_scale); const auto& temp_color = Theme::g("temp").at(clamp(cpu.temp.at(n+1).back() * 100 / cpu.temp_max, 0ll, 100ll)); if (b_column_size > 1) - out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + temp_color + out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + temp_graphs.at(n+1)(cpu.temp.at(n+1), data_same or redraw); out += temp_color + rjust(to_string(temp), 4) + Theme::c("main_fg") + unit; } @@ -711,6 +708,7 @@ namespace Mem { auto& tty_mode = Config::getB("tty_mode"); auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_mem")); auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6); + auto totalMem = Mem::get_totalMem(); string out; out.reserve(height * width); @@ -765,7 +763,7 @@ namespace Mem { for (const auto& [name, disk] : mem.disks) { if (disk.io_read.empty()) continue; - io_graphs[name + "_activity"] = Draw::Graph{disks_width - 6, 1, "", disk.io_activity, graph_symbol}; + io_graphs[name + "_activity"] = Draw::Graph{disks_width - 6, 1, "available", disk.io_activity, graph_symbol}; if (io_mode) { //? Create one combined graph for IO read/write if enabled @@ -804,7 +802,7 @@ namespace Mem { string up = (graph_height >= 2 ? Mv::l(mem_width - 2) + Mv::u(graph_height - 1) : ""); bool big_mem = mem_width > 21; - out += Mv::to(y + 1, x + 2) + Theme::c("title") + Fx::b + "Total:" + rjust(floating_humanizer(Shared::totalMem), mem_width - 9) + Fx::ub + Theme::c("main_fg"); + out += Mv::to(y + 1, x + 2) + Theme::c("title") + Fx::b + "Total:" + rjust(floating_humanizer(totalMem), mem_width - 9) + Fx::ub + Theme::c("main_fg"); vector comb_names (mem_names.begin(), mem_names.end()); if (show_swap and has_swap and not swap_disk) comb_names.insert(comb_names.end(), swap_names.begin(), swap_names.end()); for (auto name : comb_names) { @@ -828,7 +826,7 @@ namespace Mem { const string humanized = floating_humanizer(mem.stats.at(name)); const string graphics = (use_graphs ? mem_graphs.at(name)(mem.percent.at(name), redraw or data_same) : mem_meters.at(name)(mem.percent.at(name).back())); if (mem_size > 2) { - out += Mv::to(y+1+cy, x+1+cx) + divider + ljust(title, 4, false, false, not big_mem) + ljust(":", (big_mem ? 1 : 6)) + out += Mv::to(y+1+cy, x+1+cx) + divider + title.substr(0, big_mem ? 10 : 5) + ":" + Mv::to(y+1+cy, x+cx + mem_width - 2 - humanized.size()) + trans(humanized) + Mv::to(y+2+cy, x+cx + (graph_height >= 2 ? 0 : 1)) + graphics + up + rjust(to_string(mem.percent.at(name).back()) + "%", 4); cy += (graph_height == 0 ? 2 : graph_height + 1); @@ -848,6 +846,7 @@ namespace Mem { cx = mem_width; cy = 0; const bool big_disk = disks_width >= 25; divider = Mv::l(1) + Theme::c("div_line") + Symbols::div_left + Symbols::h_line * disks_width + Theme::c("mem_box") + Fx::ub + Symbols::div_right + Mv::l(disks_width); + const string hu_div = Theme::c("div_line") + Symbols::h_line + Theme::c("main_fg"); if (io_mode) { for (const auto& mount : mem.disks_order) { if (not disks.contains(mount)) continue; @@ -855,13 +854,13 @@ namespace Mem { const auto& disk = disks.at(mount); if (disk.io_read.empty()) continue; const string total = floating_humanizer(disk.total, not big_disk); - out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 2) + Mv::to(y+1+cy, x+cx + disks_width - total.size()) + out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 8) + Mv::to(y+1+cy, x+cx + disks_width - total.size()) + trans(total) + Fx::ub; if (big_disk) { const string used_percent = to_string(disk.used_percent); - out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)used_percent.size() / 2)) + Theme::c("main_fg") + used_percent + '%'; + out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)used_percent.size() / 2) - 1) + hu_div + used_percent + '%' + hu_div; } - out += Mv::to(y+2+cy++, x+1+cx) + (big_disk ? " IO% " : " IO " + Mv::l(2)) + Theme::c("inactive_fg") + graph_bg * (disks_width - 6) + Theme::g("available").at(clamp(disk.io_activity.back(), 50ll, 100ll)) + out += Mv::to(y+2+cy++, x+1+cx) + (big_disk ? " IO% " : " IO " + Mv::l(2)) + Theme::c("inactive_fg") + graph_bg * (disks_width - 6) + Mv::l(disks_width - 6) + io_graphs.at(mount + "_activity")(disk.io_activity, redraw or data_same) + Theme::c("main_fg"); if (++cy > height - 3) break; if (io_graph_combined) { @@ -890,21 +889,21 @@ namespace Mem { if (cy > height - 3) break; const auto& disk = disks.at(mount); auto comb_val = (not disk.io_read.empty() ? disk.io_read.back() + disk.io_write.back() : 0ll); - const string human_io = (comb_val > 0 and big_disk ? (disk.io_write.back() > 0 ? "▼"s : ""s) + (disk.io_read.back() > 0 ? "▲"s : ""s) + const string human_io = (comb_val > 0 ? (disk.io_write.back() > 0 and big_disk ? "▼"s : ""s) + (disk.io_read.back() > 0 and big_disk ? "▲"s : ""s) + floating_humanizer(comb_val, true) : ""); const string human_total = floating_humanizer(disk.total, not big_disk); const string human_used = floating_humanizer(disk.used, not big_disk); const string human_free = floating_humanizer(disk.free, not big_disk); - out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 2) + Mv::to(y+1+cy, x+cx + disks_width - human_total.size()) + out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 8) + Mv::to(y+1+cy, x+cx + disks_width - human_total.size()) + trans(human_total) + Fx::ub + Theme::c("main_fg"); if (big_disk and not human_io.empty()) - out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)human_io.size() / 2)) + Theme::c("main_fg") + human_io; + out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)human_io.size() / 2) - 1) + hu_div + human_io + hu_div; if (++cy > height - 3) break; if (show_io_stat and io_graphs.contains(mount + "_activity")) { out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " IO% " : " IO " + Mv::l(2)) + Theme::c("inactive_fg") + graph_bg * (disks_width - 6) + Theme::g("available").at(clamp(disk.io_activity.back(), 50ll, 100ll)) + Mv::l(disks_width - 6) + io_graphs.at(mount + "_activity")(disk.io_activity, redraw or data_same) + Theme::c("main_fg"); - if (not big_disk) out += Mv::to(y+1+cy, x+cx) + Theme::c("main_fg") + human_io; + if (not big_disk) out += Mv::to(y+1+cy, x+cx+1) + Theme::c("main_fg") + human_io; if (++cy > height - 3) break; } @@ -959,8 +958,8 @@ namespace Net { const string title_left = Theme::c("net_box") + Fx::ub + Symbols::title_left; const string title_right = Theme::c("net_box") + Fx::ub + Symbols::title_right; const int i_size = min((int)selected_iface.size(), 10); - const long long down_max = (net_auto ? graph_max.at("download") : (long long)(Config::getI("net_download") << 20) / 8); - const long long up_max = (net_auto ? graph_max.at("upload") : (long long)(Config::getI("net_upload") << 20) / 8); + const long long down_max = (net_auto ? graph_max.at("download") : ((long long)(Config::getI("net_download")) << 20) / 8); + const long long up_max = (net_auto ? graph_max.at("upload") : ((long long)(Config::getI("net_upload")) << 20) / 8); //* Redraw elements not needed to be updated every cycle if (redraw) { @@ -974,13 +973,13 @@ namespace Net { //? Interface selector and buttons - out += Mv::to(y, x+width - i_size - 10) + title_left + Fx::b + Theme::c("hi_fg") + "" + title_right - + Mv::to(y, x+width - i_size - 16) + title_left + Theme::c("hi_fg") + (net.stat.at("download").offset + net.stat.at("upload").offset > 0 ? Fx::b : "") + 'z' + + Mv::to(y, x+width - i_size - 15) + title_left + Theme::c("hi_fg") + (net.stat.at("download").offset + net.stat.at("upload").offset > 0 ? Fx::b : "") + 'z' + Theme::c("title") + "ero" + title_right; - Input::mouse_mappings["b"] = {y, x+width - i_size - 9, 1, 3}; + Input::mouse_mappings["b"] = {y, x+width - i_size - 8, 1, 3}; Input::mouse_mappings["n"] = {y, x+width - 6, 1, 3}; - Input::mouse_mappings["z"] = {y, x+width - i_size - 15, 1, 4}; + Input::mouse_mappings["z"] = {y, x+width - i_size - 14, 1, 4}; if (width - i_size - 20 > 6) { out += Mv::to(y, x+width - i_size - 21) + title_left + Theme::c("hi_fg") + (net_auto ? Fx::b : "") + 'a' + Theme::c("title") + "uto" + title_right; Input::mouse_mappings["a"] = {y, x+width - i_size - 20, 1, 4}; @@ -1033,9 +1032,10 @@ namespace Proc { int x, y, width = 20, height; int start, selected, select_max; bool shown = true, redraw = true; - int selected_pid = 0; + int selected_pid = 0, selected_depth = 0; string selected_name; unordered_flat_map p_graphs; + unordered_flat_map p_wide_cmd; unordered_flat_map p_counters; int counter = 0; Draw::TextEdit filter; @@ -1051,9 +1051,10 @@ namespace Proc { auto selected = Config::getI("proc_selected"); auto last_selected = Config::getI("proc_last_selected"); const int select_max = (Config::getB("show_detailed") ? Proc::select_max - 8 : Proc::select_max); + auto& vim_keys = Config::getB("vim_keys"); int numpids = Proc::numpids; - if (cmd_key == "up" and selected > 0) { + if ((cmd_key == "up" or (vim_keys and cmd_key == "k")) and selected > 0) { if (start > 0 and selected == 1) start--; else selected--; if (Config::getI("proc_last_selected") > 0) Config::set("proc_last_selected", 0); @@ -1064,7 +1065,7 @@ namespace Proc { else if (cmd_key == "mouse_scroll_down" and start < numpids - select_max) { start = min(numpids - select_max, start + 3); } - else if (cmd_key == "down") { + else if (cmd_key == "down" or (vim_keys and cmd_key == "j")) { if (start < numpids - select_max and selected == select_max) start++; else if (selected == 0 and last_selected > 0) { selected = last_selected; @@ -1080,11 +1081,11 @@ namespace Proc { if (selected > 0 and start >= numpids - select_max) selected = select_max; else start = clamp(start + select_max, 0, max(0, numpids - select_max)); } - else if (cmd_key == "home") { + else if (cmd_key == "home" or (vim_keys and cmd_key == "g")) { start = 0; if (selected > 0) selected = 1; } - else if (cmd_key == "end") { + else if (cmd_key == "end" or (vim_keys and cmd_key == "G")) { start = max(0, numpids - select_max); if (selected > 0) selected = select_max; } @@ -1115,11 +1116,14 @@ namespace Proc { auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_proc")); auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6); auto& mem_bytes = Config::getB("proc_mem_bytes"); + auto& vim_keys = Config::getB("vim_keys"); + auto& show_graphs = Config::getB("proc_cpu_graphs"); start = Config::getI("proc_start"); selected = Config::getI("proc_selected"); const int y = show_detailed ? Proc::y + 8 : Proc::y; const int height = show_detailed ? Proc::height - 8 : Proc::height; const int select_max = show_detailed ? Proc::select_max - 8 : Proc::select_max; + auto totalMem = Mem::get_totalMem(); int numpids = Proc::numpids; if (force_redraw) redraw = true; string out; @@ -1141,6 +1145,10 @@ namespace Proc { prog_size = (width > 70 ? 16 : ( width > 55 ? 8 : width - user_size - thread_size - 33)); cmd_size = (width > 55 ? width - prog_size - user_size - thread_size - 33 : -1); tree_size = width - user_size - thread_size - 23; + if (not show_graphs) { + cmd_size += 5; + tree_size += 5; + } //? Detailed box if (show_detailed) { @@ -1178,7 +1186,7 @@ namespace Proc { if (alive and selected == 0) Input::mouse_mappings["t"] = {d_y, mouse_x, 1, 9}; mouse_x += 11; } - out += title_left + hi_color + Fx::b + 'k' + t_color + "ill" + Fx::ub + title_right + out += title_left + hi_color + Fx::b + (vim_keys ? 'K' : 'k') + t_color + "ill" + Fx::ub + title_right + title_left + hi_color + Fx::b + 's' + t_color + "ignals" + Fx::ub + title_right + Mv::to(d_y, d_x + d_width - 10) + title_left + t_color + Fx::b + hide + Symbols::enter + Fx::ub + title_right; if (alive and selected == 0) { @@ -1271,7 +1279,7 @@ namespace Proc { mouse_x += 11; } if (width > 55) { - out += title_left_down + Fx::b + hi_color + 'k' + t_color + "ill" + Fx::ub + title_right_down; + out += title_left_down + Fx::b + hi_color + (vim_keys ? 'K' : 'k') + t_color + "ill" + Fx::ub + title_right_down; if (selected > 0) Input::mouse_mappings["k"] = {y + height - 1, mouse_x, 1, 4}; mouse_x += 6; } @@ -1291,7 +1299,7 @@ namespace Proc { out += (thread_size > 0 ? Mv::l(4) + "Threads: " : "") + ljust("User:", user_size) + ' ' + rjust((mem_bytes ? "MemB" : "Mem%"), 5) + ' ' - + rjust("Cpu%", 10) + Fx::ub; + + rjust("Cpu%", (show_graphs ? 10 : 5)) + Fx::ub; } //* End of redraw block @@ -1325,7 +1333,7 @@ namespace Proc { if (item_fit >= 8) out += cjust(to_string(detailed.entry.p_nice), item_width); - const double mem_p = (double)detailed.mem_bytes.back() * 100 / Shared::totalMem; + const double mem_p = (double)detailed.mem_bytes.back() * 100 / totalMem; string mem_str = to_string(mem_p); mem_str.resize((mem_p < 10 or mem_p >= 100 ? 3 : 4)); out += Mv::to(d_y + 4, d_x + 1) + Theme::c("title") + Fx::b + rjust((item_fit > 4 ? "Memory: " : "M:") + mem_str + "% ", (d_width / 3) - 2) @@ -1352,11 +1360,12 @@ namespace Proc { if (is_selected) { selected_pid = (int)p.pid; selected_name = p.name; + selected_depth = p.depth; } //? Update graphs for processes with above 0.0% cpu usage, delete if below 0.1% 10x times - const bool has_graph = p_counters.contains(p.pid); - if ((p.cpu_p > 0 and not has_graph) or (not data_same and has_graph)) { + const bool has_graph = show_graphs ? p_counters.contains(p.pid) : false; + if (show_graphs and ((p.cpu_p > 0 and not has_graph) or (not data_same and has_graph))) { if (not has_graph) { p_graphs[p.pid] = Draw::Graph{5, 1, "", {}, graph_symbol}; p_counters[p.pid] = 0; @@ -1383,7 +1392,7 @@ namespace Proc { if (proc_colors) { end = Theme::c("main_fg") + Fx::ub; array colors; - for (int i = 0; int v : {(int)round(p.cpu_p), (int)round(p.mem * 100 / Shared::totalMem), (int)p.threads / 3}) { + for (int i = 0; int v : {(int)round(p.cpu_p), (int)round(p.mem * 100 / totalMem), (int)p.threads / 3}) { if (proc_gradient) { int val = (min(v, 100) + 100) - calc * 100 / select_max; if (val < 100) colors[i++] = Theme::g("proc_color").at(max(0, val)); @@ -1403,12 +1412,14 @@ namespace Proc { } } + if (not p_wide_cmd.contains(p.pid)) p_wide_cmd[p.pid] = ulen(p.cmd) != ulen(p.cmd, true); + //? Normal view line if (not proc_tree) { out += Mv::to(y+2+lc, x+1) + g_color + rjust(to_string(p.pid), 8) + ' ' + c_color + ljust(p.name, prog_size, true) + ' ' + end - + (cmd_size > 0 ? g_color + ljust(p.cmd, cmd_size, true, true) + ' ' : ""); + + (cmd_size > 0 ? g_color + ljust(p.cmd, cmd_size, true, p_wide_cmd[p.pid]) + Mv::to(y+2+lc, x+11+prog_size+cmd_size) + ' ' : ""); } //? Tree view line else { @@ -1421,17 +1432,23 @@ namespace Proc { width_left -= (ulen(p.name) + 1); } if (width_left > 7 and p.short_cmd != p.name) { - out += g_color + '(' + uresize(p.short_cmd, width_left - 3, true) + ") "; + out += g_color + '(' + uresize(p.short_cmd, width_left - 3, p_wide_cmd[p.pid]) + ") "; width_left -= (ulen(p.short_cmd, true) + 3); } - out += string(max(0, width_left), ' '); + out += string(max(0, width_left), ' ') + Mv::to(y+2+lc, x+2+tree_size); } //? Common end of line string cpu_str = to_string(p.cpu_p); - if (p.cpu_p < 10 or p.cpu_p >= 100) cpu_str.resize(3); + if (p.cpu_p < 10 or (p.cpu_p >= 100 and p.cpu_p < 1000)) cpu_str.resize(3); + else if (p.cpu_p >= 10'000) { + cpu_str = to_string(p.cpu_p / 1000); + cpu_str.resize(3); + if (cpu_str.ends_with('.')) cpu_str.pop_back(); + cpu_str += "k"; + } string mem_str = (mem_bytes ? floating_humanizer(p.mem, true) : ""); if (not mem_bytes) { - double mem_p = clamp((double)p.mem * 100 / Shared::totalMem, 0.0, 100.0); + double mem_p = clamp((double)p.mem * 100 / totalMem, 0.0, 100.0); mem_str = to_string(mem_p); if (mem_str.size() < 4) mem_str = "0"; else mem_str.resize((mem_p < 10 or mem_p >= 100 ? 3 : 4)); @@ -1440,7 +1457,7 @@ namespace Proc { out += (thread_size > 0 ? t_color + rjust(to_string(min(p.threads, (size_t)9999)), thread_size) + ' ' + end : "" ) + g_color + ljust((cmp_greater(p.user.size(), user_size) ? p.user.substr(0, user_size - 1) + '+' : p.user), user_size) + ' ' + m_color + rjust(mem_str, 5) + end + ' ' - + (is_selected ? "" : Theme::c("inactive_fg")) + graph_bg * 5 + + (is_selected ? "" : Theme::c("inactive_fg")) + (show_graphs ? graph_bg * 5: "") + (p_graphs.contains(p.pid) ? Mv::l(5) + c_color + p_graphs.at(p.pid)({(p.cpu_p >= 0.1 and p.cpu_p < 5 ? 5ll : (long long)round(p.cpu_p))}, data_same) : "") + end + ' ' + c_color + rjust(cpu_str, 4) + " " + end; if (lc++ > height - 5) break; @@ -1476,6 +1493,15 @@ namespace Proc { } p_graphs.compact(); p_counters.compact(); + + for (auto element = p_wide_cmd.begin(); element != p_wide_cmd.end();) { + if (rng::find(plist, element->first, &proc_info::pid) == plist.end()) { + element = p_wide_cmd.erase(element); + } + else + ++element; + } + p_wide_cmd.compact(); } if (selected == 0 and selected_pid != 0) { @@ -1653,4 +1679,4 @@ namespace Draw { box = createBox(x, y, width, height, Theme::c("proc_box"), true, "proc", "", 4); } } -} \ No newline at end of file +} diff --git a/src/btop_draw.hpp b/src/btop_draw.hpp index 203eed1..e425845 100644 --- a/src/btop_draw.hpp +++ b/src/btop_draw.hpp @@ -52,7 +52,7 @@ namespace Symbols { const string down = "↓"; const string left = "←"; const string right = "→"; - const string enter = "↲"; + const string enter = "↵"; } namespace Draw { diff --git a/src/btop_input.cpp b/src/btop_input.cpp index 2056bea..586dea9 100644 --- a/src/btop_input.cpp +++ b/src/btop_input.cpp @@ -19,6 +19,8 @@ tab-size = 4 #include #include #include +#include +#include #include #include @@ -72,20 +74,74 @@ namespace Input { }; std::atomic interrupt (false); + std::atomic polling (false); array mouse_pos; unordered_flat_map mouse_mappings; deque history(50, ""); string old_filter; + struct InputThr { + InputThr() : thr(run, this) { + } + + static void run(InputThr* that) { + that->runImpl(); + } + + void runImpl() { + char ch = 0; + + // TODO(pg83): read whole buffer + while (cin.get(ch)) { + std::lock_guard g(lock); + current.push_back(ch); + if (current.size() > 100) { + current.clear(); + } + } + } + + size_t avail() { + std::lock_guard g(lock); + + return current.size(); + } + + std::string get() { + std::string res; + + { + std::lock_guard g(lock); + + res.swap(current); + } + + return res; + } + + static InputThr& instance() { + // intentional memory leak, to simplify shutdown process + static InputThr* input = new InputThr(); + + return *input; + } + + std::string current; + // TODO(pg83): use std::conditional_variable instead of sleep + std::mutex lock; + std::thread thr; + }; + bool poll(int timeout) { - if (timeout < 1) return cin.rdbuf()->in_avail() > 0; + atomic_lock lck(polling); + if (timeout < 1) return InputThr::instance().avail() > 0; while (timeout > 0) { if (interrupt) { interrupt = false; return false; } - if (cin.rdbuf()->in_avail() > 0) return true; + if (InputThr::instance().avail() > 0) return true; sleep_ms(timeout < 10 ? timeout : 10); timeout -= 10; } @@ -93,9 +149,7 @@ namespace Input { } string get() { - string key; - while (cin.rdbuf()->in_avail() > 0 and key.size() < 100) key += cin.get(); - if (cin.rdbuf()->in_avail() > 0) cin.ignore(cin.rdbuf()->in_avail()); + string key = InputThr::instance().get(); if (not key.empty()) { //? Remove escape code prefix if present if (key.substr(0, 2) == Fx::e) { @@ -105,14 +159,14 @@ namespace Input { if (key.starts_with("[<")) { std::string_view key_view = key; string mouse_event; - if (key_view.starts_with("[<0;") and key_view.ends_with('M')) { + if (key_view.starts_with("[<0;") and key_view.find('M') != std::string_view::npos) { mouse_event = "mouse_click"; key_view.remove_prefix(4); } - else if (key_view.starts_with("[<0;") and key_view.ends_with('m')) { - mouse_event = "mouse_release"; - key_view.remove_prefix(4); - } + // else if (key_view.starts_with("[<0;") and key_view.ends_with('m')) { + // mouse_event = "mouse_release"; + // key_view.remove_prefix(4); + // } else if (key_view.starts_with("[<64;")) { mouse_event = "mouse_scroll_up"; key_view.remove_prefix(5); @@ -168,32 +222,34 @@ namespace Input { } string wait() { - while (cin.rdbuf()->in_avail() < 1) { + while (InputThr::instance().avail() < 1) { sleep_ms(10); } return get(); } void clear() { - if (cin.rdbuf()->in_avail() > 0) cin.ignore(SSmax); + // do not need it, actually } void process(const string& key) { if (key.empty()) return; try { auto& filtering = Config::getB("proc_filtering"); - + auto& vim_keys = Config::getB("vim_keys"); + auto help_key = (vim_keys ? "H" : "h"); + auto kill_key = (vim_keys ? "K" : "k"); //? Global input actions if (not filtering) { bool keep_going = false; if (str_to_lower(key) == "q") { - exit(0); + clean_quit(0); } else if (is_in(key, "escape", "m")) { Menu::show(Menu::Menus::Main); return; } - else if (is_in(key, "F1", "h")) { + else if (is_in(key, "F1", "?", help_key)) { Menu::show(Menu::Menus::Help); return; } @@ -235,11 +291,15 @@ namespace Input { bool no_update = true; bool redraw = true; if (filtering) { - if (key == "enter") { + if (key == "enter" or key == "down") { Config::set("proc_filter", Proc::filter.text); Config::set("proc_filtering", false); - old_filter.clear(); - } + old_filter.clear(); + if(key == "down"){ + process("down"); + return; + } + } else if (key == "escape" or key == "mouse_click") { Config::set("proc_filter", old_filter); Config::set("proc_filtering", false); @@ -252,19 +312,19 @@ namespace Input { else return; } - else if (key == "left") { + else if (key == "left" or (vim_keys and key == "h")) { int cur_i = v_index(Proc::sort_vector, Config::getS("proc_sorting")); if (--cur_i < 0) cur_i = Proc::sort_vector.size() - 1; Config::set("proc_sorting", Proc::sort_vector.at(cur_i)); } - else if (key == "right") { + else if (key == "right" or (vim_keys and key == "l")) { int cur_i = v_index(Proc::sort_vector, Config::getS("proc_sorting")); if (std::cmp_greater(++cur_i, Proc::sort_vector.size() - 1)) cur_i = 0; Config::set("proc_sorting", Proc::sort_vector.at(cur_i)); } - else if (key == "f") { + else if (is_in(key, "f", "/")) { Config::flip("proc_filtering"); Proc::filter = { Config::getS("proc_filter") }; old_filter = Proc::filter.text; @@ -294,7 +354,16 @@ namespace Input { const auto& current_selection = Config::getI("proc_selected"); if (current_selection == line - y - 1) { redraw = true; - goto proc_mouse_enter; + if (Config::getB("proc_tree")) { + const int x_pos = col - Proc::x; + const int offset = Config::getI("selected_depth") * 3; + if (x_pos > offset and x_pos < 4 + offset) { + process("space"); + return; + } + } + process("enter"); + return; } else if (current_selection == 0 or line - y - 1 == 0) redraw = true; @@ -320,7 +389,6 @@ namespace Input { keep_going = true; } else if (key == "enter") { - proc_mouse_enter: if (Config::getI("proc_selected") == 0 and not Config::getB("show_detailed")) { return; } @@ -344,7 +412,7 @@ namespace Input { if (key == "-" or key == "space") Proc::collapse = pid; no_update = false; } - else if (is_in(key, "t", "k") and (Config::getB("show_detailed") or Config::getI("selected_pid") > 0)) { + else if (is_in(key, "t", kill_key) and (Config::getB("show_detailed") or Config::getI("selected_pid") > 0)) { atomic_wait(Runner::active); if (Config::getB("show_detailed") and Config::getI("proc_selected") == 0 and Proc::detailed.status == "Dead") return; Menu::show(Menu::Menus::SignalSend, (key == "t" ? SIGTERM : SIGKILL)); @@ -356,7 +424,7 @@ namespace Input { Menu::show(Menu::Menus::SignalChoose); return; } - else if (is_in(key, "up", "down", "page_up", "page_down", "home", "end")) { + else if (is_in(key, "up", "down", "page_up", "page_down", "home", "end") or (vim_keys and is_in(key, "j", "k", "g", "G"))) { proc_mouse_scroll: redraw = false; auto old_selected = Config::getI("proc_selected"); @@ -463,8 +531,8 @@ namespace Input { ndev.stat.at("upload").offset = 0; } else { - ndev.stat.at("download").offset = ndev.stat.at("download").last; - ndev.stat.at("upload").offset = ndev.stat.at("upload").last; + ndev.stat.at("download").offset = ndev.stat.at("download").last + ndev.stat.at("download").rollover; + ndev.stat.at("upload").offset = ndev.stat.at("upload").last + ndev.stat.at("upload").rollover; } no_update = false; } diff --git a/src/btop_input.hpp b/src/btop_input.hpp index fdcf6e2..7342709 100644 --- a/src/btop_input.hpp +++ b/src/btop_input.hpp @@ -42,6 +42,7 @@ namespace Input { extern unordered_flat_map mouse_mappings; extern atomic interrupt; + extern atomic polling; //* Mouse column and line position extern array mouse_pos; diff --git a/src/btop_menu.cpp b/src/btop_menu.cpp index 3128544..8037e0f 100644 --- a/src/btop_menu.cpp +++ b/src/btop_menu.cpp @@ -111,7 +111,7 @@ namespace Menu { {"4", "Toggle PROC box."}, {"d", "Toggle disks view in MEM box."}, {"F2, o", "Shows options."}, - {"F1, h", "Shows this window."}, + {"F1, ?, h", "Shows this window."}, {"ctrl + z", "Sleep program and put in background."}, {"q, ctrl + c", "Quits program."}, {"+, -", "Add/Subtract 100ms to/from update timer."}, @@ -126,7 +126,7 @@ namespace Menu { {"z", "Toggle totals reset for current network device"}, {"a", "Toggle auto scaling for the network graphs."}, {"y", "Toggle synced scaling mode for network graphs."}, - {"f", "To enter a process filter."}, + {"f, /", "To enter a process filter."}, {"delete", "Clear any entered filter."}, {"c", "Toggle per-core cpu usage of processes."}, {"r", "Reverse sorting order in processes box."}, @@ -177,6 +177,15 @@ namespace Menu { "Will force 16-color mode and TTY theme,", "set all graph symbols to \"tty\" and swap", "out other non tty friendly symbols."}, + {"vim_keys", + "Enable vim keys.", + "Set to True to enable \"h,j,k,l\" keys for", + "directional control in lists.", + "", + "Conflicting keys for", + "h (help) and k (kill)", + "is accessible while holding shift."}, + {"presets", "Define presets for the layout of the boxes.", "", @@ -185,10 +194,10 @@ namespace Menu { "Max 9 presets.", "", "Format: \"box_name:P:G,box_name:P:G\"", - "P=(0 or 1) for alternate positons.", + "P=(0 or 1) for alternate positions.", "G=graph symbol to use for box.", "", - "Use withespace \" \" as seprator between", + "Use withespace \" \" as separator between", "different presets.", "", "Example:", @@ -197,7 +206,7 @@ namespace Menu { "Manually set which boxes to show.", "", "Available values are \"cpu mem net proc\".", - "Seperate values with whitespace.", + "Separate values with whitespace.", "", "Toggle between presets with key \"p\"."}, {"update_ms", @@ -248,6 +257,14 @@ namespace Menu { "\"%H\" = 24h hour, \"%I\" = 12h hour", "\"%M\" = minute, \"%S\" = second", "\"%d\" = day, \"%m\" = month, \"%y\" = year"}, + {"base_10_sizes", + "Use base 10 for bits and bytes sizes.", + "", + "Uses KB = 1000 instead of KiB = 1024,", + "MB = 1000KB instead of MiB = 1024KiB,", + "and so on.", + "", + "True or False."}, {"background_update", "Update main ui when menus are showing.", "", @@ -261,6 +278,13 @@ namespace Menu { "", "Show battery stats in the top right corner", "if a battery is present."}, + {"selected_battery", + "Select battery.", + "", + "Which battery to use if multiple are present.", + "Can be both batteries and UPS.", + "", + "\"Auto\" for auto detection."}, {"log_level", "Set loglevel for error.log", "", @@ -419,7 +443,7 @@ namespace Menu { "equals 100 percent in the io graphs.", "(100 MiB/s by default).", "", - "Format: \"device:speed\" seperate disks with", + "Format: \"device:speed\" separate disks with", "whitespace \" \".", "", "Example: \"/dev/sda:100, /dev/sdb:20\"."}, @@ -440,12 +464,28 @@ namespace Menu { "", "True or False."}, {"use_fstab", - "Read disks list from /etc/fstab.", - "(Has no effect on macOS X)", + "(Linux) Read disks list from /etc/fstab.", "", "This also disables only_physical.", "", "True or False."}, + {"zfs_hide_datasets", + "(Linux) Hide ZFS datasets in disks list.", + "", + "Setting this to True will hide all datasets,", + "and only show ZFS pools.", + "", + "(IO stats will be calculated per-pool)", + "", + "True or False."}, + {"disk_free_priv", + "(Linux) Type of available disk space.", + "", + "Set to true to show how much disk space is", + "available for privileged users.", + "", + "Set to false to show available for normal", + "users."}, {"disks_filter", "Optional filter for shown disks.", "", @@ -459,6 +499,15 @@ namespace Menu { "", "Example:", "\"exclude=/boot /home/user\""}, + {"zfs_arc_cached", + "(Linux) Count ZFS ARC as cached memory.", + "", + "Add ZFS ARC used to cached memory and", + "ZFS ARC available to available memory.", + "These are otherwise reported by the Linux", + "kernel as used memory.", + "", + "True or False."}, }, { {"graph_symbol_net", @@ -470,13 +519,13 @@ namespace Menu { {"net_download", "Fixed network graph download value.", "", - "Value in Mebibytes, default \"100\".", + "Value in Mebibits, default \"100\".", "", "Can be toggled with auto button."}, {"net_upload", "Fixed network graph upload value.", "", - "Value in Mebibytes, default \"100\".", + "Value in Mebibits, default \"100\".", "", "Can be toggled with auto button."}, {"net_auto", @@ -519,10 +568,10 @@ namespace Menu { "Possible values:", "\"pid\", \"program\", \"arguments\", \"threads\",", "\"user\", \"memory\", \"cpu lazy\" and", - "\"cpu responsive\".", + "\"cpu direct\".", "", "\"cpu lazy\" updates top process over time.", - "\"cpu responsive\" updates top process", + "\"cpu direct\" updates top process", "directly."}, {"proc_reversed", "Reverse processes sorting order.", @@ -558,6 +607,15 @@ namespace Menu { " ", "Will show percentage of total memory", "if False."}, + {"proc_cpu_graphs", + "Show cpu graph for each process.", + "", + "True or False"}, + {"proc_filter_kernel", + "(Linux) Filter kernel processes from output.", + "", + "Set to 'True' to filter out internal", + "processes started by the Linux kernel."}, } }; @@ -581,7 +639,7 @@ namespace Menu { box_contents = Draw::createBox(x, y, width, height, Theme::c("hi_fg"), true, title) + Mv::d(1); for (const auto& line : content) { - box_contents += Mv::save + Mv::r(width / 2 - Fx::uncolor(line).size() / 2 - 1) + line + Mv::restore + Mv::d(1); + box_contents += Mv::save + Mv::r(max((size_t)0, (width / 2) - (Fx::uncolor(line).size() / 2) - 1)) + line + Mv::restore + Mv::d(1); } } @@ -663,7 +721,7 @@ namespace Menu { if (redraw) { x = Term::width/2 - 40; y = Term::height/2 - 9; - bg = Draw::createBox(x + 2, y, 78, 18, Theme::c("hi_fg"), true, "signals"); + bg = Draw::createBox(x + 2, y, 78, 19, Theme::c("hi_fg"), true, "signals"); bg += Mv::to(y+2, x+3) + Theme::c("title") + Fx::b + cjust("Send signal to PID " + to_string(s_pid) + " (" + uresize((s_pid == Config::getI("detailed_pid") ? Proc::detailed.entry.name : Config::getS("selected_name")), 30) + ")", 76); } @@ -695,7 +753,7 @@ namespace Menu { else if (key == "backspace" and selected_signal != -1) { selected_signal = (selected_signal < 10 ? -1 : selected_signal / 10); } - else if (key == "up" and selected_signal != 16) { + else if (is_in(key, "up", "k") and selected_signal != 16) { if (selected_signal == 1) selected_signal = 31; else if (selected_signal < 6) selected_signal += 25; else { @@ -704,7 +762,7 @@ namespace Menu { if (selected_signal <= 16 and offset) selected_signal--; } } - else if (key == "down") { + else if (is_in(key, "down", "j")) { if (selected_signal == 31) selected_signal = 1; else if (selected_signal < 1 or selected_signal == 16) selected_signal = 1; else if (selected_signal > 26) selected_signal -= 25; @@ -715,11 +773,11 @@ namespace Menu { if (selected_signal > 31) selected_signal = 31; } } - else if (key == "left" and selected_signal > 0 and selected_signal != 16) { + else if (is_in(key, "left", "h") and selected_signal > 0 and selected_signal != 16) { if (--selected_signal < 1) selected_signal = 31; else if (selected_signal == 16) selected_signal--; } - else if (key == "right" and selected_signal <= 31 and selected_signal != 16) { + else if (is_in(key, "right", "l") and selected_signal <= 31 and selected_signal != 16) { if (++selected_signal > 31) selected_signal = 1; else if (selected_signal == 16) selected_signal++; } @@ -745,11 +803,12 @@ namespace Menu { } cy++; - out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust("ENTER", 38) + Theme::c("main_fg") + Fx::ub + " | To send signal."; - mouse_mappings["enter"] = {cy, x, 1, 78}; - out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust( "↑ ↓ ← →", 38, true) + Theme::c("main_fg") + Fx::ub + " | To choose signal."; - out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust("ESC or \"q\"", 38) + Theme::c("main_fg") + Fx::ub + " | To abort."; - mouse_mappings["escape"] = {cy, x, 1, 78}; + out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust( "↑ ↓ ← →", 33, true) + Theme::c("main_fg") + Fx::ub + " | To choose signal."; + out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust("0-9", 33) + Theme::c("main_fg") + Fx::ub + " | Enter manually."; + out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust("ENTER", 33) + Theme::c("main_fg") + Fx::ub + " | To send signal."; + mouse_mappings["enter"] = {cy, x, 1, 73}; + out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust("ESC or \"q\"", 33) + Theme::c("main_fg") + Fx::ub + " | To abort."; + mouse_mappings["escape"] = {cy, x, 1, 73}; out += Fx::reset; } @@ -876,10 +935,7 @@ namespace Menu { }; } } - else if (key == "q") { - exit(0); - } - else if (is_in(key, "escape", "m", "mouse_click")) { + else if (is_in(key, "escape", "q", "m", "mouse_click")) { return Closed; } else if (key.starts_with("button_")) { @@ -900,13 +956,13 @@ namespace Menu { currentMenu = Menus::Help; return Switch; case Quit: - exit(0); + clean_quit(0); } } - else if (is_in(key, "down", "tab", "mouse_scroll_down")) { + else if (is_in(key, "down", "tab", "mouse_scroll_down", "j")) { if (++selected > 2) selected = 0; } - else if (is_in(key, "up", "shift_tab", "mouse_scroll_up")) { + else if (is_in(key, "up", "shift_tab", "mouse_scroll_up", "k")) { if (--selected < 0) selected = 2; } else { @@ -953,9 +1009,11 @@ namespace Menu { {"graph_symbol_proc", std::cref(Config::valid_graph_symbols_def)}, {"cpu_graph_upper", std::cref(Cpu::available_fields)}, {"cpu_graph_lower", std::cref(Cpu::available_fields)}, - {"cpu_sensor", std::cref(Cpu::available_sensors)} + {"cpu_sensor", std::cref(Cpu::available_sensors)}, + {"selected_battery", std::cref(Config::available_batteries)}, }; auto& tty_mode = Config::getB("tty_mode"); + auto& vim_keys = Config::getB("vim_keys"); if (max_items == 0) { for (const auto& cat : categories) { if ((int)cat.size() > max_items) max_items = cat.size(); @@ -1054,14 +1112,14 @@ namespace Menu { else if (is_in(key, "escape", "q", "o", "backspace")) { return Closed; } - else if (is_in(key, "down", "mouse_scroll_down")) { + else if (is_in(key, "down", "mouse_scroll_down") or (vim_keys and key == "j")) { if (++selected > select_max or selected >= item_height) { if (page < pages - 1) page++; else if (pages > 1) page = 0; selected = 0; } } - else if (is_in(key, "up", "mouse_scroll_up")) { + else if (is_in(key, "up", "mouse_scroll_up") or (vim_keys and key == "k")) { if (--selected < 0) { if (page > 0) page--; else if (pages > 1) page = pages - 1; @@ -1089,12 +1147,12 @@ namespace Menu { selected_cat = key.back() - '0' - 1; page = selected = 0; } - else if (is_in(key, "left", "right")) { + else if (is_in(key, "left", "right") or (vim_keys and is_in(key, "h", "l"))) { const auto& option = categories[selected_cat][item_height * page + selected][0]; if (selPred.test(isInt)) { const int mod = (option == "update_ms" ? 100 : 1); long value = Config::getI(option); - if (key == "right") value += mod; + if (key == "right" or (vim_keys and key == "l")) value += mod; else value -= mod; if (Config::intValid(option, to_string(value))) @@ -1119,13 +1177,16 @@ namespace Menu { else if (option == "background_update") { Runner::pause_output = false; } + else if (option == "base_10_sizes") { + recollect = true; + } } else if (selPred.test(isBrowseable)) { auto& optList = optionsList.at(option).get(); int i = v_index(optList, Config::getS(option)); - if (key == "right" and ++i >= (int)optList.size()) i = 0; - else if (key == "left" and --i < 0) i = optList.size() - 1; + if ((key == "right" or (vim_keys and key == "l")) and ++i >= (int)optList.size()) i = 0; + else if ((key == "left" or (vim_keys and key == "h")) and --i < 0) i = optList.size() - 1; Config::set(option, optList.at(i)); if (option == "color_theme") diff --git a/src/btop_shared.cpp b/src/btop_shared.cpp new file mode 100644 index 0000000..606c082 --- /dev/null +++ b/src/btop_shared.cpp @@ -0,0 +1,174 @@ +/* Copyright 2021 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. + +indent = tab +tab-size = 4 +*/ + +#include + +#include +#include + +using std::string_literals::operator""s; +namespace rng = std::ranges; +using namespace Tools; + + +namespace Proc { + void proc_sorter(vector& proc_vec, const string& sorting, const bool reverse, const bool tree) { + if (reverse) { + switch (v_index(sort_vector, sorting)) { + case 0: rng::stable_sort(proc_vec, rng::less{}, &proc_info::pid); break; + case 1: rng::stable_sort(proc_vec, rng::less{}, &proc_info::name); break; + case 2: rng::stable_sort(proc_vec, rng::less{}, &proc_info::cmd); break; + case 3: rng::stable_sort(proc_vec, rng::less{}, &proc_info::threads); break; + case 4: rng::stable_sort(proc_vec, rng::less{}, &proc_info::user); break; + case 5: rng::stable_sort(proc_vec, rng::less{}, &proc_info::mem); break; + case 6: rng::stable_sort(proc_vec, rng::less{}, &proc_info::cpu_p); break; + case 7: rng::stable_sort(proc_vec, rng::less{}, &proc_info::cpu_c); break; + } + } + else { + switch (v_index(sort_vector, sorting)) { + case 0: rng::stable_sort(proc_vec, rng::greater{}, &proc_info::pid); break; + case 1: rng::stable_sort(proc_vec, rng::greater{}, &proc_info::name); break; + case 2: rng::stable_sort(proc_vec, rng::greater{}, &proc_info::cmd); break; + case 3: rng::stable_sort(proc_vec, rng::greater{}, &proc_info::threads); break; + case 4: rng::stable_sort(proc_vec, rng::greater{}, &proc_info::user); break; + case 5: rng::stable_sort(proc_vec, rng::greater{}, &proc_info::mem); break; + case 6: rng::stable_sort(proc_vec, rng::greater{}, &proc_info::cpu_p); break; + case 7: rng::stable_sort(proc_vec, rng::greater{}, &proc_info::cpu_c); break; + } + } + + //* When sorting with "cpu lazy" push processes over threshold cpu usage to the front regardless of cumulative usage + if (not tree and not reverse and sorting == "cpu lazy") { + double max = 10.0, target = 30.0; + for (size_t i = 0, x = 0, offset = 0; i < proc_vec.size(); i++) { + if (i <= 5 and proc_vec.at(i).cpu_p > max) + max = proc_vec.at(i).cpu_p; + else if (i == 6) + target = (max > 30.0) ? max : 10.0; + if (i == offset and proc_vec.at(i).cpu_p > 30.0) + offset++; + else if (proc_vec.at(i).cpu_p > target) { + rotate(proc_vec.begin() + offset, proc_vec.begin() + i, proc_vec.begin() + i + 1); + if (++x > 10) break; + } + } + } + } + + void tree_sort(vector& proc_vec, const string& sorting, const bool reverse, int& c_index, const int index_max, const bool collapsed) { + if (proc_vec.size() > 1) { + if (reverse) { + switch (v_index(sort_vector, sorting)) { + case 3: rng::stable_sort(proc_vec, [](const auto& a, const auto& b) { return a.entry.get().threads < b.entry.get().threads; }); break; + case 5: rng::stable_sort(proc_vec, [](const auto& a, const auto& b) { return a.entry.get().mem < b.entry.get().mem; }); break; + case 6: rng::stable_sort(proc_vec, [](const auto& a, const auto& b) { return a.entry.get().cpu_p < b.entry.get().cpu_p; }); break; + case 7: rng::stable_sort(proc_vec, [](const auto& a, const auto& b) { return a.entry.get().cpu_c < b.entry.get().cpu_c; }); break; + } + } + else { + switch (v_index(sort_vector, sorting)) { + case 3: rng::stable_sort(proc_vec, [](const auto& a, const auto& b) { return a.entry.get().threads > b.entry.get().threads; }); break; + case 5: rng::stable_sort(proc_vec, [](const auto& a, const auto& b) { return a.entry.get().mem > b.entry.get().mem; }); break; + case 6: rng::stable_sort(proc_vec, [](const auto& a, const auto& b) { return a.entry.get().cpu_p > b.entry.get().cpu_p; }); break; + case 7: rng::stable_sort(proc_vec, [](const auto& a, const auto& b) { return a.entry.get().cpu_c > b.entry.get().cpu_c; }); break; + } + } + } + + for (auto& r : proc_vec) { + r.entry.get().tree_index = (collapsed or r.entry.get().filtered ? index_max : c_index++); + if (not r.children.empty()) { + tree_sort(r.children, sorting, reverse, c_index, (collapsed or r.entry.get().collapsed or r.entry.get().tree_index == (size_t)index_max)); + } + } + } + + void _tree_gen(proc_info& cur_proc, vector& in_procs, vector& out_procs, int cur_depth, const bool collapsed, const string& filter, bool found, const bool no_update, const bool should_filter) { + auto cur_pos = out_procs.size(); + bool filtering = false; + + //? If filtering, include children of matching processes + if (not found and (should_filter or not filter.empty())) { + if (not s_contains(std::to_string(cur_proc.pid), filter) + and not s_contains(cur_proc.name, filter) + and not s_contains(cur_proc.cmd, filter) + and not s_contains(cur_proc.user, filter)) { + filtering = true; + cur_proc.filtered = true; + filter_found++; + } + else { + found = true; + cur_depth = 0; + } + } + else if (cur_proc.filtered) cur_proc.filtered = false; + + cur_proc.depth = cur_depth; + + //? Set tree index position for process if not filtered out or currently in a collapsed sub-tree + out_procs.push_back({ cur_proc, {} }); + if (not collapsed and not filtering) { + cur_proc.tree_index = out_procs.size() - 1; + + //? Try to find name of the binary file and append to program name if not the same + if (cur_proc.short_cmd.empty() and not cur_proc.cmd.empty()) { + std::string_view cmd_view = cur_proc.cmd; + cmd_view = cmd_view.substr((size_t)0, std::min(cmd_view.find(' '), cmd_view.size())); + cmd_view = cmd_view.substr(std::min(cmd_view.find_last_of('/') + 1, cmd_view.size())); + cur_proc.short_cmd = (string)cmd_view; + } + } + else { + cur_proc.tree_index = in_procs.size(); + } + + //? Recursive iteration over all children + int children = 0; + for (auto& p : rng::equal_range(in_procs, cur_proc.pid, rng::less{}, &proc_info::ppid)) { + if (collapsed and not filtering) { + cur_proc.filtered = true; + } + children++; + + _tree_gen(p, in_procs, out_procs.back().children, cur_depth + 1, (collapsed or cur_proc.collapsed), filter, found, no_update, should_filter); + + if (not no_update and not filtering and (collapsed or cur_proc.collapsed)) { + //auto& parent = cur_proc; + cur_proc.cpu_p += p.cpu_p; + cur_proc.cpu_c += p.cpu_c; + cur_proc.mem += p.mem; + cur_proc.threads += p.threads; + filter_found++; + p.filtered = true; + } + } + if (collapsed or filtering) { + return; + } + + //? Add tree terminator symbol if it's the last child in a sub-tree + if (children > 0 and out_procs.back().children.back().entry.get().prefix.size() >= 8 and not out_procs.back().children.back().entry.get().prefix.ends_with("]─")) + out_procs.back().children.back().entry.get().prefix.replace(out_procs.back().children.back().entry.get().prefix.size() - 8, 8, " └─ "); + + //? Add collapse/expand symbols if process have any children + out_procs.at(cur_pos).entry.get().prefix = " │ "s * cur_depth + (children > 0 ? (cur_proc.collapsed ? "[+]─" : "[-]─") : " ├─ "); + } + +} \ No newline at end of file diff --git a/src/btop_shared.hpp b/src/btop_shared.hpp index 4d0f323..1eb81b6 100644 --- a/src/btop_shared.hpp +++ b/src/btop_shared.hpp @@ -27,12 +27,15 @@ tab-size = 4 #include #include #include +#include using std::string, std::vector, std::deque, robin_hood::unordered_flat_map, std::atomic, std::array, std::tuple; void term_resize(bool force=false); void banner_gen(); +extern void clean_quit(int sig); + namespace Global { extern const vector> Banner_src; extern const string Version; @@ -43,6 +46,7 @@ namespace Global { extern atomic resized; extern string overlay; extern string clock; + extern uid_t real_uid, set_uid; } namespace Runner { @@ -70,8 +74,6 @@ namespace Shared { void init(); extern long coreCount, page_size, clk_tck; - extern int totalMem_len; - extern uint64_t totalMem; } @@ -129,6 +131,7 @@ namespace Mem { struct disk_info { std::filesystem::path dev; string name; + string fstype = ""; std::filesystem::path stat = ""; int64_t total = 0, used = 0, free = 0; int used_percent = 0, free_percent = 0; @@ -149,11 +152,15 @@ namespace Mem { vector disks_order; }; + //?* Get total system memory + uint64_t get_totalMem(); + //* Collect mem & disks stats auto collect(const bool no_update=false) -> mem_info&; //* Draw contents of mem box using as source string draw(const mem_info& mem, const bool force_redraw=false, const bool data_same=false); + } namespace Net { @@ -166,7 +173,7 @@ namespace Net { extern unordered_flat_map graph_max; struct net_stat { - uint64_t speed = 0, top = 0, total = 0, last = 0, offset = 0; + uint64_t speed = 0, top = 0, total = 0, last = 0, offset = 0, rollover = 0; }; struct net_info { @@ -193,7 +200,7 @@ namespace Proc { extern bool shown, redraw; extern int select_max; extern atomic detailed_pid; - extern int selected_pid, start, selected, collapse, expand; + extern int selected_pid, start, selected, collapse, expand, filter_found, selected_depth; extern string selected_name; //? Contains the valid sorting options for processes @@ -262,4 +269,18 @@ namespace Proc { //* Draw contents of proc box using as data source string draw(const vector& plist, const bool force_redraw=false, const bool data_same=false); + + struct tree_proc { + std::reference_wrapper entry; + vector children; + }; + + //* Sort vector of proc_info's + void proc_sorter(vector& proc_vec, const string& sorting, const bool reverse, const bool tree = false); + + //* Recursive sort of process tree + void tree_sort(vector& proc_vec, const string& sorting, const bool reverse, int& c_index, const int index_max, const bool collapsed = false); + + //* Generate process tree list + void _tree_gen(proc_info& cur_proc, vector& in_procs, vector& out_procs, int cur_depth, const bool collapsed, const string& filter, bool found=false, const bool no_update=false, const bool should_filter=false); } diff --git a/src/btop_tools.cpp b/src/btop_tools.cpp index d801fe1..301e554 100644 --- a/src/btop_tools.cpp +++ b/src/btop_tools.cpp @@ -23,15 +23,17 @@ tab-size = 4 #include #include #include +#include #include +#include #include -#include #include #include #include #include +#include using std::string_view, std::max, std::floor, std::to_string, std::cin, std::cout, std::flush, robin_hood::unordered_flat_map; namespace fs = std::filesystem; @@ -46,7 +48,6 @@ namespace Term { atomic width = 0; atomic height = 0; string current_tty; - char* custombuf; namespace { struct termios initial_settings; @@ -73,12 +74,14 @@ namespace Term { } } - bool refresh() { + bool refresh(bool only_check) { struct winsize w; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) < 0) return false; if (width != w.ws_col or height != w.ws_row) { - width = w.ws_col; - height = w.ws_row; + if (not only_check) { + width = w.ws_col; + height = w.ws_row; + } return true; } return false; @@ -120,9 +123,6 @@ namespace Term { linebuffered(false); refresh(); - //? Set 1MB buffer for cout - std::cout.rdbuf()->pubsetbuf(custombuf, 1048576); - cout << alt_screen << hide_cursor << mouse_on << flush; Global::resized = false; } @@ -141,21 +141,85 @@ namespace Term { //? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- +// ! Dsiabled due to issue when compiling with musl, reverted back to using regex +// namespace Fx { +// string uncolor(const string& s) { +// string out = s; +// for (size_t offset = 0, start_pos = 0, end_pos = 0;;) { +// start_pos = (offset == 0) ? out.find('\x1b') : offset; +// if (start_pos == string::npos) +// break; +// offset = start_pos + 1; +// end_pos = out.find('m', offset); +// if (end_pos == string::npos) +// break; +// else if (auto next_pos = out.find('\x1b', offset); not isdigit(out[end_pos - 1]) or end_pos > next_pos) { +// offset = next_pos; +// continue; +// } + +// out.erase(start_pos, (end_pos - start_pos)+1); +// offset = 0; +// } +// out.shrink_to_fit(); +// return out; +// } +// } + namespace Tools { - atomic active_locks (0); + size_t wide_ulen(const string& str) { + unsigned int chars = 0; + try { + std::wstring_convert> conv; + auto w_str = conv.from_bytes((str.size() > 10000 ? str.substr(0, 10000).c_str() : str.c_str())); + + for (auto c : w_str) { + chars += utf8::wcwidth(c); + } + } + catch (...) { + return ulen(str); + } + + return chars; + } + + size_t wide_ulen(const std::wstring& w_str) { + unsigned int chars = 0; + + for (auto c : w_str) { + chars += utf8::wcwidth(c); + } + + return chars; + } string uresize(string str, const size_t len, const bool wide) { if (len < 1 or str.empty()) return ""; - for (size_t x = 0, i = 0; i < str.size(); i++) { - if (wide and static_cast(str.at(i)) > 0xef) x += 2; - else if ((static_cast(str.at(i)) & 0xC0) != 0x80) x++; - if (x >= len + 1) { - str.resize(i); - str.shrink_to_fit(); - break; + if (wide) { + try { + std::wstring_convert> conv; + auto w_str = conv.from_bytes((str.size() > 10000 ? str.substr(0, 10000).c_str() : str.c_str())); + while (wide_ulen(w_str) > len) + w_str.pop_back(); + string n_str = conv.to_bytes(w_str); + return n_str; + } + catch (...) { + return uresize(str, len, false); } } + else { + for (size_t x = 0, i = 0; i < str.size(); i++) { + if ((static_cast(str.at(i)) & 0xC0) != 0x80) x++; + if (x >= len + 1) { + str.resize(i); + break; + } + } + } + str.shrink_to_fit(); return str; } @@ -271,28 +335,45 @@ namespace Tools { string floating_humanizer(uint64_t value, const bool shorten, size_t start, const bool bit, const bool per_second) { string out; const size_t mult = (bit) ? 8 : 1; - static const array Units_bit = {"bit", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib", "Bib", "GEb"}; - static const array Units_byte = {"Byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "BiB", "GEB"}; - const auto& units = (bit) ? Units_bit : Units_byte; + const bool mega = Config::getB("base_10_sizes"); + static const array mebiUnits_bit = {"bit", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib", "Bib", "GEb"}; + static const array mebiUnits_byte = {"Byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "BiB", "GEB"}; + static const array megaUnits_bit = {"bit", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb", "Bb", "Gb"}; + static const array megaUnits_byte = {"Byte", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "GB"}; + const auto& units = (bit) ? ( mega ? megaUnits_bit : mebiUnits_bit) : ( mega ? megaUnits_byte : mebiUnits_byte); value *= 100 * mult; - while (value >= 102400) { - value >>= 10; - if (value < 100) { - out = to_string(value); - break; + if (mega) { + while (value >= 100000) { + value /= 1000; + if (value < 100) { + out = to_string(value); + break; + } + start++; + } + } + else { + while (value >= 102400) { + value >>= 10; + if (value < 100) { + out = to_string(value); + break; + } + start++; } - start++; } if (out.empty()) { out = to_string(value); - if (out.size() == 4 and start > 0) { out.pop_back(); out.insert(2, ".");} + if (not mega and out.size() == 4 and start > 0) { out.pop_back(); out.insert(2, ".");} else if (out.size() == 3 and start > 0) out.insert(1, "."); else if (out.size() >= 2) out.resize(out.size() - 2); } if (shorten) { - if (out.find('.') != string::npos) out = to_string((int)round(stof(out))); + auto f_pos = out.find('.'); + if (f_pos == 1 and out.size() > 3) out = to_string(round(stof(out) * 10) / 10).substr(0,3); + else if (f_pos != string::npos) out = to_string((int)round(stof(out))); if (out.size() > 3) { out = to_string((int)(out[0] - '0') + 1); start++;} out.push_back(units[start][0]); } @@ -319,15 +400,22 @@ namespace Tools { return ss.str(); } - atomic_lock::atomic_lock(atomic& atom) : atom(atom) { - active_locks++; - while (not this->atom.compare_exchange_strong(this->not_true, true)); + void atomic_wait(const atomic& atom, const bool old) noexcept { + while (atom.load(std::memory_order_relaxed) == old ) busy_wait(); + } + + void atomic_wait_for(const atomic& atom, const bool old, const uint64_t wait_ms) noexcept { + const uint64_t start_time = time_ms(); + while (atom.load(std::memory_order_relaxed) == old and (time_ms() - start_time < wait_ms)) sleep_ms(1); + } + + atomic_lock::atomic_lock(atomic& atom, bool wait) : atom(atom) { + if (wait) while (not this->atom.compare_exchange_strong(this->not_true, true)); + else this->atom.store(true); } atomic_lock::~atomic_lock() { - active_locks--; this->atom.store(false); - atomic_notify(this->atom); } string readfile(const std::filesystem::path& path, const string& fallback) { @@ -338,7 +426,8 @@ namespace Tools { for (string readstr; getline(file, readstr); out += readstr); } catch (const std::exception& e) { - throw std::runtime_error("readfile() : Exception when reading " + (string)path + " : " + e.what()); + Logger::error("readfile() : Exception when reading " + (string)path + " : " + e.what()); + return fallback; } return (out.empty() ? fallback : out); } @@ -371,22 +460,33 @@ namespace Tools { namespace Logger { using namespace Tools; - namespace { - std::atomic busy (false); - bool first = true; - const string tdf = "%Y/%m/%d (%T) | "; - } + std::atomic busy (false); + bool first = true; + const string tdf = "%Y/%m/%d (%T) | "; size_t loglevel; fs::path logfile; + //* Wrapper for lowering priviliges if using SUID bit and currently isn't using real userid + class lose_priv { + int status = -1; + public: + lose_priv() { + if (geteuid() != Global::real_uid) this->status = seteuid(Global::real_uid); + } + ~lose_priv() { + if (status == 0) status = seteuid(Global::set_uid); + } + }; + void set(const string& level) { loglevel = v_index(log_levels, level); } void log_write(const size_t level, const string& msg) { if (loglevel < level or logfile.empty()) return; - atomic_lock lck(busy); + atomic_lock lck(busy, true); + lose_priv neutered{}; std::error_code ec; try { if (fs::exists(logfile) and fs::file_size(logfile, ec) > 1024 << 10 and not ec) { diff --git a/src/btop_tools.hpp b/src/btop_tools.hpp index da4a784..862981a 100644 --- a/src/btop_tools.hpp +++ b/src/btop_tools.hpp @@ -21,16 +21,24 @@ tab-size = 4 #include #include #include -#include #include +#include #include #include #include #include #include #include +#include +#ifndef HOST_NAME_MAX + #ifdef __APPLE__ + #define HOST_NAME_MAX 255 + #else + #define HOST_NAME_MAX 64 + #endif +#endif -using std::string, std::vector, std::atomic, std::to_string, std::regex, std::tuple, std::array; +using std::string, std::vector, std::atomic, std::to_string, std::tuple, std::array; //? ------------------------------------------------- NAMESPACES ------------------------------------------------------ @@ -58,13 +66,14 @@ namespace Fx { extern string reset; //* Regex for matching color, style and cursor move escape sequences - const regex escape_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m|f|s|u|C|D|A|B){1}"); + const std::regex escape_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m|f|s|u|C|D|A|B){1}"); //* Regex for matching only color and style escape sequences - const regex color_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m){1}"); + const std::regex color_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m){1}"); //* Return a string with all colors and text styling removed - inline string uncolor(const string& s) { return regex_replace(s, color_regex, ""); } + inline string uncolor(const string& s) { return std::regex_replace(s, color_regex, ""); } + // string uncolor(const string& s); } @@ -107,14 +116,14 @@ namespace Term { const string clear_end = Fx::e + "0J"; const string clear_begin = Fx::e + "1J"; const string mouse_on = Fx::e + "?1002h" + Fx::e + "?1015h" + Fx::e + "?1006h"; //? Enable reporting of mouse position on click and release - const string mouse_off = Fx::e + "?1002l"; + const string mouse_off = Fx::e + "?1002l" + Fx::e + "?1015l" + Fx::e + "?1006l"; const string mouse_direct_on = Fx::e + "?1003h"; //? Enable reporting of mouse position at any movement const string mouse_direct_off = Fx::e + "?1003l"; const string sync_start = Fx::e + "?2026h"; //? Start of terminal synchronized output const string sync_end = Fx::e + "?2026l"; //? End of terminal synchronized output //* Returns true if terminal has been resized and updates width and height - bool refresh(); + bool refresh(bool only_check=false); //* Returns an array with the lowest possible width, height with current box config auto get_min_size(const string& boxes) -> array; @@ -130,12 +139,13 @@ namespace Term { namespace Tools { constexpr auto SSmax = std::numeric_limits::max(); - extern atomic active_locks; - //* Return number of UTF8 characters in a string (wide=true counts UTF-8 characters with a width > 1 as 2 characters) + size_t wide_ulen(const string& str); + size_t wide_ulen(const std::wstring& w_str); + + //* Return number of UTF8 characters in a string (wide=true for column size needed on terminal) inline size_t ulen(const string& str, const bool wide=false) { - return std::ranges::count_if(str, [](char c) { return (static_cast(c) & 0xC0) != 0x80; }) - + (wide ? std::ranges::count_if(str, [](char c) { return (static_cast(c) > 0xef); }) : 0); + return (wide ? wide_ulen(str) : std::ranges::count_if(str, [](char c) { return (static_cast(c) & 0xC0) != 0x80; })); } //* Resize a string consisting of UTF8 characters (only reduces size) @@ -177,6 +187,16 @@ namespace Tools { return str.find(find_val) != string::npos; } + //* Check if string contains string , while ignoring case + inline bool s_contains_ic(const string& str, const string& find_val) { + auto it = std::search( + str.begin(), str.end(), + find_val.begin(), find_val.end(), + [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); } + ); + return it != str.end(); + } + //* Return index of from vector , returns size of if is not present template inline size_t v_index(const vector& vec, const T& find_val) { @@ -269,20 +289,28 @@ namespace Tools { string hostname(); string username(); -#if __GNUC__ < 11 - inline void atomic_wait(const atomic& atom, const bool old=true) noexcept { while (atom.load() == old) sleep_ms(1); } - inline void atomic_notify(const atomic& atom) noexcept { (void)atom; } -#else - inline void atomic_wait(const atomic& atom, const bool old=true) noexcept { atom.wait(old); } - inline void atomic_notify(const atomic& atom) noexcept { atom.notify_all(); } -#endif + static inline void busy_wait (void) { + #if defined __i386__ || defined __x86_64__ + __builtin_ia32_pause(); + #elif defined __ia64__ + __asm volatile("hint @pause" : : : "memory"); + #elif defined __sparc__ && (defined __arch64__ || defined __sparc_v9__) + __asm volatile("membar #LoadLoad" : : : "memory"); + #else + __asm volatile("" : : : "memory"); + #endif + } - //* Waits for atomic to be false and sets it to true on construct, sets to false and notifies on destruct + void atomic_wait(const atomic& atom, const bool old=true) noexcept; + + void atomic_wait_for(const atomic& atom, const bool old=true, const uint64_t wait_ms=0) noexcept; + + //* Sets atomic to true on construct, sets to false on destruct class atomic_lock { atomic& atom; bool not_true = false; public: - atomic_lock(atomic& atom); + atomic_lock(atomic& atom, bool wait=false); ~atomic_lock(); }; diff --git a/src/freebsd/btop_collect.cpp b/src/freebsd/btop_collect.cpp new file mode 100644 index 0000000..9e5be73 --- /dev/null +++ b/src/freebsd/btop_collect.cpp @@ -0,0 +1,1319 @@ +/* Copyright 2021 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. + +indent = tab +tab-size = 4 +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; +using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min; +namespace fs = std::filesystem; +namespace rng = std::ranges; +using namespace Tools; + +//? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- + +namespace Cpu { + vector core_old_totals; + vector core_old_idles; + vector available_fields = {"total"}; + vector available_sensors = {"Auto"}; + cpu_info current_cpu; + bool got_sensors = false, cpu_temp_only = false; + + //* Populate found_sensors map + bool get_sensors(); + + //* Get current cpu clock speed + string get_cpuHz(); + + //* Search /proc/cpuinfo for a cpu name + string get_cpuName(); + + struct Sensor { + fs::path path; + string label; + int64_t temp = 0; + int64_t high = 0; + int64_t crit = 0; + }; + + string cpu_sensor; + vector core_sensors; + unordered_flat_map core_mapping; +} // namespace Cpu + +namespace Mem { + double old_uptime; + std::vector zpools; + + void get_zpools(); +} + +namespace Shared { + + fs::path passwd_path; + uint64_t totalMem; + long pageSize, clkTck, coreCount, physicalCoreCount, arg_max; + int totalMem_len, kfscale; + long bootTime; + + void init() { + //? Shared global variables init + int mib[2]; + mib[0] = CTL_HW; + mib[1] = HW_NCPU; + int ncpu; + size_t len = sizeof(ncpu); + if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { + Logger::warning("Could not determine number of cores, defaulting to 1."); + } else { + coreCount = ncpu; + } + + pageSize = sysconf(_SC_PAGE_SIZE); + if (pageSize <= 0) { + pageSize = 4096; + Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect."); + } + + clkTck = sysconf(_SC_CLK_TCK); + if (clkTck <= 0) { + clkTck = 100; + Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect."); + } + + int64_t memsize = 0; + size_t size = sizeof(memsize); + if (sysctlbyname("hw.physmem", &memsize, &size, NULL, 0) < 0) { + Logger::warning("Could not get memory size"); + } + totalMem = memsize; + + struct timeval result; + size = sizeof(result); + if (sysctlbyname("kern.boottime", &result, &size, NULL, 0) < 0) { + Logger::warning("Could not get boot time"); + } else { + bootTime = result.tv_sec; + } + + size = sizeof(kfscale); + if (sysctlbyname("kern.fscale", &kfscale, &size, NULL, 0) == -1) { + kfscale = 2048; + } + + //* Get maximum length of process arguments + arg_max = sysconf(_SC_ARG_MAX); + + //? Init for namespace Cpu + Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {}); + Cpu::current_cpu.temp.insert(Cpu::current_cpu.temp.begin(), Shared::coreCount + 1, {}); + Cpu::core_old_totals.insert(Cpu::core_old_totals.begin(), Shared::coreCount, 0); + Cpu::core_old_idles.insert(Cpu::core_old_idles.begin(), Shared::coreCount, 0); + Cpu::collect(); + for (auto &[field, vec] : Cpu::current_cpu.cpu_percent) { + if (not vec.empty() and not v_contains(Cpu::available_fields, field)) Cpu::available_fields.push_back(field); + } + Cpu::cpuName = Cpu::get_cpuName(); + Cpu::got_sensors = Cpu::get_sensors(); + Cpu::core_mapping = Cpu::get_core_mapping(); + + //? Init for namespace Mem + Mem::old_uptime = system_uptime(); + Mem::collect(); + Mem::get_zpools(); + } + +} // namespace Shared + +namespace Cpu { + string cpuName; + string cpuHz; + bool has_battery = true; + tuple current_bat; + + const array time_names = {"user", "nice", "system", "idle"}; + + unordered_flat_map cpu_old = { + {"totals", 0}, + {"idles", 0}, + {"user", 0}, + {"nice", 0}, + {"system", 0}, + {"idle", 0} + }; + + string get_cpuName() { + string name; + char buffer[1024]; + size_t size = sizeof(buffer); + if (sysctlbyname("hw.model", &buffer, &size, NULL, 0) < 0) { + Logger::error("Failed to get CPU name"); + return name; + } + name = string(buffer); + + auto name_vec = ssplit(name); + + if ((s_contains(name, "Xeon"s) or v_contains(name_vec, "Duo"s)) and v_contains(name_vec, "CPU"s)) { + auto cpu_pos = v_index(name_vec, "CPU"s); + if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')')) + name = name_vec.at(cpu_pos + 1); + else + name.clear(); + } else if (v_contains(name_vec, "Ryzen"s)) { + auto ryz_pos = v_index(name_vec, "Ryzen"s); + name = "Ryzen" + (ryz_pos < name_vec.size() - 1 ? ' ' + name_vec.at(ryz_pos + 1) : "") + (ryz_pos < name_vec.size() - 2 ? ' ' + name_vec.at(ryz_pos + 2) : ""); + } else if (s_contains(name, "Intel"s) and v_contains(name_vec, "CPU"s)) { + auto cpu_pos = v_index(name_vec, "CPU"s); + if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')') and name_vec.at(cpu_pos + 1) != "@") + name = name_vec.at(cpu_pos + 1); + else + name.clear(); + } else + name.clear(); + + if (name.empty() and not name_vec.empty()) { + for (const auto &n : name_vec) { + if (n == "@") break; + name += n + ' '; + } + name.pop_back(); + for (const auto& replace : {"Processor", "CPU", "(R)", "(TM)", "Intel", "AMD", "Core"}) { + name = s_replace(name, replace, ""); + name = s_replace(name, " ", " "); + } + name = trim(name); + } + + return name; + } + + bool get_sensors() { + got_sensors = false; + if (Config::getB("show_coretemp") and Config::getB("check_temp")) { + int32_t temp; + size_t size = sizeof(temp); + if (sysctlbyname("dev.cpu.0.temperature", &temp, &size, NULL, 0) < 0) { + Logger::warning("Could not get temp sensor - maybe you need to load the coretemp module"); + } else { + got_sensors = true; + int temp; + size_t size = sizeof(temp); + sysctlbyname("dev.cpu.0.coretemp.tjmax", &temp, &size, NULL, 0); //asuming the max temp is same for all cores + temp = (temp - 2732) / 10; // since it's an int, it's multiplied by 10, and offset to absolute zero... + current_cpu.temp_max = temp; + } + } + return got_sensors; + } + + void update_sensors() { + int temp = 0; + int p_temp = 0; + int found = 0; + bool got_package = false; + size_t size = sizeof(p_temp); + if (sysctlbyname("hw.acpi.thermal.tz0.temperature", &p_temp, &size, NULL, 0) >= 0) { + got_package = true; + p_temp = (p_temp - 2732) / 10; // since it's an int, it's multiplied by 10, and offset to absolute zero... + } + + size = sizeof(temp); + for (int i = 0; i < Shared::coreCount; i++) { + string s = "dev.cpu." + std::to_string(i) + ".temperature"; + if (sysctlbyname(s.c_str(), &temp, &size, NULL, 0) >= 0) { + temp = (temp - 2732) / 10; + if (not got_package) { + p_temp += temp; + found++; + } + if (cmp_less(i + 1, current_cpu.temp.size())) { + current_cpu.temp.at(i + 1).push_back(temp); + if (current_cpu.temp.at(i + 1).size() > 20) + current_cpu.temp.at(i + 1).pop_front(); + } + } + } + + if (not got_package) p_temp /= found; + current_cpu.temp.at(0).push_back(p_temp); + if (current_cpu.temp.at(0).size() > 20) + current_cpu.temp.at(0).pop_front(); + + } + + string get_cpuHz() { + unsigned int freq = 1; + size_t size = sizeof(freq); + + if (sysctlbyname("dev.cpu.0.freq", &freq, &size, NULL, 0) < 0) { + return ""; + } + return std::to_string(freq / 1000.0 ).substr(0, 3); // seems to be in MHz + } + + auto get_core_mapping() -> unordered_flat_map { + unordered_flat_map core_map; + if (cpu_temp_only) return core_map; + + for (long i = 0; i < Shared::coreCount; i++) { + core_map[i] = i; + } + + //? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely, map 0-0 1-1 2-2 etc. + if (cmp_less(core_map.size(), Shared::coreCount)) { + if (Shared::coreCount % 2 == 0 and (long) core_map.size() == Shared::coreCount / 2) { + for (int i = 0, n = 0; i < Shared::coreCount / 2; i++) { + if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; + core_map[Shared::coreCount / 2 + i] = n++; + } + } else { + core_map.clear(); + for (int i = 0, n = 0; i < Shared::coreCount; i++) { + if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; + core_map[i] = n++; + } + } + } + + //? Apply user set custom mapping if any + const auto &custom_map = Config::getS("cpu_core_map"); + if (not custom_map.empty()) { + try { + for (const auto &split : ssplit(custom_map)) { + const auto vals = ssplit(split, ':'); + if (vals.size() != 2) continue; + int change_id = std::stoi(vals.at(0)); + int new_id = std::stoi(vals.at(1)); + if (not core_map.contains(change_id) or cmp_greater(new_id, core_sensors.size())) continue; + core_map.at(change_id) = new_id; + } + } catch (...) { + } + } + + return core_map; + } + + auto get_battery() -> tuple { + if (not has_battery) return {0, 0, ""}; + + long seconds = -1; + uint32_t percent = -1; + size_t size = sizeof(percent); + string status = "discharging"; + if (sysctlbyname("hw.acpi.battery.life", &percent, &size, NULL, 0) < 0) { + has_battery = false; + } else { + has_battery = true; + size_t size = sizeof(seconds); + if (sysctlbyname("hw.acpi.battery.time", &seconds, &size, NULL, 0) < 0) { + seconds = 0; + } + int state; + size = sizeof(state); + if (sysctlbyname("hw.acpi.battery.state", &state, &size, NULL, 0) < 0) { + status = "unknown"; + } else { + if (state == 2) { + status = "charging"; + } + } + if (percent == 100) { + status = "full"; + } + } + + return {percent, seconds, status}; + } + + auto collect(const bool no_update) -> cpu_info & { + if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty())) + return current_cpu; + auto &cpu = current_cpu; + + double avg[3]; + + if (getloadavg(avg, sizeof(avg)) < 0) { + Logger::error("failed to get load averages"); + } + + cpu.load_avg = { (float)avg[0], (float)avg[1], (float)avg[2]}; + + vector> cpu_time(Shared::coreCount); + size_t size = sizeof(long) * CPUSTATES * Shared::coreCount; + if (sysctlbyname("kern.cp_times", &cpu_time[0], &size, NULL, 0) == -1) { + Logger::error("failed to get CPU times"); + } + long long global_totals = 0; + long long global_idles = 0; + vector times_summed = {0, 0, 0, 0}; + + for (long i = 0; i < Shared::coreCount; i++) { + vector times; + //? 0=user, 1=nice, 2=system, 3=idle + for (int x = 0; const unsigned int c_state : {CP_USER, CP_NICE, CP_SYS, CP_IDLE}) { + auto val = cpu_time[i][c_state]; + times.push_back(val); + times_summed.at(x++) += val; + } + try { + //? All values + const long long totals = std::accumulate(times.begin(), times.end(), 0ll); + + //? Idle time + const long long idles = times.at(3); + + global_totals += totals; + global_idles += idles; + + //? Calculate cpu total for each core + if (i > Shared::coreCount) break; + const long long calc_totals = max(0ll, totals - core_old_totals.at(i)); + const long long calc_idles = max(0ll, idles - core_old_idles.at(i)); + core_old_totals.at(i) = totals; + core_old_idles.at(i) = idles; + + cpu.core_percent.at(i).push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); + + //? Reduce size if there are more values than needed for graph + if (cpu.core_percent.at(i).size() > 40) cpu.core_percent.at(i).pop_front(); + + } catch (const std::exception &e) { + Logger::error("Cpu::collect() : " + (string)e.what()); + throw std::runtime_error("collect() : " + (string)e.what()); + } + + } + + const long long calc_totals = max(1ll, global_totals - cpu_old.at("totals")); + const long long calc_idles = max(1ll, global_idles - cpu_old.at("idles")); + + //? Populate cpu.cpu_percent with all fields from syscall + for (int ii = 0; const auto &val : times_summed) { + cpu.cpu_percent.at(time_names.at(ii)).push_back(clamp((long long)round((double)(val - cpu_old.at(time_names.at(ii))) * 100 / calc_totals), 0ll, 100ll)); + cpu_old.at(time_names.at(ii)) = val; + + //? Reduce size if there are more values than needed for graph + while (cmp_greater(cpu.cpu_percent.at(time_names.at(ii)).size(), width * 2)) cpu.cpu_percent.at(time_names.at(ii)).pop_front(); + + ii++; + } + + cpu_old.at("totals") = global_totals; + cpu_old.at("idles") = global_idles; + + //? Total usage of cpu + cpu.cpu_percent.at("total").push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); + + //? Reduce size if there are more values than needed for graph + while (cmp_greater(cpu.cpu_percent.at("total").size(), width * 2)) cpu.cpu_percent.at("total").pop_front(); + + if (Config::getB("show_cpu_freq")) { + auto hz = get_cpuHz(); + if (hz != "") { + cpuHz = hz; + } + } + + if (Config::getB("check_temp") and got_sensors) + update_sensors(); + + if (Config::getB("show_battery") and has_battery) + current_bat = get_battery(); + + return cpu; + } +} // namespace Cpu + +namespace Mem { + bool has_swap = false; + vector fstab; + fs::file_time_type fstab_time; + int disk_ios = 0; + vector last_found; + + mem_info current_mem{}; + + uint64_t get_totalMem() { + return Shared::totalMem; + } + + void assign_values(struct disk_info& disk, int64_t readBytes, int64_t writeBytes) { + disk_ios++; + if (disk.io_read.empty()) { + disk.io_read.push_back(0); + } else { + disk.io_read.push_back(max((int64_t)0, (readBytes - disk.old_io.at(0)))); + } + disk.old_io.at(0) = readBytes; + while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); + + if (disk.io_write.empty()) { + disk.io_write.push_back(0); + } else { + disk.io_write.push_back(max((int64_t)0, (writeBytes - disk.old_io.at(1)))); + } + disk.old_io.at(1) = writeBytes; + while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); + + // no io times - need to push something anyway or we'll get an ABORT + if (disk.io_activity.empty()) + disk.io_activity.push_back(0); + else + disk.io_activity.push_back(clamp((long)round((double)(disk.io_write.back() + disk.io_read.back()) / (1 << 20)), 0l, 100l)); + while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); + } + + class PipeWrapper { + public: + PipeWrapper(const char *file, const char *mode) {fd = popen(file, mode);} + virtual ~PipeWrapper() {if (fd) pclose(fd);} + auto operator()() -> FILE* { return fd;}; + private: + FILE *fd; + }; + + // find all zpools in the system. Do this only at startup. + void get_zpools() { + PipeWrapper poolPipe = PipeWrapper("zpool list -H -o name", "r"); + while (not std::feof(poolPipe())) { + char poolName[512]; + size_t len = 512; + if (fgets(poolName, len, poolPipe())) { + poolName[strcspn(poolName, "\n")] = 0; + Logger::debug("zpool found: " + string(poolName)); + Mem::zpools.push_back(poolName); + } + } + } + + void collect_disk(unordered_flat_map &disks, unordered_flat_map &mapping) { + // this bit is for 'regular' mounts + static struct statinfo cur; + long double etime = 0; + uint64_t total_bytes_read; + uint64_t total_bytes_write; + + static std::unique_ptr curDevInfo (reinterpret_cast(std::calloc(1, sizeof(struct devinfo))), std::free); + cur.dinfo = curDevInfo.get(); + + if (devstat_getdevs(NULL, &cur) != -1) { + for (int i = 0; i < cur.dinfo->numdevs; i++) { + auto d = cur.dinfo->devices[i]; + string devStatName = "/dev/" + string(d.device_name) + std::to_string(d.unit_number); + for (auto& [ignored, disk] : disks) { // find matching mountpoints - could be multiple as d.device_name is only ada (and d.unit_number is the device number), while the disk.dev is like /dev/ada0s1 + if (disk.dev.string().rfind(devStatName, 0) == 0) { + devstat_compute_statistics(&d, NULL, etime, DSM_TOTAL_BYTES_READ, &total_bytes_read, DSM_TOTAL_BYTES_WRITE, &total_bytes_write, DSM_NONE); + assign_values(disk, total_bytes_read, total_bytes_write); + string mountpoint = mapping.at(disk.dev); + Logger::debug("dev " + devStatName + " -> " + mountpoint + " read=" + std::to_string(total_bytes_read) + " write=" + std::to_string(total_bytes_write)); + } + } + + } + Logger::debug(""); + } + + // this code is for ZFS mounts + for (string poolName : Mem::zpools) { + char sysCtl[1024]; + snprintf(sysCtl, sizeof(sysCtl), "sysctl kstat.zfs.%s.dataset | egrep \'dataset_name|nread|nwritten\'", poolName.c_str()); + PipeWrapper f = PipeWrapper(sysCtl, "r"); + if (f()) { + char buf[512]; + size_t len = 512; + while (not std::feof(f())) { + uint64_t nread = 0, nwritten = 0; + if (fgets(buf, len, f())) { + char *name = std::strtok(buf, ": \n"); + char *value = std::strtok(NULL, ": \n"); + if (string(name).find("dataset_name") != string::npos) { + // create entry if datasetname matches with anything in mapping + // relies on the fact that the dataset name is last value in the list + // alternatively you could parse the objset-0x... when this changes, you have a new entry + string datasetname = string(value);// this is the zfs volume, like 'zroot/usr/home' -> this maps onto the device we get back from getmntinfo(3) + if (mapping.contains(datasetname)) { + string mountpoint = mapping.at(datasetname); + if (disks.contains(mountpoint)) { + auto& disk = disks.at(mountpoint); + assign_values(disk, nread, nwritten); + } + } + } else if (string(name).find("nread") != string::npos) { + nread = atoll(value); + } else if (string(name).find("nwritten") != string::npos) { + nwritten = atoll(value); + } + } + } + } + } + + } + + auto collect(const bool no_update) -> mem_info & { + if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty())) + return current_mem; + + auto &show_swap = Config::getB("show_swap"); + auto &show_disks = Config::getB("show_disks"); + auto &swap_disk = Config::getB("swap_disk"); + auto &mem = current_mem; + static const bool snapped = (getenv("BTOP_SNAPPED") != NULL); + + int mib[4]; + u_int memActive, memWire, cachedMem, freeMem; + size_t len; + + len = 4; sysctlnametomib("vm.stats.vm.v_active_count", mib, &len); + len = sizeof(memActive); + sysctl(mib, 4, &(memActive), &len, NULL, 0); + memActive *= Shared::pageSize; + + len = 4; sysctlnametomib("vm.stats.vm.v_wire_count", mib, &len); + len = sizeof(memWire); + sysctl(mib, 4, &(memWire), &len, NULL, 0); + memWire *= Shared::pageSize; + + mem.stats.at("used") = memWire + memActive; + mem.stats.at("available") = Shared::totalMem - memActive - memWire; + + len = sizeof(cachedMem); + len = 4; sysctlnametomib("vm.stats.vm.v_cache_count", mib, &len); + sysctl(mib, 4, &(cachedMem), &len, NULL, 0); + cachedMem *= Shared::pageSize; + mem.stats.at("cached") = cachedMem; + + len = sizeof(freeMem); + len = 4; sysctlnametomib("vm.stats.vm.v_free_count", mib, &len); + sysctl(mib, 4, &(freeMem), &len, NULL, 0); + freeMem *= Shared::pageSize; + mem.stats.at("free") = freeMem; + + if (show_swap and mem.stats.at("swap_total") > 0) { + for (const auto &name : swap_names) { + mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / mem.stats.at("swap_total"))); + while (cmp_greater(mem.percent.at(name).size(), width * 2)) + mem.percent.at(name).pop_front(); + } + has_swap = true; + } else + has_swap = false; + //? Calculate percentages + for (const auto &name : mem_names) { + mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem)); + while (cmp_greater(mem.percent.at(name).size(), width * 2)) + mem.percent.at(name).pop_front(); + } + + if (show_disks) { + unordered_flat_map mapping; // keep mapping from device -> mountpoint, since IOKit doesn't give us the mountpoint + double uptime = system_uptime(); + auto &disks_filter = Config::getS("disks_filter"); + bool filter_exclude = false; + // auto &only_physical = Config::getB("only_physical"); + auto &disks = mem.disks; + vector filter; + if (not disks_filter.empty()) { + filter = ssplit(disks_filter); + if (filter.at(0).starts_with("exclude=")) { + filter_exclude = true; + filter.at(0) = filter.at(0).substr(8); + } + } + + struct statfs *stfs; + int count = getmntinfo(&stfs, MNT_WAIT); + vector found; + found.reserve(last_found.size()); + for (int i = 0; i < count; i++) { + auto fstype = string(stfs[i].f_fstypename); + if (fstype == "autofs" || fstype == "devfs" || fstype == "linprocfs" || fstype == "procfs" || fstype == "tmpfs" || fstype == "linsysfs" || + fstype == "fdesckfs") { + // in memory filesystems -> not useful to show + continue; + } + + std::error_code ec; + string mountpoint = stfs[i].f_mntonname; + string dev = stfs[i].f_mntfromname; + mapping[dev] = mountpoint; + + //? Match filter if not empty + if (not filter.empty()) { + bool match = v_contains(filter, mountpoint); + if ((filter_exclude and match) or (not filter_exclude and not match)) + continue; + } + + found.push_back(mountpoint); + if (not disks.contains(mountpoint)) { + disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; + + if (disks.at(mountpoint).dev.empty()) + disks.at(mountpoint).dev = dev; + + if (disks.at(mountpoint).name.empty()) + disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint); + } + + + if (not v_contains(last_found, mountpoint)) + redraw = true; + + disks.at(mountpoint).free = stfs[i].f_bfree; + disks.at(mountpoint).total = stfs[i].f_iosize; + } + + //? Remove disks no longer mounted or filtered out + if (swap_disk and has_swap) found.push_back("swap"); + for (auto it = disks.begin(); it != disks.end();) { + if (not v_contains(found, it->first)) + it = disks.erase(it); + else + it++; + } + if (found.size() != last_found.size()) redraw = true; + last_found = std::move(found); + + //? Get disk/partition stats + for (auto &[mountpoint, disk] : disks) { + if (std::error_code ec; not fs::exists(mountpoint, ec)) + continue; + struct statvfs vfs; + if (statvfs(mountpoint.c_str(), &vfs) < 0) { + Logger::warning("Failed to get disk/partition stats with statvfs() for: " + mountpoint); + continue; + } + disk.total = vfs.f_blocks * vfs.f_frsize; + disk.free = vfs.f_bfree * vfs.f_frsize; + disk.used = disk.total - disk.free; + disk.used_percent = round((double)disk.used * 100 / disk.total); + disk.free_percent = 100 - disk.used_percent; + } + + //? Setup disks order in UI and add swap if enabled + mem.disks_order.clear(); + if (snapped and disks.contains("/mnt")) + mem.disks_order.push_back("/mnt"); + else if (disks.contains("/")) + mem.disks_order.push_back("/"); + if (swap_disk and has_swap) { + mem.disks_order.push_back("swap"); + if (not disks.contains("swap")) + disks["swap"] = {"", "swap"}; + disks.at("swap").total = mem.stats.at("swap_total"); + disks.at("swap").used = mem.stats.at("swap_used"); + disks.at("swap").free = mem.stats.at("swap_free"); + disks.at("swap").used_percent = mem.percent.at("swap_used").back(); + disks.at("swap").free_percent = mem.percent.at("swap_free").back(); + } + for (const auto &name : last_found) + if (not is_in(name, "/", "swap", "/dev")) + mem.disks_order.push_back(name); + + disk_ios = 0; + collect_disk(disks, mapping); + + old_uptime = uptime; + } + return mem; + } + +} // namespace Mem + +namespace Net { + unordered_flat_map current_net; + net_info empty_net = {}; + vector interfaces; + string selected_iface; + int errors = 0; + unordered_flat_map graph_max = {{"download", {}}, {"upload", {}}}; + unordered_flat_map> max_count = {{"download", {}}, {"upload", {}}}; + bool rescale = true; + uint64_t timestamp = 0; + + //* RAII wrapper for getifaddrs + class getifaddr_wrapper { + struct ifaddrs *ifaddr; + + public: + int status; + getifaddr_wrapper() { status = getifaddrs(&ifaddr); } + ~getifaddr_wrapper() { freeifaddrs(ifaddr); } + auto operator()() -> struct ifaddrs * { return ifaddr; } + }; + + auto collect(const bool no_update) -> net_info & { + auto &net = current_net; + auto &config_iface = Config::getS("net_iface"); + auto &net_sync = Config::getB("net_sync"); + auto &net_auto = Config::getB("net_auto"); + auto new_timestamp = time_ms(); + + if (not no_update and errors < 3) { + //? Get interface list using getifaddrs() wrapper + getifaddr_wrapper if_wrap{}; + if (if_wrap.status != 0) { + errors++; + Logger::error("Net::collect() -> getifaddrs() failed with id " + to_string(if_wrap.status)); + redraw = true; + return empty_net; + } + int family = 0; + char ip[NI_MAXHOST]; + interfaces.clear(); + string ipv4, ipv6; + + //? Iteration over all items in getifaddrs() list + for (auto *ifa = if_wrap(); ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL) continue; + family = ifa->ifa_addr->sa_family; + const auto &iface = ifa->ifa_name; + //? Get IPv4 address + if (family == AF_INET) { + if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) + net[iface].ipv4 = ip; + } + //? Get IPv6 address + // else if (family == AF_INET6) { + // if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) + // net[iface].ipv6 = ip; + // } + + //? Update available interfaces vector and get status of interface + if (not v_contains(interfaces, iface)) { + interfaces.push_back(iface); + net[iface].connected = (ifa->ifa_flags & IFF_RUNNING); + } + } + + unordered_flat_map> ifstats; + int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, 0}; + size_t len; + if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { + Logger::error("failed getting network interfaces"); + } else { + std::unique_ptr buf(new char[len]); + if (sysctl(mib, 6, buf.get(), &len, NULL, 0) < 0) { + Logger::error("failed getting network interfaces"); + } else { + char *lim = buf.get() + len; + char *next = NULL; + for (next = buf.get(); next < lim;) { + struct if_msghdr *ifm = (struct if_msghdr *)next; + next += ifm->ifm_msglen; + struct if_data ifm_data = ifm->ifm_data; + if (ifm->ifm_addrs & RTA_IFP) { + struct sockaddr_dl *sdl = (struct sockaddr_dl *)(ifm + 1); + char iface[32]; + strncpy(iface, sdl->sdl_data, sdl->sdl_nlen); + iface[sdl->sdl_nlen] = 0; + ifstats[iface] = std::tuple(ifm_data.ifi_ibytes, ifm_data.ifi_obytes); + } + } + } + } + + //? Get total recieved and transmitted bytes + device address if no ip was found + for (const auto &iface : interfaces) { + for (const string dir : {"download", "upload"}) { + auto &saved_stat = net.at(iface).stat.at(dir); + auto &bandwidth = net.at(iface).bandwidth.at(dir); + uint64_t val = dir == "download" ? std::get<0>(ifstats[iface]) : std::get<1>(ifstats[iface]); + + //? Update speed, total and top values + if (val < saved_stat.last) { + saved_stat.rollover += saved_stat.last; + saved_stat.last = 0; + } + if (cmp_greater((unsigned long long)saved_stat.rollover + (unsigned long long)val, numeric_limits::max())) { + saved_stat.rollover = 0; + saved_stat.last = 0; + } + saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000)); + if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed; + if (saved_stat.offset > val + saved_stat.rollover) saved_stat.offset = 0; + saved_stat.total = (val + saved_stat.rollover) - saved_stat.offset; + saved_stat.last = val; + + //? Add values to graph + bandwidth.push_back(saved_stat.speed); + while (cmp_greater(bandwidth.size(), width * 2)) bandwidth.pop_front(); + + //? Set counters for auto scaling + if (net_auto and selected_iface == iface) { + if (saved_stat.speed > graph_max[dir]) { + ++max_count[dir][0]; + if (max_count[dir][1] > 0) --max_count[dir][1]; + } else if (graph_max[dir] > 10 << 10 and saved_stat.speed < graph_max[dir] / 10) { + ++max_count[dir][1]; + if (max_count[dir][0] > 0) --max_count[dir][0]; + } + } + } + } + + //? Clean up net map if needed + if (net.size() > interfaces.size()) { + for (auto it = net.begin(); it != net.end();) { + if (not v_contains(interfaces, it->first)) + it = net.erase(it); + else + it++; + } + net.compact(); + } + + timestamp = new_timestamp; + } + //? Return empty net_info struct if no interfaces was found + if (net.empty()) + return empty_net; + + //? Find an interface to display if selected isn't set or valid + if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) { + max_count["download"][0] = max_count["download"][1] = max_count["upload"][0] = max_count["upload"][1] = 0; + redraw = true; + if (net_auto) rescale = true; + if (not config_iface.empty() and v_contains(interfaces, config_iface)) + selected_iface = config_iface; + else { + //? Sort interfaces by total upload + download bytes + auto sorted_interfaces = interfaces; + rng::sort(sorted_interfaces, [&](const auto &a, const auto &b) { + return cmp_greater(net.at(a).stat["download"].total + net.at(a).stat["upload"].total, + net.at(b).stat["download"].total + net.at(b).stat["upload"].total); + }); + selected_iface.clear(); + //? Try to set to a connected interface + for (const auto &iface : sorted_interfaces) { + if (net.at(iface).connected) selected_iface = iface; + break; + } + //? If no interface is connected set to first available + if (selected_iface.empty() and not sorted_interfaces.empty()) + selected_iface = sorted_interfaces.at(0); + else if (sorted_interfaces.empty()) + return empty_net; + } + } + + //? Calculate max scale for graphs if needed + if (net_auto) { + bool sync = false; + for (const auto &dir : {"download", "upload"}) { + for (const auto &sel : {0, 1}) { + if (rescale or max_count[dir][sel] >= 5) { + const uint64_t avg_speed = (net[selected_iface].bandwidth[dir].size() > 5 + ? std::accumulate(net.at(selected_iface).bandwidth.at(dir).rbegin(), net.at(selected_iface).bandwidth.at(dir).rbegin() + 5, 0) / 5 + : net[selected_iface].stat[dir].speed); + graph_max[dir] = max(uint64_t(avg_speed * (sel == 0 ? 1.3 : 3.0)), (uint64_t)10 << 10); + max_count[dir][0] = max_count[dir][1] = 0; + redraw = true; + if (net_sync) sync = true; + break; + } + } + //? Sync download/upload graphs if enabled + if (sync) { + const auto other = (string(dir) == "upload" ? "download" : "upload"); + graph_max[other] = graph_max[dir]; + max_count[other][0] = max_count[other][1] = 0; + break; + } + } + } + + rescale = false; + return net.at(selected_iface); + } +} // namespace Net + +namespace Proc { + + vector current_procs; + unordered_flat_map uid_user; + string current_sort; + string current_filter; + bool current_rev = false; + + fs::file_time_type passwd_time; + + uint64_t cputimes; + int collapse = -1, expand = -1; + uint64_t old_cputimes = 0; + atomic numpids = 0; + int filter_found = 0; + + detail_container detailed; + + string get_status(char s) { + if (s & SRUN) return "Running"; + if (s & SSLEEP) return "Sleeping"; + if (s & SIDL) return "Idle"; + if (s & SSTOP) return "Stopped"; + if (s & SZOMB) return "Zombie"; + return "Unknown"; + } + + //* Get detailed info for selected process + void _collect_details(const size_t pid, vector &procs) { + if (pid != detailed.last_pid) { + detailed = {}; + detailed.last_pid = pid; + detailed.skip_smaps = not Config::getB("proc_info_smaps"); + } + + //? Copy proc_info for process from proc vector + auto p_info = rng::find(procs, pid, &proc_info::pid); + detailed.entry = *p_info; + + //? Update cpu percent deque for process cpu graph + if (not Config::getB("proc_per_core")) detailed.entry.cpu_p *= Shared::coreCount; + detailed.cpu_percent.push_back(clamp((long long)round(detailed.entry.cpu_p), 0ll, 100ll)); + while (cmp_greater(detailed.cpu_percent.size(), width)) detailed.cpu_percent.pop_front(); + + //? Process runtime : current time - start time (both in unix time - seconds since epoch) + struct timeval currentTime; + gettimeofday(¤tTime, NULL); + detailed.elapsed = sec_to_dhms(currentTime.tv_sec - detailed.entry.cpu_s); // only interested in second granularity, so ignoring tc_usec + if (detailed.elapsed.size() > 8) detailed.elapsed.resize(detailed.elapsed.size() - 3); + + //? Get parent process name + if (detailed.parent.empty()) { + auto p_entry = rng::find(procs, detailed.entry.ppid, &proc_info::pid); + if (p_entry != procs.end()) detailed.parent = p_entry->name; + } + + //? Expand process status from single char to explanative string + detailed.status = get_status(detailed.entry.state); + + detailed.mem_bytes.push_back(detailed.entry.mem); + detailed.memory = floating_humanizer(detailed.entry.mem); + + if (detailed.first_mem == -1 or detailed.first_mem < detailed.mem_bytes.back() / 2 or detailed.first_mem > detailed.mem_bytes.back() * 4) { + detailed.first_mem = min((uint64_t)detailed.mem_bytes.back() * 2, Mem::get_totalMem()); + redraw = true; + } + + while (cmp_greater(detailed.mem_bytes.size(), width)) detailed.mem_bytes.pop_front(); + + // rusage_info_current rusage; + // if (proc_pid_rusage(pid, RUSAGE_INFO_CURRENT, (void **)&rusage) == 0) { + // // this fails for processes we don't own - same as in Linux + // detailed.io_read = floating_humanizer(rusage.ri_diskio_bytesread); + // detailed.io_write = floating_humanizer(rusage.ri_diskio_byteswritten); + // } + } + + //* RAII wrapper for kvm_openfiles + class kvm_openfiles_wrapper { + kvm_t* kd = NULL; + public: + kvm_openfiles_wrapper(const char* execf, const char* coref, const char* swapf, int flags, char* err) { + this->kd = kvm_openfiles(execf, coref, swapf, flags, err); + } + ~kvm_openfiles_wrapper() { kvm_close(kd); } + auto operator()() -> kvm_t* { return kd; } + }; + + //* Collects and sorts process information from /proc + auto collect(const bool no_update) -> vector & { + const auto &sorting = Config::getS("proc_sorting"); + const auto &reverse = Config::getB("proc_reversed"); + const auto &filter = Config::getS("proc_filter"); + const auto &per_core = Config::getB("proc_per_core"); + const auto &tree = Config::getB("proc_tree"); + const auto &show_detailed = Config::getB("show_detailed"); + const size_t detailed_pid = Config::getI("detailed_pid"); + bool should_filter = current_filter != filter; + if (should_filter) current_filter = filter; + const bool sorted_change = (sorting != current_sort or reverse != current_rev or should_filter); + if (sorted_change) { + current_sort = sorting; + current_rev = reverse; + } + + const int cmult = (per_core) ? Shared::coreCount : 1; + bool got_detailed = false; + + static vector found; + + vector> cpu_time(Shared::coreCount); + size_t size = sizeof(long) * CPUSTATES * Shared::coreCount; + if (sysctlbyname("kern.cp_times", &cpu_time[0], &size, NULL, 0) == -1) { + Logger::error("failed to get CPU times"); + } + cputimes = 0; + for (const auto core : cpu_time) { + for (const unsigned int c_state : {CP_USER, CP_NICE, CP_SYS, CP_IDLE}) { + cputimes += core[c_state]; + } + } + + //* Use pids from last update if only changing filter, sorting or tree options + if (no_update and not current_procs.empty()) { + if (show_detailed and detailed_pid != detailed.last_pid) _collect_details(detailed_pid, current_procs); + } else { + //* ---------------------------------------------Collection start---------------------------------------------- + + should_filter = true; + found.clear(); + struct timeval currentTime; + gettimeofday(¤tTime, NULL); + const double timeNow = currentTime.tv_sec + (currentTime.tv_usec / 1'000'000); + + int count = 0; + char buf[_POSIX2_LINE_MAX]; + kvm_openfiles_wrapper kd(NULL, _PATH_DEVNULL, NULL, O_RDONLY, buf); + const struct kinfo_proc* kprocs = kvm_getprocs(kd(), KERN_PROC_PROC, 0, &count); + + for (int i = 0; i < count; i++) { + const struct kinfo_proc* kproc = &kprocs[i]; + const size_t pid = (size_t)kproc->ki_pid; + if (pid < 1) continue; + found.push_back(pid); + + //? Check if pid already exists in current_procs + bool no_cache = false; + auto find_old = rng::find(current_procs, pid, &proc_info::pid); + if (find_old == current_procs.end()) { + current_procs.push_back({pid}); + find_old = current_procs.end() - 1; + no_cache = true; + } + + auto &new_proc = *find_old; + + //? Get program name, command, username, parent pid, nice and status + if (no_cache) { + if (kproc->ki_comm == NULL or kproc->ki_comm == "idle"s) { + current_procs.pop_back(); + found.pop_back(); + continue; + } + new_proc.name = kproc->ki_comm; + char** argv = kvm_getargv(kd(), kproc, 0); + if (argv) { + for (int i = 0; argv[i] and cmp_less(new_proc.cmd.size(), 1000); i++) { + new_proc.cmd += argv[i] + " "s; + } + if (not new_proc.cmd.empty()) new_proc.cmd.pop_back(); + } + if (new_proc.cmd.empty()) new_proc.cmd = new_proc.name; + if (new_proc.cmd.size() > 1000) { + new_proc.cmd.resize(1000); + new_proc.cmd.shrink_to_fit(); + } + new_proc.ppid = kproc->ki_ppid; + new_proc.cpu_s = round(kproc->ki_start.tv_sec); + struct passwd *pwd = getpwuid(kproc->ki_uid); + if (pwd) + new_proc.user = pwd->pw_name; + } + new_proc.p_nice = kproc->ki_nice; + new_proc.state = kproc->ki_stat; + + int cpu_t = 0; + cpu_t = kproc->ki_rusage.ru_utime.tv_sec * 1'000'000 + kproc->ki_rusage.ru_utime.tv_usec + + kproc->ki_rusage.ru_stime.tv_sec * 1'000'000 + kproc->ki_rusage.ru_stime.tv_usec; + + new_proc.mem = kproc->ki_rssize * Shared::pageSize; + new_proc.threads = kproc->ki_numthreads; + + //? Process cpu usage since last update + new_proc.cpu_p = clamp((100.0 * kproc->ki_pctcpu / Shared::kfscale) * cmult, 0.0, 100.0 * Shared::coreCount); + + //? Process cumulative cpu usage since process start + new_proc.cpu_c = (double)(cpu_t * Shared::clkTck / 1'000'000) / max(1.0, timeNow - new_proc.cpu_s); + + //? Update cached value with latest cpu times + new_proc.cpu_t = cpu_t; + + if (show_detailed and not got_detailed and new_proc.pid == detailed_pid) { + got_detailed = true; + } + } + + //? Clear dead processes from current_procs + auto eraser = rng::remove_if(current_procs, [&](const auto &element) { return not v_contains(found, element.pid); }); + current_procs.erase(eraser.begin(), eraser.end()); + + //? Update the details info box for process if active + if (show_detailed and got_detailed) { + _collect_details(detailed_pid, current_procs); + } else if (show_detailed and not got_detailed and detailed.status != "Dead") { + detailed.status = "Dead"; + redraw = true; + } + + old_cputimes = cputimes; + + } + + //* ---------------------------------------------Collection done----------------------------------------------- + + //* Match filter if defined + if (should_filter) { + filter_found = 0; + for (auto& p : current_procs) { + if (not tree and not filter.empty()) { + if (not s_contains_ic(to_string(p.pid), filter) + and not s_contains_ic(p.name, filter) + and not s_contains_ic(p.cmd, filter) + and not s_contains_ic(p.user, filter)) { + p.filtered = true; + filter_found++; + } + else { + p.filtered = false; + } + } + else { + p.filtered = false; + } + } + } + + //* Sort processes + if (sorted_change or not no_update) { + proc_sorter(current_procs, sorting, reverse, tree); + } + + //* Generate tree view if enabled + if (tree and (not no_update or should_filter or sorted_change)) { + bool locate_selection = false; + if (auto find_pid = (collapse != -1 ? collapse : expand); find_pid != -1) { + auto collapser = rng::find(current_procs, find_pid, &proc_info::pid); + if (collapser != current_procs.end()) { + if (collapse == expand) { + collapser->collapsed = not collapser->collapsed; + } + else if (collapse > -1) { + collapser->collapsed = true; + } + else if (expand > -1) { + collapser->collapsed = false; + } + if (Config::ints.at("proc_selected") > 0) locate_selection = true; + } + collapse = expand = -1; + } + if (should_filter or not filter.empty()) filter_found = 0; + + vector tree_procs; + tree_procs.reserve(current_procs.size()); + + for (auto& p : current_procs) { + if (not v_contains(found, p.ppid)) p.ppid = 0; + } + + //? Stable sort to retain selected sorting among processes with the same parent + rng::stable_sort(current_procs, rng::less{}, & proc_info::ppid); + + //? Start recursive iteration over processes with the lowest shared parent pids + for (auto& p : rng::equal_range(current_procs, current_procs.at(0).ppid, rng::less{}, &proc_info::ppid)) { + _tree_gen(p, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter); + } + + //? Recursive sort over tree structure to account for collapsed processes in the tree + int index = 0; + tree_sort(tree_procs, sorting, reverse, index, current_procs.size()); + + //? Add tree begin symbol to first item if childless + if (tree_procs.front().children.empty()) + tree_procs.front().entry.get().prefix.replace(tree_procs.front().entry.get().prefix.size() - 8, 8, " ┌─ "); + + //? Add tree terminator symbol to last item if childless + if (tree_procs.back().children.empty()) + tree_procs.back().entry.get().prefix.replace(tree_procs.back().entry.get().prefix.size() - 8, 8, " └─ "); + + //? Final sort based on tree index + rng::sort(current_procs, rng::less{}, & proc_info::tree_index); + + //? Move current selection/view to the selected process when collapsing/expanding in the tree + if (locate_selection) { + int loc = rng::find(current_procs, Proc::selected_pid, &proc_info::pid)->tree_index; + if (Config::ints.at("proc_start") >= loc or Config::ints.at("proc_start") <= loc - Proc::select_max) + Config::ints.at("proc_start") = max(0, loc - 1); + Config::ints.at("proc_selected") = loc - Config::ints.at("proc_start") + 1; + } + } + + numpids = (int)current_procs.size() - filter_found; + return current_procs; + } +} // namespace Proc + +namespace Tools { + double system_uptime() { + struct timeval ts, currTime; + std::size_t len = sizeof(ts); + int mib[2] = {CTL_KERN, KERN_BOOTTIME}; + if (sysctl(mib, 2, &ts, &len, NULL, 0) != -1) { + gettimeofday(&currTime, NULL); + return currTime.tv_sec - ts.tv_sec; + } + return 0.0; + } +} // namespace Tools diff --git a/src/linux/btop_collect.cpp b/src/linux/btop_collect.cpp index 3c8b9fe..86514a9 100644 --- a/src/linux/btop_collect.cpp +++ b/src/linux/btop_collect.cpp @@ -16,17 +16,21 @@ indent = tab tab-size = 4 */ +#include #include #include #include #include #include -#include #include #include #include #include +#if !(defined(STATIC_BUILD) && defined(__GLIBC__)) + #include +#endif + #include #include #include @@ -78,9 +82,7 @@ namespace Mem { namespace Shared { fs::path procPath, passwd_path; - uint64_t totalMem; long pageSize, clkTck, coreCount; - int totalMem_len; void init() { @@ -111,16 +113,6 @@ namespace Shared { Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect."); } - ifstream meminfo(Shared::procPath / "meminfo"); - if (meminfo.good()) { - meminfo.ignore(SSmax, ':'); - meminfo >> totalMem; - totalMem_len = to_string(totalMem).size(); - totalMem <<= 10; - } - if (not meminfo.good() or totalMem == 0) - throw std::runtime_error("Could not get total memory size from /proc/meminfo"); - //? Init for namespace Cpu if (not fs::exists(Cpu::freq_path) or access(Cpu::freq_path.c_str(), R_OK) == -1) Cpu::freq_path.clear(); Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {}); @@ -225,9 +217,9 @@ namespace Cpu { name += n + ' '; } name.pop_back(); - for (const auto& reg : {regex("Processor"), regex("CPU"), regex("\\(R\\)"), regex("\\(TM\\)"), regex("Intel"), - regex("AMD"), regex("Core"), regex("\\d?\\.?\\d+[mMgG][hH][zZ]")}) { - name = std::regex_replace(name, reg, ""); + for (const auto& replace : {"Processor", "CPU", "(R)", "(TM)", "Intel", "AMD", "Core"}) { + name = s_replace(name, replace, ""); + name = s_replace(name, " ", " "); } name = trim(name); } @@ -249,20 +241,36 @@ namespace Cpu { if (s_contains(add_path, "coretemp")) got_coretemp = true; - if (fs::exists(add_path / "temp1_input")) { - search_paths.push_back(add_path); + for (const auto & file : fs::directory_iterator(add_path)) { + if (string(file.path().filename()) == "device") { + for (const auto & dev_file : fs::directory_iterator(file.path())) { + string dev_filename = dev_file.path().filename(); + if (dev_filename.starts_with("temp") and dev_filename.ends_with("_input")) { + search_paths.push_back(file.path()); + break; + } + } + } + + string filename = file.path().filename(); + if (filename.starts_with("temp") and filename.ends_with("_input")) { + search_paths.push_back(add_path); + break; + } } - else if (fs::exists(add_path / "device/temp1_input")) - search_paths.push_back(add_path / "device"); } } if (not got_coretemp and fs::exists(fs::path("/sys/devices/platform/coretemp.0/hwmon"))) { for (auto& d : fs::directory_iterator(fs::path("/sys/devices/platform/coretemp.0/hwmon"))) { fs::path add_path = fs::canonical(d.path()); - if (fs::exists(d.path() / "temp1_input") and not v_contains(search_paths, add_path)) { - search_paths.push_back(add_path); - got_coretemp = true; + for (const auto & file : fs::directory_iterator(add_path)) { + string filename = file.path().filename(); + if (filename.starts_with("temp") and filename.ends_with("_input") and not v_contains(search_paths, add_path)) { + search_paths.push_back(add_path); + got_coretemp = true; + break; + } } } } @@ -270,9 +278,17 @@ namespace Cpu { if (not search_paths.empty()) { for (const auto& path : search_paths) { const string pname = readfile(path / "name", path.filename()); - for (int i = 1; fs::exists(path / string("temp" + to_string(i) + "_input")); i++) { - const string basepath = path / string("temp" + to_string(i) + "_"); - const string label = readfile(fs::path(basepath + "label"), "temp" + to_string(i)); + for (const auto & file : fs::directory_iterator(path)) { + const string file_suffix = "input"; + const int file_id = atoi(file.path().filename().c_str() + 4); // skip "temp" prefix + string file_path = file.path(); + + if (!s_contains(file_path, file_suffix)) { + continue; + } + + const string basepath = file_path.erase(file_path.find(file_suffix), file_suffix.length()); + const string label = readfile(fs::path(basepath + "label"), "temp" + to_string(file_id)); const string sensor_name = pname + "/" + label; const int64_t temp = stol(readfile(fs::path(basepath + "input"), "0")) / 1000; const int64_t high = stol(readfile(fs::path(basepath + "max"), "80000")) / 1000; @@ -318,10 +334,19 @@ namespace Cpu { } catch (...) {} - if (not got_coretemp or core_sensors.empty()) cpu_temp_only = true; + if (not got_coretemp or core_sensors.empty()) { + cpu_temp_only = true; + } + else { + rng::sort(core_sensors, rng::less{}); + rng::stable_sort(core_sensors, [](const auto& a, const auto& b){ + return a.size() < b.size(); + }); + } + if (cpu_sensor.empty() and not found_sensors.empty()) { for (const auto& [name, sensor] : found_sensors) { - if (s_contains(str_to_lower(name), "cpu")) { + if (s_contains(str_to_lower(name), "cpu") or s_contains(str_to_lower(name), "k10temp")) { cpu_sensor = name; break; } @@ -476,61 +501,104 @@ namespace Cpu { return core_map; } + struct battery { + fs::path base_dir, energy_now, energy_full, power_now, status, online; + string device_type; + bool use_energy = true; + }; + auto get_battery() -> tuple { if (not has_battery) return {0, 0, ""}; - static fs::path bat_dir, energy_now_path, energy_full_path, power_now_path, status_path, online_path; - static bool use_energy = true; + static string auto_sel; + static unordered_flat_map batteries; //? Get paths to needed files and check for valid values on first run - if (bat_dir.empty() and has_battery) { - if (fs::exists("/sys/class/power_supply")) { - for (const auto& d : fs::directory_iterator("/sys/class/power_supply")) { - if (const string dir_name = d.path().filename(); d.is_directory() and (dir_name.starts_with("BAT") or s_contains(str_to_lower(dir_name), "battery"))) { - bat_dir = d.path(); - break; + if (batteries.empty() and has_battery) { + try { + if (fs::exists("/sys/class/power_supply")) { + for (const auto& d : fs::directory_iterator("/sys/class/power_supply")) { + //? Only consider online power supplies of type Battery or UPS + //? see kernel docs for details on the file structure and contents + //? https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power + battery new_bat; + fs::path bat_dir; + try { + if (not d.is_directory() + or not fs::exists(d.path() / "type") + or not fs::exists(d.path() / "present") + or stoi(readfile(d.path() / "present")) != 1) + continue; + string dev_type = readfile(d.path() / "type"); + if (is_in(dev_type, "Battery", "UPS")) { + bat_dir = d.path(); + new_bat.base_dir = d.path(); + new_bat.device_type = dev_type; + } + } catch (...) { + //? skip power supplies not conforming to the kernel standard + continue; + } + + if (fs::exists(bat_dir / "energy_now")) new_bat.energy_now = bat_dir / "energy_now"; + else if (fs::exists(bat_dir / "charge_now")) new_bat.energy_now = bat_dir / "charge_now"; + else new_bat.use_energy = false; + + if (fs::exists(bat_dir / "energy_full")) new_bat.energy_full = bat_dir / "energy_full"; + else if (fs::exists(bat_dir / "charge_full")) new_bat.energy_full = bat_dir / "charge_full"; + else new_bat.use_energy = false; + + if (not new_bat.use_energy and not fs::exists(bat_dir / "capacity")) { + continue; + } + + if (fs::exists(bat_dir / "power_now")) new_bat.power_now = bat_dir / "power_now"; + else if (fs::exists(bat_dir / "current_now")) new_bat.power_now = bat_dir / "current_now"; + + if (fs::exists(bat_dir / "AC0/online")) new_bat.online = bat_dir / "AC0/online"; + else if (fs::exists(bat_dir / "AC/online")) new_bat.online = bat_dir / "AC/online"; + + batteries[bat_dir.filename()] = new_bat; + Config::available_batteries.push_back(bat_dir.filename()); } } } - if (bat_dir.empty()) { + catch (...) { + batteries.clear(); + } + if (batteries.empty()) { has_battery = false; return {0, 0, ""}; } - else { - if (fs::exists(bat_dir / "energy_now")) energy_now_path = bat_dir / "energy_now"; - else if (fs::exists(bat_dir / "charge_now")) energy_now_path = bat_dir / "charge_now"; - else use_energy = false; - - if (fs::exists(bat_dir / "energy_full")) energy_full_path = bat_dir / "energy_full"; - else if (fs::exists(bat_dir / "charge_full")) energy_full_path = bat_dir / "charge_full"; - else use_energy = false; - - if (not use_energy and not fs::exists(bat_dir / "capacity")) { - has_battery = false; - return {0, 0, ""}; - } - - if (fs::exists(bat_dir / "power_now")) power_now_path = bat_dir / "power_now"; - else if (fs::exists(bat_dir / "current_now")) power_now_path = bat_dir / "current_now"; - - if (fs::exists(bat_dir / "AC0/online")) online_path = bat_dir / "AC0/online"; - else if (fs::exists(bat_dir / "AC/online")) online_path = bat_dir / "AC/online"; - } } + auto& battery_sel = Config::getS("selected_battery"); + + if (auto_sel.empty()) { + for (auto& [name, bat] : batteries) { + if (bat.device_type == "Battery") { + auto_sel = name; + break; + } + } + if (auto_sel.empty()) auto_sel = batteries.begin()->first; + } + + auto& b = (battery_sel != "Auto" and batteries.contains(battery_sel) ? batteries.at(battery_sel) : batteries.at(auto_sel)); + int percent = -1; long seconds = -1; //? Try to get battery percentage - if (use_energy) { + if (b.use_energy) { try { - percent = round(100.0 * stoll(readfile(energy_now_path, "-1")) / stoll(readfile(energy_full_path, "1"))); + percent = round(100.0 * stoll(readfile(b.energy_now, "-1")) / stoll(readfile(b.energy_full, "1"))); } catch (const std::invalid_argument&) { } catch (const std::out_of_range&) { } } if (percent < 0) { try { - percent = stoll(readfile(bat_dir / "capacity", "-1")); + percent = stoll(readfile(b.base_dir / "capacity", "-1")); } catch (const std::invalid_argument&) { } catch (const std::out_of_range&) { } @@ -541,9 +609,9 @@ namespace Cpu { } //? Get charging/discharging status - string status = str_to_lower(readfile(bat_dir / "status", "unknown")); - if (status == "unknown" and not online_path.empty()) { - const auto online = readfile(online_path, "0"); + string status = str_to_lower(readfile(b.base_dir / "status", "unknown")); + if (status == "unknown" and not b.online.empty()) { + const auto online = readfile(b.online, "0"); if (online == "1" and percent < 100) status = "charging"; else if (online == "1") status = "full"; else status = "discharging"; @@ -551,16 +619,16 @@ namespace Cpu { //? Get seconds to empty if (not is_in(status, "charging", "full")) { - if (use_energy and not power_now_path.empty()) { + if (b.use_energy and not b.power_now.empty()) { try { - seconds = round((double)stoll(readfile(energy_now_path, "0")) / stoll(readfile(power_now_path, "1")) * 3600); + seconds = round((double)stoll(readfile(b.energy_now, "0")) / stoll(readfile(b.power_now, "1")) * 3600); } catch (const std::invalid_argument&) { } catch (const std::out_of_range&) { } } - if (seconds < 0 and fs::exists(bat_dir / "time_to_empty")) { + if (seconds < 0 and fs::exists(b.base_dir / "time_to_empty")) { try { - seconds = stoll(readfile(bat_dir / "time_to_empty", "0")) * 60; + seconds = stoll(readfile(b.base_dir / "time_to_empty", "0")) * 60; } catch (const std::invalid_argument&) { } catch (const std::out_of_range&) { } @@ -670,23 +738,62 @@ namespace Mem { fs::file_time_type fstab_time; int disk_ios = 0; vector last_found; + const std::regex zfs_size_regex("^size\\s+\\d\\s+(\\d+)"); + + //?* Find the filepath to the specified ZFS object's stat file + fs::path get_zfs_stat_file(const string& device_name, size_t dataset_name_start, bool zfs_hide_datasets); + + //?* Collect total ZFS pool io stats + bool zfs_collect_pool_total_stats(struct disk_info &disk); mem_info current_mem {}; + uint64_t get_totalMem() { + ifstream meminfo(Shared::procPath / "meminfo"); + int64_t totalMem; + if (meminfo.good()) { + meminfo.ignore(SSmax, ':'); + meminfo >> totalMem; + totalMem <<= 10; + } + if (not meminfo.good() or totalMem == 0) + throw std::runtime_error("Could not get total memory size from /proc/meminfo"); + + return totalMem; + } + auto collect(const bool no_update) -> mem_info& { if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty())) return current_mem; auto& show_swap = Config::getB("show_swap"); auto& swap_disk = Config::getB("swap_disk"); auto& show_disks = Config::getB("show_disks"); + auto& zfs_arc_cached = Config::getB("zfs_arc_cached"); + auto totalMem = get_totalMem(); auto& mem = current_mem; mem.stats.at("swap_total") = 0; + //? Read ZFS ARC info from /proc/spl/kstat/zfs/arcstats + uint64_t arc_size = 0; + if (zfs_arc_cached) { + ifstream arcstats(Shared::procPath / "spl/kstat/zfs/arcstats"); + if (arcstats.good()) { + std::string line; + while (std::getline(arcstats, line)) { + std::smatch match; + if (std::regex_match(line, match, zfs_size_regex) && match.size() == 2) { + arc_size = stoull(match.str(1)); + } + } + } + arcstats.close(); + } + //? Read memory info from /proc/meminfo ifstream meminfo(Shared::procPath / "meminfo"); if (meminfo.good()) { bool got_avail = false; - for (string label; meminfo >> label;) { + for (string label; meminfo.peek() != 'D' and meminfo >> label;) { if (label == "MemFree:") { meminfo >> mem.stats.at("free"); mem.stats.at("free") <<= 10; @@ -713,7 +820,11 @@ namespace Mem { meminfo.ignore(SSmax, '\n'); } if (not got_avail) mem.stats.at("available") = mem.stats.at("free") + mem.stats.at("cached"); - mem.stats.at("used") = Shared::totalMem - mem.stats.at("available"); + if (zfs_arc_cached) { + mem.stats.at("cached") += arc_size; + mem.stats.at("available") += arc_size; + } + mem.stats.at("used") = totalMem - mem.stats.at("available"); if (mem.stats.at("swap_total") > 0) mem.stats.at("swap_used") = mem.stats.at("swap_total") - mem.stats.at("swap_free"); } else @@ -723,7 +834,7 @@ namespace Mem { //? Calculate percentages for (const auto& name : mem_names) { - mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem)); + mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / totalMem)); while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); } @@ -740,11 +851,13 @@ namespace Mem { //? Get disks stats if (show_disks) { double uptime = system_uptime(); + auto free_priv = Config::getB("disk_free_priv"); try { auto& disks_filter = Config::getS("disks_filter"); bool filter_exclude = false; auto& use_fstab = Config::getB("use_fstab"); auto& only_physical = Config::getB("only_physical"); + auto& zfs_hide_datasets = Config::getB("zfs_hide_datasets"); auto& disks = mem.disks; ifstream diskread; @@ -783,7 +896,12 @@ namespace Mem { for (string instr; diskread >> instr;) { if (not instr.starts_with('#')) { diskread >> instr; - if (not is_in(instr, "none", "swap")) fstab.push_back(instr); + #ifdef SNAPPED + if (instr == "/") fstab.push_back("/mnt"); + else if (not is_in(instr, "none", "swap")) fstab.push_back(instr); + #else + if (not is_in(instr, "none", "swap")) fstab.push_back(instr); + #endif } diskread.ignore(SSmax, '\n'); } @@ -802,6 +920,9 @@ namespace Mem { while (not diskread.eof()) { std::error_code ec; diskread >> dev >> mountpoint >> fstype; + diskread.ignore(SSmax, '\n'); + + if (v_contains(found, mountpoint)) continue; //? Match filter if not empty if (not filter.empty()) { @@ -810,29 +931,55 @@ namespace Mem { continue; } + //? Skip ZFS datasets if zfs_hide_datasets option is enabled + size_t zfs_dataset_name_start = 0; + if (fstype == "zfs" && (zfs_dataset_name_start = dev.find('/')) != std::string::npos && zfs_hide_datasets) continue; + if ((not use_fstab and not only_physical) or (use_fstab and v_contains(fstab, mountpoint)) or (not use_fstab and only_physical and v_contains(fstypes, fstype))) { found.push_back(mountpoint); if (not v_contains(last_found, mountpoint)) redraw = true; - //? Save mountpoint, name, dev path and path to /sys/block stat file + //? Save mountpoint, name, fstype, dev path and path to /sys/block stat file if (not disks.contains(mountpoint)) { - disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; + disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename(), fstype}; if (disks.at(mountpoint).dev.empty()) disks.at(mountpoint).dev = dev; + #ifdef SNAPPED + if (mountpoint == "/mnt") disks.at(mountpoint).name = "root"; + #endif if (disks.at(mountpoint).name.empty()) disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint); string devname = disks.at(mountpoint).dev.filename(); + int c = 0; while (devname.size() >= 2) { if (fs::exists("/sys/block/" + devname + "/stat", ec) and access(string("/sys/block/" + devname + "/stat").c_str(), R_OK) == 0) { - disks.at(mountpoint).stat = "/sys/block/" + devname + "/stat"; + if (c > 0 and fs::exists("/sys/block/" + devname + '/' + disks.at(mountpoint).dev.filename().string() + "/stat", ec)) + disks.at(mountpoint).stat = "/sys/block/" + devname + '/' + disks.at(mountpoint).dev.filename().string() + "/stat"; + else + disks.at(mountpoint).stat = "/sys/block/" + devname + "/stat"; + break; + //? Set ZFS stat filepath + } else if (fstype == "zfs") { + disks.at(mountpoint).stat = get_zfs_stat_file(dev, zfs_dataset_name_start, zfs_hide_datasets); + if (disks.at(mountpoint).stat.empty()) { + Logger::debug("Failed to get ZFS stat file for device " + dev); + } break; } devname.resize(devname.size() - 1); + c++; } } + //? If zfs_hide_datasets option was switched, refresh stat filepath + if (fstype == "zfs" && ((zfs_hide_datasets && !is_directory(disks.at(mountpoint).stat)) + || (!zfs_hide_datasets && is_directory(disks.at(mountpoint).stat)))) { + disks.at(mountpoint).stat = get_zfs_stat_file(dev, zfs_dataset_name_start, zfs_hide_datasets); + if (disks.at(mountpoint).stat.empty()) { + Logger::debug("Failed to get ZFS stat file for device " + dev); + } + } } - diskread.ignore(SSmax, '\n'); } //? Remove disks no longer mounted or filtered out if (swap_disk and has_swap) found.push_back("swap"); @@ -852,13 +999,13 @@ namespace Mem { //? Get disk/partition stats for (auto& [mountpoint, disk] : disks) { if (std::error_code ec; not fs::exists(mountpoint, ec)) continue; - struct statvfs vfs; - if (statvfs(mountpoint.c_str(), &vfs) < 0) { + struct statvfs64 vfs; + if (statvfs64(mountpoint.c_str(), &vfs) < 0) { Logger::warning("Failed to get disk/partition stats with statvfs() for: " + mountpoint); continue; } disk.total = vfs.f_blocks * vfs.f_frsize; - disk.free = vfs.f_bfree * vfs.f_frsize; + disk.free = (free_priv ? vfs.f_bfree : vfs.f_bavail) * vfs.f_frsize; disk.used = disk.total - disk.free; disk.used_percent = round((double)disk.used * 100 / disk.total); disk.free_percent = 100 - disk.used_percent; @@ -866,10 +1013,14 @@ namespace Mem { //? Setup disks order in UI and add swap if enabled mem.disks_order.clear(); - if (disks.contains("/")) mem.disks_order.push_back("/"); + #ifdef SNAPPED + if (disks.contains("/mnt")) mem.disks_order.push_back("/mnt"); + #else + if (disks.contains("/")) mem.disks_order.push_back("/"); + #endif if (swap_disk and has_swap) { mem.disks_order.push_back("swap"); - if (not disks.contains("swap")) disks["swap"] = {"", "swap"}; + if (not disks.contains("swap")) disks["swap"] = {"", "swap", "swap"}; disks.at("swap").total = mem.stats.at("swap_total"); disks.at("swap").used = mem.stats.at("swap_used"); disks.at("swap").free = mem.stats.at("swap_free"); @@ -877,42 +1028,93 @@ namespace Mem { disks.at("swap").free_percent = mem.percent.at("swap_free").back(); } for (const auto& name : last_found) - if (not is_in(name, "/", "swap")) mem.disks_order.push_back(name); + #ifdef SNAPPED + if (not is_in(name, "/mnt", "swap")) mem.disks_order.push_back(name); + #else + if (not is_in(name, "/", "swap")) mem.disks_order.push_back(name); + #endif //? Get disks IO - int64_t sectors_read, sectors_write, io_ticks; + int64_t sectors_read, sectors_write, io_ticks, io_ticks_temp; disk_ios = 0; for (auto& [ignored, disk] : disks) { if (disk.stat.empty() or access(disk.stat.c_str(), R_OK) != 0) continue; + if (disk.fstype == "zfs" && zfs_hide_datasets && zfs_collect_pool_total_stats(disk)) { + disk_ios++; + continue; + } diskread.open(disk.stat); if (diskread.good()) { disk_ios++; - for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } - diskread >> sectors_read; - if (disk.io_read.empty()) - disk.io_read.push_back(0); - else - disk.io_read.push_back(max((int64_t)0, (sectors_read - disk.old_io.at(0)) * 512)); - disk.old_io.at(0) = sectors_read; - while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); + //? ZFS Pool Support + if (disk.fstype == "zfs") { + // skip first three lines + for (int i = 0; i < 3; i++) diskread.ignore(numeric_limits::max(), '\n'); + // skip characters until '4' is reached, indicating data type 4, next value will be out target + diskread.ignore(numeric_limits::max(), '4'); + diskread >> io_ticks; - for (int i = 0; i < 3; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } - diskread >> sectors_write; - if (disk.io_write.empty()) - disk.io_write.push_back(0); - else - disk.io_write.push_back(max((int64_t)0, (sectors_write - disk.old_io.at(1)) * 512)); - disk.old_io.at(1) = sectors_write; - while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); + // skip characters until '4' is reached, indicating data type 4, next value will be out target + diskread.ignore(numeric_limits::max(), '4'); + diskread >> sectors_write; // nbytes written + if (disk.io_write.empty()) + disk.io_write.push_back(0); + else + disk.io_write.push_back(max((int64_t)0, (sectors_write - disk.old_io.at(1)))); + disk.old_io.at(1) = sectors_write; + while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); - for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } - diskread >> io_ticks; - if (disk.io_activity.empty()) - disk.io_activity.push_back(0); - else - disk.io_activity.push_back(clamp((long)round((double)(io_ticks - disk.old_io.at(2)) / (uptime - old_uptime) / 10), 0l, 100l)); - disk.old_io.at(2) = io_ticks; - while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); + // skip characters until '4' is reached, indicating data type 4, next value will be out target + diskread.ignore(numeric_limits::max(), '4'); + diskread >> io_ticks_temp; + io_ticks += io_ticks_temp; + + // skip characters until '4' is reached, indicating data type 4, next value will be out target + diskread.ignore(numeric_limits::max(), '4'); + diskread >> sectors_read; // nbytes read + if (disk.io_read.empty()) + disk.io_read.push_back(0); + else + disk.io_read.push_back(max((int64_t)0, (sectors_read - disk.old_io.at(0)))); + disk.old_io.at(0) = sectors_read; + while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); + + if (disk.io_activity.empty()) + disk.io_activity.push_back(0); + else + disk.io_activity.push_back(max((int64_t)0, (io_ticks - disk.old_io.at(2)))); + disk.old_io.at(2) = io_ticks; + while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); + } else { + for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> sectors_read; + if (disk.io_read.empty()) + disk.io_read.push_back(0); + else + disk.io_read.push_back(max((int64_t)0, (sectors_read - disk.old_io.at(0)) * 512)); + disk.old_io.at(0) = sectors_read; + while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); + + for (int i = 0; i < 3; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> sectors_write; + if (disk.io_write.empty()) + disk.io_write.push_back(0); + else + disk.io_write.push_back(max((int64_t)0, (sectors_write - disk.old_io.at(1)) * 512)); + disk.old_io.at(1) = sectors_write; + while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); + + for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } + diskread >> io_ticks; + if (disk.io_activity.empty()) + disk.io_activity.push_back(0); + else + disk.io_activity.push_back(clamp((long)round((double)(io_ticks - disk.old_io.at(2)) / (uptime - old_uptime) / 10), 0l, 100l)); + disk.old_io.at(2) = io_ticks; + while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); + } + } else { + Logger::debug("Error in Mem::collect() : when opening " + (string)disk.stat); } diskread.close(); } @@ -926,6 +1128,129 @@ namespace Mem { return mem; } + fs::path get_zfs_stat_file(const string& device_name, size_t dataset_name_start, bool zfs_hide_datasets) { + fs::path zfs_pool_stat_path; + if (zfs_hide_datasets) { + zfs_pool_stat_path = Shared::procPath / "spl/kstat/zfs" / device_name; + if (access(zfs_pool_stat_path.c_str(), R_OK) == 0) { + return zfs_pool_stat_path; + } else { + Logger::debug("Cant access folder: " + zfs_pool_stat_path.string()); + return ""; + } + } + + ifstream filestream; + string filename; + string name_compare; + + if (dataset_name_start != std::string::npos) { // device is a dataset + zfs_pool_stat_path = Shared::procPath / "spl/kstat/zfs" / device_name.substr(0, dataset_name_start); + } else { // device is a pool + zfs_pool_stat_path = Shared::procPath / "spl/kstat/zfs" / device_name; + } + + // looking through all files that start with 'objset' to find the one containing `device_name` object stats + for (const auto& file: fs::directory_iterator(zfs_pool_stat_path)) { + filename = file.path().filename(); + if (filename.starts_with("objset")) { + filestream.open(file.path()); + if (filestream.good()) { + // skip first two lines + for (int i = 0; i < 2; i++) filestream.ignore(numeric_limits::max(), '\n'); + // skip characters until '7' is reached, indicating data type 7, next value will be object name + filestream.ignore(numeric_limits::max(), '7'); + filestream >> name_compare; + if (name_compare == device_name) { + filestream.close(); + if (access(file.path().c_str(), R_OK) == 0) { + return file.path(); + } else { + Logger::debug("Can't access file: " + file.path().string()); + return ""; + } + } + } + filestream.close(); + } + } + + Logger::debug("Could not read directory: " + zfs_pool_stat_path.string()); + return ""; + } + + bool zfs_collect_pool_total_stats(struct disk_info &disk) { + ifstream diskread; + + int64_t bytes_read, bytes_write, io_ticks, bytes_read_total = 0, bytes_write_total = 0, io_ticks_total = 0, objects_read = 0; + + // looking through all files that start with 'objset' + for (const auto& file: fs::directory_iterator(disk.stat)) { + if ((file.path().filename()).string().starts_with("objset")) { + diskread.open(file.path()); + if (diskread.good()) { + try { + // skip first three lines + for (int i = 0; i < 3; i++) diskread.ignore(numeric_limits::max(), '\n'); + // skip characters until '4' is reached, indicating data type 4, next value will be out target + diskread.ignore(numeric_limits::max(), '4'); + diskread >> io_ticks; + io_ticks_total += io_ticks; + + // skip characters until '4' is reached, indicating data type 4, next value will be out target + diskread.ignore(numeric_limits::max(), '4'); + diskread >> bytes_write; + bytes_write_total += bytes_write; + + // skip characters until '4' is reached, indicating data type 4, next value will be out target + diskread.ignore(numeric_limits::max(), '4'); + diskread >> io_ticks; + io_ticks_total += io_ticks; + + // skip characters until '4' is reached, indicating data type 4, next value will be out target + diskread.ignore(numeric_limits::max(), '4'); + diskread >> bytes_read; + bytes_read_total += bytes_read; + } catch (const std::exception& e) { + continue; + } + + // increment read objects counter if no errors were encountered + objects_read++; + } else { + Logger::debug("Could not read file: " + file.path().string()); + } + diskread.close(); + } + } + + // if for some reason no objects were read + if (objects_read == 0) return false; + + if (disk.io_write.empty()) + disk.io_write.push_back(0); + else + disk.io_write.push_back(max((int64_t)0, (bytes_write_total - disk.old_io.at(1)))); + disk.old_io.at(1) = bytes_write_total; + while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); + + if (disk.io_read.empty()) + disk.io_read.push_back(0); + else + disk.io_read.push_back(max((int64_t)0, (bytes_read_total - disk.old_io.at(0)))); + disk.old_io.at(0) = bytes_read_total; + while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); + + if (disk.io_activity.empty()) + disk.io_activity.push_back(0); + else + disk.io_activity.push_back(max((int64_t)0, (io_ticks_total - disk.old_io.at(2)))); + disk.old_io.at(2) = io_ticks_total; + while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); + + return true; + } + } namespace Net { @@ -1004,13 +1329,24 @@ namespace Net { auto& saved_stat = net.at(iface).stat.at(dir); auto& bandwidth = net.at(iface).bandwidth.at(dir); - const uint64_t val = max((uint64_t)stoul(readfile(sys_file, "0")), saved_stat.last); + uint64_t val = 0; + try { val = (uint64_t)stoull(readfile(sys_file, "0")); } + catch (const std::invalid_argument&) {} + catch (const std::out_of_range&) {} //? Update speed, total and top values + if (val < saved_stat.last) { + saved_stat.rollover += saved_stat.last; + saved_stat.last = 0; + } + if (cmp_greater((unsigned long long)saved_stat.rollover + (unsigned long long)val, numeric_limits::max())) { + saved_stat.rollover = 0; + saved_stat.last = 0; + } saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000)); if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed; - if (saved_stat.offset > val) saved_stat.offset = 0; - saved_stat.total = val - saved_stat.offset; + if (saved_stat.offset > val + saved_stat.rollover) saved_stat.offset = 0; + saved_stat.total = (val + saved_stat.rollover) - saved_stat.offset; saved_stat.last = val; //? Add values to graph @@ -1019,6 +1355,7 @@ namespace Net { //? Set counters for auto scaling if (net_auto and selected_iface == iface) { + if (net_sync and saved_stat.speed < net.at(iface).stat.at(dir == "download" ? "upload" : "download").speed) continue; if (saved_stat.speed > graph_max[dir]) { ++max_count[dir][0]; if (max_count[dir][1] > 0) --max_count[dir][1]; @@ -1124,69 +1461,8 @@ namespace Proc { int filter_found = 0; detail_container detailed; - - //* Generate process tree list - void _tree_gen(proc_info& cur_proc, vector& in_procs, vector>& out_procs, int cur_depth, const bool collapsed, const string& filter, bool found=false, const bool no_update=false, const bool should_filter=false) { - auto cur_pos = out_procs.size(); - bool filtering = false; - - //? If filtering, include children of matching processes - if (not found and (should_filter or not filter.empty())) { - if (not s_contains(std::to_string(cur_proc.pid), filter) - and not s_contains(cur_proc.name, filter) - and not s_contains(cur_proc.cmd, filter) - and not s_contains(cur_proc.user, filter)) { - filtering = true; - cur_proc.filtered = true; - filter_found++; - } - else { - found = true; - cur_depth = 0; - } - } - else if (cur_proc.filtered) cur_proc.filtered = false; - - //? Set tree index position for process if not filtered out or currently in a collapsed sub-tree - if (not collapsed and not filtering) { - out_procs.push_back(std::ref(cur_proc)); - cur_proc.tree_index = out_procs.size() - 1; - //? Try to find name of the binary file and append to program name if not the same - if (cur_proc.short_cmd.empty() and not cur_proc.cmd.empty()) { - std::string_view cmd_view = cur_proc.cmd; - cmd_view = cmd_view.substr((size_t)0, min(cmd_view.find(' '), cmd_view.size())); - cmd_view = cmd_view.substr(min(cmd_view.find_last_of('/') + 1, cmd_view.size())); - cur_proc.short_cmd = (string)cmd_view; - } - } - else { - cur_proc.tree_index = in_procs.size(); - } - - //? Recursive iteration over all children - int children = 0; - for (auto& p : rng::equal_range(in_procs, cur_proc.pid, rng::less{}, &proc_info::ppid)) { - if (not no_update and not filtering and (collapsed or cur_proc.collapsed)) { - out_procs.back().get().cpu_p += p.cpu_p; - out_procs.back().get().mem += p.mem; - out_procs.back().get().threads += p.threads; - filter_found++; - } - if (collapsed and not filtering) { - cur_proc.filtered = true; - } - else children++; - _tree_gen(p, in_procs, out_procs, cur_depth + 1, (collapsed ? true : cur_proc.collapsed), filter, found, no_update, should_filter); - } - if (collapsed or filtering) return; - - //? Add tree terminator symbol if it's the last child in a sub-tree - if (out_procs.size() > cur_pos + 1 and not out_procs.back().get().prefix.ends_with("]─")) - out_procs.back().get().prefix.replace(out_procs.back().get().prefix.size() - 8, 8, " └─ "); - - //? Add collapse/expand symbols if process have any children - out_procs.at(cur_pos).get().prefix = " │ "s * cur_depth + (children > 0 ? (cur_proc.collapsed ? "[+]─" : "[-]─") : " ├─ "); - } + constexpr size_t KTHREADD = 2; + static robin_hood::unordered_set kernels_procs = {KTHREADD}; //* Get detailed info for selected process void _collect_details(const size_t pid, const uint64_t uptime, vector& procs) { @@ -1253,7 +1529,7 @@ namespace Proc { detailed.memory = floating_humanizer(detailed.entry.mem); } if (detailed.first_mem == -1 or detailed.first_mem < detailed.mem_bytes.back() / 2 or detailed.first_mem > detailed.mem_bytes.back() * 4) { - detailed.first_mem = min((uint64_t)detailed.mem_bytes.back() * 2, Shared::totalMem); + detailed.first_mem = min((uint64_t)detailed.mem_bytes.back() * 2, Mem::get_totalMem()); redraw = true; } @@ -1291,6 +1567,7 @@ namespace Proc { const auto& reverse = Config::getB("proc_reversed"); const auto& filter = Config::getS("proc_filter"); const auto& per_core = Config::getB("proc_per_core"); + const auto& should_filter_kernel = Config::getB("proc_filter_kernel"); const auto& tree = Config::getB("proc_tree"); const auto& show_detailed = Config::getB("show_detailed"); const size_t detailed_pid = Config::getI("detailed_pid"); @@ -1305,11 +1582,15 @@ namespace Proc { string long_string; string short_str; + static vector found; + const double uptime = system_uptime(); const int cmult = (per_core) ? Shared::coreCount : 1; bool got_detailed = false; + static size_t proc_clear_count = 0; + //* Use pids from last update if only changing filter, sorting or tree options if (no_update and not current_procs.empty()) { if (show_detailed and detailed_pid != detailed.last_pid) _collect_details(detailed_pid, round(uptime), current_procs); @@ -1317,6 +1598,19 @@ namespace Proc { //* ---------------------------------------------Collection start---------------------------------------------- else { should_filter = true; + found.clear(); + + //? First make sure kernel proc cache is cleared. + if (should_filter_kernel and ++proc_clear_count >= 256) { + //? Clearing the cache is used in the event of a pid wrap around. + //? In that event processes that acquire old kernel pids would also be filtered out so we need to manually clean the cache every now and then. + kernels_procs.clear(); + kernels_procs.emplace(KTHREADD); + proc_clear_count = 0; + } + + auto totalMem = Mem::get_totalMem(); + int totalMem_len = to_string(totalMem >> 10).size(); //? Update uid_user map if /etc/passwd changed since last run if (not Shared::passwd_path.empty() and fs::last_write_time(Shared::passwd_path) != passwd_time) { @@ -1325,7 +1619,7 @@ namespace Proc { uid_user.clear(); pread.open(Shared::passwd_path); if (pread.good()) { - while (not pread.eof()) { + while (pread.good()) { getline(pread, r_user, ':'); pread.ignore(SSmax, ':'); getline(pread, r_uid, ':'); @@ -1350,7 +1644,6 @@ namespace Proc { pread.close(); //? Iterate over all pids in /proc - vector found; for (const auto& d: fs::directory_iterator(Shared::procPath)) { if (Runner::stopping) return current_procs; @@ -1360,6 +1653,11 @@ namespace Proc { if (not isdigit(pid_str[0])) continue; const size_t pid = stoul(pid_str); + + if (should_filter_kernel and kernels_procs.contains(pid)) { + continue; + } + found.push_back(pid); //? Check if pid already exists in current_procs @@ -1385,7 +1683,13 @@ namespace Proc { pread.open(d.path() / "cmdline"); if (not pread.good()) continue; long_string.clear(); - while(getline(pread, long_string, '\0')) new_proc.cmd += long_string + ' '; + while(getline(pread, long_string, '\0')) { + new_proc.cmd += long_string + ' '; + if (new_proc.cmd.size() > 1000) { + new_proc.cmd.resize(1000); + break; + } + } pread.close(); if (not new_proc.cmd.empty()) new_proc.cmd.pop_back(); @@ -1393,7 +1697,7 @@ namespace Proc { if (not pread.good()) continue; string uid; string line; - while (not pread.eof()) { + while (pread.good()) { getline(pread, line, ':'); if (line == "Uid") { pread.ignore(); @@ -1404,7 +1708,26 @@ namespace Proc { } } pread.close(); - new_proc.user = (uid_user.contains(uid)) ? uid_user.at(uid) : uid; + if (uid_user.contains(uid)) { + new_proc.user = uid_user.at(uid); + } + else { + #if !(defined(STATIC_BUILD) && defined(__GLIBC__)) + try { + struct passwd* udet; + udet = getpwuid(stoi(uid)); + if (udet != NULL and udet->pw_name != NULL) { + new_proc.user = string(udet->pw_name); + } + else { + new_proc.user = uid; + } + } + catch (...) { new_proc.user = uid; } + #else + new_proc.user = uid; + #endif + } } //? Parse /proc/[pid]/stat @@ -1417,9 +1740,9 @@ namespace Proc { uint64_t cpu_t = 0; try { for (;;) { - while (++x < next_x + offset) pread.ignore(SSmax, ' '); - getline(pread, short_str, ' '); + while (pread.good() and ++x < next_x + offset) pread.ignore(SSmax, ' '); if (not pread.good()) break; + else getline(pread, short_str, ' '); switch (x-offset) { case 3: //? Process state @@ -1454,8 +1777,8 @@ namespace Proc { next_x = 24; continue; case 24: //? RSS memory (can be inaccurate, but parsing smaps increases total cpu usage by ~20x) - if (cmp_greater(short_str.size(), Shared::totalMem_len)) - new_proc.mem = Shared::totalMem; + if (cmp_greater(short_str.size(), totalMem_len)) + new_proc.mem = totalMem; else new_proc.mem = stoull(short_str) * Shared::pageSize; } @@ -1468,10 +1791,15 @@ namespace Proc { pread.close(); + if (should_filter_kernel and new_proc.ppid == KTHREADD) { + kernels_procs.emplace(new_proc.pid); + found.pop_back(); + } + if (x-offset < 24) continue; //? Get RSS memory from /proc/[pid]/statm if value from /proc/[pid]/stat looks wrong - if (new_proc.mem >= Shared::totalMem) { + if (new_proc.mem >= totalMem) { pread.open(d.path() / "statm"); if (not pread.good()) continue; pread.ignore(SSmax, ' '); @@ -1494,7 +1822,7 @@ namespace Proc { } } - //? Clear dead processes from current_procs + //? Clear dead processes from current_procs and remove kernel processes if enabled auto eraser = rng::remove_if(current_procs, [&](const auto& element){ return not v_contains(found, element.pid); }); current_procs.erase(eraser.begin(), eraser.end()); @@ -1511,47 +1839,15 @@ namespace Proc { } //* ---------------------------------------------Collection done----------------------------------------------- - //* Sort processes - if (sorted_change or not no_update) { - switch (v_index(sort_vector, sorting)) { - case 0: rng::sort(current_procs, rng::greater{}, &proc_info::pid); break; - case 1: rng::sort(current_procs, rng::greater{}, &proc_info::name); break; - case 2: rng::sort(current_procs, rng::greater{}, &proc_info::cmd); break; - case 3: rng::sort(current_procs, rng::greater{}, &proc_info::threads); break; - case 4: rng::sort(current_procs, rng::greater{}, &proc_info::user); break; - case 5: rng::sort(current_procs, rng::greater{}, &proc_info::mem); break; - case 6: rng::sort(current_procs, rng::greater{}, &proc_info::cpu_p); break; - case 7: rng::sort(current_procs, rng::greater{}, &proc_info::cpu_c); break; - } - if (reverse) rng::reverse(current_procs); - - //* When sorting with "cpu lazy" push processes over threshold cpu usage to the front regardless of cumulative usage - if (not tree and not reverse and sorting == "cpu lazy") { - double max = 10.0, target = 30.0; - for (size_t i = 0, x = 0, offset = 0; i < current_procs.size(); i++) { - if (i <= 5 and current_procs.at(i).cpu_p > max) - max = current_procs.at(i).cpu_p; - else if (i == 6) - target = (max > 30.0) ? max : 10.0; - if (i == offset and current_procs.at(i).cpu_p > 30.0) - offset++; - else if (current_procs.at(i).cpu_p > target) { - rotate(current_procs.begin() + offset, current_procs.begin() + i, current_procs.begin() + i + 1); - if (++x > 10) break; - } - } - } - } - //* Match filter if defined if (should_filter) { filter_found = 0; for (auto& p : current_procs) { if (not tree and not filter.empty()) { - if (not s_contains(to_string(p.pid), filter) - and not s_contains(p.name, filter) - and not s_contains(p.cmd, filter) - and not s_contains(p.user, filter)) { + if (not s_contains_ic(to_string(p.pid), filter) + and not s_contains_ic(p.name, filter) + and not s_contains_ic(p.cmd, filter) + and not s_contains_ic(p.user, filter)) { p.filtered = true; filter_found++; } @@ -1565,8 +1861,14 @@ namespace Proc { } } + //* Sort processes + if (sorted_change or not no_update) { + proc_sorter(current_procs, sorting, reverse, tree); + } + //* Generate tree view if enabled if (tree and (not no_update or should_filter or sorted_change)) { + bool locate_selection = false; if (auto find_pid = (collapse != -1 ? collapse : expand); find_pid != -1) { auto collapser = rng::find(current_procs, find_pid, &proc_info::pid); if (collapser != current_procs.end()) { @@ -1579,26 +1881,49 @@ namespace Proc { else if (expand > -1) { collapser->collapsed = false; } + if (Config::ints.at("proc_selected") > 0) locate_selection = true; } collapse = expand = -1; } if (should_filter or not filter.empty()) filter_found = 0; - vector> tree_procs; + vector tree_procs; tree_procs.reserve(current_procs.size()); + for (auto& p : current_procs) { + if (not v_contains(found, p.ppid)) p.ppid = 0; + } + //? Stable sort to retain selected sorting among processes with the same parent - rng::stable_sort(current_procs, rng::less{}, &proc_info::ppid); + rng::stable_sort(current_procs, rng::less{}, & proc_info::ppid); //? Start recursive iteration over processes with the lowest shared parent pids for (auto& p : rng::equal_range(current_procs, current_procs.at(0).ppid, rng::less{}, &proc_info::ppid)) { _tree_gen(p, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter); } - //? Final sort based on tree index - rng::sort(current_procs, rng::less{}, &proc_info::tree_index); - if (reverse) rng::reverse(current_procs); + //? Recursive sort over tree structure to account for collapsed processes in the tree + int index = 0; + tree_sort(tree_procs, sorting, reverse, index, current_procs.size()); + //? Add tree begin symbol to first item if childless + if (tree_procs.front().children.empty()) + tree_procs.front().entry.get().prefix.replace(tree_procs.front().entry.get().prefix.size() - 8, 8, " ┌─ "); + + //? Add tree terminator symbol to last item if childless + if (tree_procs.back().children.empty()) + tree_procs.back().entry.get().prefix.replace(tree_procs.back().entry.get().prefix.size() - 8, 8, " └─ "); + + //? Final sort based on tree index + rng::sort(current_procs, rng::less{}, & proc_info::tree_index); + + //? Move current selection/view to the selected process when collapsing/expanding in the tree + if (locate_selection) { + int loc = rng::find(current_procs, Proc::selected_pid, &proc_info::pid)->tree_index; + if (Config::ints.at("proc_start") >= loc or Config::ints.at("proc_start") <= loc - Proc::select_max) + Config::ints.at("proc_start") = max(0, loc - 1); + Config::ints.at("proc_selected") = loc - Config::ints.at("proc_start") + 1; + } } numpids = (int)current_procs.size() - filter_found; @@ -1622,4 +1947,4 @@ namespace Tools { } throw std::runtime_error("Failed get uptime from from " + (string)Shared::procPath + "/uptime"); } -} \ No newline at end of file +} diff --git a/src/osx/btop_collect.cpp b/src/osx/btop_collect.cpp new file mode 100644 index 0000000..d7877a1 --- /dev/null +++ b/src/osx/btop_collect.cpp @@ -0,0 +1,1367 @@ +/* Copyright 2021 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. + +indent = tab +tab-size = 4 +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sensors.hpp" +#include "smc.hpp" + +using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; +using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min; +namespace fs = std::filesystem; +namespace rng = std::ranges; +using namespace Tools; + +//? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- + +namespace Cpu { + vector core_old_totals; + vector core_old_idles; + vector available_fields = {"total"}; + vector available_sensors = {"Auto"}; + cpu_info current_cpu; + fs::path freq_path = "/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq"; + bool got_sensors = false, cpu_temp_only = false; + int core_offset = 0; + + //* Populate found_sensors map + bool get_sensors(); + + //* Get current cpu clock speed + string get_cpuHz(); + + //* Search /proc/cpuinfo for a cpu name + string get_cpuName(); + + struct Sensor { + fs::path path; + string label; + int64_t temp = 0; + int64_t high = 0; + int64_t crit = 0; + }; + + string cpu_sensor; + vector core_sensors; + unordered_flat_map core_mapping; +} // namespace Cpu + +namespace Mem { + double old_uptime; +} + + class MachProcessorInfo { + public: + processor_info_array_t info_array; + mach_msg_type_number_t info_count; + MachProcessorInfo() {} + virtual ~MachProcessorInfo() {vm_deallocate(mach_task_self(), (vm_address_t)info_array, (vm_size_t)sizeof(processor_info_array_t) * info_count);} + }; + +namespace Shared { + + fs::path passwd_path; + uint64_t totalMem; + long pageSize, coreCount, clkTck, physicalCoreCount, arg_max; + double machTck; + int totalMem_len; + + void init() { + //? Shared global variables init + + coreCount = sysconf(_SC_NPROCESSORS_ONLN); // this returns all logical cores (threads) + if (coreCount < 1) { + coreCount = 1; + Logger::warning("Could not determine number of cores, defaulting to 1."); + } + + size_t physicalCoreCountSize = sizeof(physicalCoreCount); + if (sysctlbyname("hw.physicalcpu", &physicalCoreCount, &physicalCoreCountSize, NULL, 0) < 0) { + Logger::error("Could not get physical core count"); + } + + pageSize = sysconf(_SC_PAGE_SIZE); + if (pageSize <= 0) { + pageSize = 4096; + Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect."); + } + + mach_timebase_info_data_t convf; + if (mach_timebase_info(&convf) == KERN_SUCCESS) { + machTck = convf.numer / convf.denom; + } else { + Logger::warning("Could not get mach clock tick conversion factor. Defaulting to 100, processes cpu usage might be incorrect."); + machTck = 100; + } + + clkTck = sysconf(_SC_CLK_TCK); + if (clkTck <= 0) { + clkTck = 100; + Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect."); + } + + int64_t memsize = 0; + size_t size = sizeof(memsize); + if (sysctlbyname("hw.memsize", &memsize, &size, NULL, 0) < 0) { + Logger::warning("Could not get memory size"); + } + totalMem = memsize; + + //* Get maximum length of process arguments + arg_max = sysconf(_SC_ARG_MAX); + + //? Init for namespace Cpu + if (not fs::exists(Cpu::freq_path) or access(Cpu::freq_path.c_str(), R_OK) == -1) Cpu::freq_path.clear(); + Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {}); + Cpu::current_cpu.temp.insert(Cpu::current_cpu.temp.begin(), Shared::coreCount + 1, {}); + Cpu::core_old_totals.insert(Cpu::core_old_totals.begin(), Shared::coreCount, 0); + Cpu::core_old_idles.insert(Cpu::core_old_idles.begin(), Shared::coreCount, 0); + Cpu::collect(); + for (auto &[field, vec] : Cpu::current_cpu.cpu_percent) { + if (not vec.empty() and not v_contains(Cpu::available_fields, field)) Cpu::available_fields.push_back(field); + } + Cpu::cpuName = Cpu::get_cpuName(); + Cpu::got_sensors = Cpu::get_sensors(); + Cpu::core_mapping = Cpu::get_core_mapping(); + + //? Init for namespace Mem + Mem::old_uptime = system_uptime(); + Mem::collect(); + } + +} // namespace Shared + +namespace Cpu { + string cpuName; + string cpuHz; + bool has_battery = true; + bool macM1 = false; + tuple current_bat; + + const array time_names = {"user", "nice", "system", "idle"}; + + unordered_flat_map cpu_old = { + {"totals", 0}, + {"idles", 0}, + {"user", 0}, + {"nice", 0}, + {"system", 0}, + {"idle", 0} + }; + + string get_cpuName() { + string name; + char buffer[1024]; + size_t size = sizeof(buffer); + if (sysctlbyname("machdep.cpu.brand_string", &buffer, &size, NULL, 0) < 0) { + Logger::error("Failed to get CPU name"); + return name; + } + name = string(buffer); + + auto name_vec = ssplit(name); + + if ((s_contains(name, "Xeon"s) or v_contains(name_vec, "Duo"s)) and v_contains(name_vec, "CPU"s)) { + auto cpu_pos = v_index(name_vec, "CPU"s); + if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')')) + name = name_vec.at(cpu_pos + 1); + else + name.clear(); + } else if (v_contains(name_vec, "Ryzen"s)) { + auto ryz_pos = v_index(name_vec, "Ryzen"s); + name = "Ryzen" + (ryz_pos < name_vec.size() - 1 ? ' ' + name_vec.at(ryz_pos + 1) : "") + (ryz_pos < name_vec.size() - 2 ? ' ' + name_vec.at(ryz_pos + 2) : ""); + } else if (s_contains(name, "Intel"s) and v_contains(name_vec, "CPU"s)) { + auto cpu_pos = v_index(name_vec, "CPU"s); + if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')') and name_vec.at(cpu_pos + 1) != "@") + name = name_vec.at(cpu_pos + 1); + else + name.clear(); + } else + name.clear(); + + if (name.empty() and not name_vec.empty()) { + for (const auto &n : name_vec) { + if (n == "@") break; + name += n + ' '; + } + name.pop_back(); + for (const auto& replace : {"Processor", "CPU", "(R)", "(TM)", "Intel", "AMD", "Core"}) { + name = s_replace(name, replace, ""); + name = s_replace(name, " ", " "); + } + name = trim(name); + } + + return name; + } + + bool get_sensors() { + Logger::debug("get_sensors(): show_coretemp=" + std::to_string(Config::getB("show_coretemp")) + " check_temp=" + std::to_string(Config::getB("check_temp"))); + got_sensors = false; + if (Config::getB("show_coretemp") and Config::getB("check_temp")) { + ThermalSensors sensors; + if (sensors.getSensors() > 0) { + Logger::debug("M1 sensors found"); + got_sensors = true; + cpu_temp_only = true; + macM1 = true; + } else { + // try SMC (intel) + Logger::debug("checking intel"); + SMCConnection smcCon; + try { + long long t = smcCon.getTemp(-1); // check if we have package T + if (t > -1) { + Logger::debug("intel sensors found"); + got_sensors = true; + t = smcCon.getTemp(0); + if (t == -1) { + // for some macs the core offset is 1 - check if we get a sane value with 1 + if (smcCon.getTemp(1) > -1) { + Logger::debug("intel sensors with offset 1"); + core_offset = 1; + } + } + } else { + Logger::debug("no intel sensors found"); + got_sensors = false; + } + } catch (std::runtime_error &e) { + // ignore, we don't have temp + got_sensors = false; + } + } + } + return got_sensors; + } + + void update_sensors() { + current_cpu.temp_max = 95; // we have no idea how to get the critical temp + try { + if (macM1) { + ThermalSensors sensors; + current_cpu.temp.at(0).push_back(sensors.getSensors()); + if (current_cpu.temp.at(0).size() > 20) + current_cpu.temp.at(0).pop_front(); + + } else { + SMCConnection smcCon; + int threadsPerCore = Shared::coreCount / Shared::physicalCoreCount; + long long packageT = smcCon.getTemp(-1); // -1 returns package T + current_cpu.temp.at(0).push_back(packageT); + + for (int core = 0; core < Shared::coreCount; core++) { + long long temp = smcCon.getTemp((core / threadsPerCore) + core_offset); // same temp for all threads of same physical core + if (cmp_less(core + 1, current_cpu.temp.size())) { + current_cpu.temp.at(core + 1).push_back(temp); + if (current_cpu.temp.at(core + 1).size() > 20) + current_cpu.temp.at(core + 1).pop_front(); + } + } + } + } catch (std::runtime_error &e) { + got_sensors = false; + Logger::error("failed getting CPU temp"); + } + } + + string get_cpuHz() { + unsigned int freq = 1; + size_t size = sizeof(freq); + + int mib[] = {CTL_HW, HW_CPU_FREQ}; + + if (sysctl(mib, 2, &freq, &size, NULL, 0) < 0) { + // this fails on Apple Silicon macs. Apparently you're not allowed to know + return ""; + } + return std::to_string(freq / 1000.0 / 1000.0 / 1000.0).substr(0, 3); + } + + auto get_core_mapping() -> unordered_flat_map { + unordered_flat_map core_map; + if (cpu_temp_only) return core_map; + + natural_t cpu_count; + natural_t i; + MachProcessorInfo info {}; + kern_return_t error; + + error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count); + if (error != KERN_SUCCESS) { + Logger::error("Failed getting CPU info"); + return core_map; + } + for (i = 0; i < cpu_count; i++) { + core_map[i] = i; + } + + //? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely, map 0-0 1-1 2-2 etc. + if (cmp_less(core_map.size(), Shared::coreCount)) { + if (Shared::coreCount % 2 == 0 and (long) core_map.size() == Shared::coreCount / 2) { + for (int i = 0, n = 0; i < Shared::coreCount / 2; i++) { + if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; + core_map[Shared::coreCount / 2 + i] = n++; + } + } else { + core_map.clear(); + for (int i = 0, n = 0; i < Shared::coreCount; i++) { + if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; + core_map[i] = n++; + } + } + } + + //? Apply user set custom mapping if any + const auto &custom_map = Config::getS("cpu_core_map"); + if (not custom_map.empty()) { + try { + for (const auto &split : ssplit(custom_map)) { + const auto vals = ssplit(split, ':'); + if (vals.size() != 2) continue; + int change_id = std::stoi(vals.at(0)); + int new_id = std::stoi(vals.at(1)); + if (not core_map.contains(change_id) or cmp_greater(new_id, core_sensors.size())) continue; + core_map.at(change_id) = new_id; + } + } catch (...) { + } + } + + return core_map; + } + + class IOPSInfo_Wrap { + CFTypeRef data; + public: + IOPSInfo_Wrap() { data = IOPSCopyPowerSourcesInfo(); } + CFTypeRef& operator()() { return data; } + ~IOPSInfo_Wrap() { CFRelease(data); } + }; + + class IOPSList_Wrap { + CFArrayRef data; + public: + IOPSList_Wrap(CFTypeRef cft_ref) { data = IOPSCopyPowerSourcesList(cft_ref); } + CFArrayRef& operator()() { return data; } + ~IOPSList_Wrap() { CFRelease(data); } + }; + + auto get_battery() -> tuple { + if (not has_battery) return {0, 0, ""}; + + uint32_t percent = -1; + long seconds = -1; + string status = "discharging"; + IOPSInfo_Wrap ps_info{}; + if (ps_info()) { + IOPSList_Wrap one_ps_descriptor(ps_info()); + if (one_ps_descriptor()) { + if (CFArrayGetCount(one_ps_descriptor())) { + CFDictionaryRef one_ps = IOPSGetPowerSourceDescription(ps_info(), CFArrayGetValueAtIndex(one_ps_descriptor(), 0)); + has_battery = true; + CFNumberRef remaining = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSTimeToEmptyKey)); + int32_t estimatedMinutesRemaining; + if (remaining) { + CFNumberGetValue(remaining, kCFNumberSInt32Type, &estimatedMinutesRemaining); + seconds = estimatedMinutesRemaining * 60; + } + CFNumberRef charge = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSCurrentCapacityKey)); + if (charge) { + CFNumberGetValue(charge, kCFNumberSInt32Type, &percent); + } + CFBooleanRef charging = (CFBooleanRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSIsChargingKey)); + if (charging) { + bool isCharging = CFBooleanGetValue(charging); + if (isCharging) { + status = "charging"; + } + } + if (percent == 100) { + status = "full"; + } + } else { + has_battery = false; + } + } else { + has_battery = false; + } + } + return {percent, seconds, status}; + } + + auto collect(const bool no_update) -> cpu_info & { + if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty())) + return current_cpu; + auto &cpu = current_cpu; + + double avg[3]; + + if (getloadavg(avg, sizeof(avg)) < 0) { + Logger::error("failed to get load averages"); + } + + cpu.load_avg = { (float)avg[0], (float)avg[1], (float)avg[2]}; + + natural_t cpu_count; + natural_t i; + kern_return_t error; + processor_cpu_load_info_data_t *cpu_load_info = NULL; + + MachProcessorInfo info{}; + error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count); + if (error != KERN_SUCCESS) { + Logger::error("Failed getting CPU load info"); + } + cpu_load_info = (processor_cpu_load_info_data_t *)info.info_array; + long long global_totals = 0; + long long global_idles = 0; + vector times_summed = {0, 0, 0, 0}; + for (i = 0; i < cpu_count; i++) { + vector times; + //? 0=user, 1=nice, 2=system, 3=idle + for (int x = 0; const unsigned int c_state : {CPU_STATE_USER, CPU_STATE_NICE, CPU_STATE_SYSTEM, CPU_STATE_IDLE}) { + auto val = cpu_load_info[i].cpu_ticks[c_state]; + times.push_back(val); + times_summed.at(x++) += val; + } + + try { + //? All values + const long long totals = std::accumulate(times.begin(), times.end(), 0ll); + + //? Idle time + const long long idles = times.at(3); + + global_totals += totals; + global_idles += idles; + + //? Calculate cpu total for each core + if (i > Shared::coreCount) break; + const long long calc_totals = max(0ll, totals - core_old_totals.at(i)); + const long long calc_idles = max(0ll, idles - core_old_idles.at(i)); + core_old_totals.at(i) = totals; + core_old_idles.at(i) = idles; + + cpu.core_percent.at(i).push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); + + //? Reduce size if there are more values than needed for graph + if (cpu.core_percent.at(i).size() > 40) cpu.core_percent.at(i).pop_front(); + + } catch (const std::exception &e) { + Logger::error("Cpu::collect() : " + (string)e.what()); + throw std::runtime_error("collect() : " + (string)e.what()); + } + } + + const long long calc_totals = max(1ll, global_totals - cpu_old.at("totals")); + const long long calc_idles = max(1ll, global_idles - cpu_old.at("idles")); + + //? Populate cpu.cpu_percent with all fields from syscall + for (int ii = 0; const auto &val : times_summed) { + cpu.cpu_percent.at(time_names.at(ii)).push_back(clamp((long long)round((double)(val - cpu_old.at(time_names.at(ii))) * 100 / calc_totals), 0ll, 100ll)); + cpu_old.at(time_names.at(ii)) = val; + + //? Reduce size if there are more values than needed for graph + while (cmp_greater(cpu.cpu_percent.at(time_names.at(ii)).size(), width * 2)) cpu.cpu_percent.at(time_names.at(ii)).pop_front(); + + ii++; + } + + cpu_old.at("totals") = global_totals; + cpu_old.at("idles") = global_idles; + + //? Total usage of cpu + cpu.cpu_percent.at("total").push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); + + //? Reduce size if there are more values than needed for graph + while (cmp_greater(cpu.cpu_percent.at("total").size(), width * 2)) cpu.cpu_percent.at("total").pop_front(); + + if (Config::getB("show_cpu_freq")) { + auto hz = get_cpuHz(); + if (hz != "") { + cpuHz = hz; + } + } + + if (Config::getB("check_temp") and got_sensors) + update_sensors(); + + if (Config::getB("show_battery") and has_battery) + current_bat = get_battery(); + + return cpu; + } +} // namespace Cpu + +namespace Mem { + bool has_swap = false; + vector fstab; + fs::file_time_type fstab_time; + int disk_ios = 0; + vector last_found; + + mem_info current_mem{}; + + uint64_t get_totalMem() { + return Shared::totalMem; + } + + int64_t getCFNumber(CFDictionaryRef dict, const void *key) { + CFNumberRef ref = (CFNumberRef)CFDictionaryGetValue(dict, key); + if (ref) { + int64_t value; + CFNumberGetValue(ref, kCFNumberSInt64Type, &value); + return value; + } + return 0; + } + + string getCFString(io_registry_entry_t volumeRef, CFStringRef key) { + CFStringRef bsdNameRef = (CFStringRef)IORegistryEntryCreateCFProperty(volumeRef, key, kCFAllocatorDefault, 0); + if (bsdNameRef) { + char buf[200]; + CFStringGetCString(bsdNameRef, buf, 200, kCFStringEncodingASCII); + CFRelease(bsdNameRef); + return string(buf); + } + return ""; + } + + bool isWhole(io_registry_entry_t volumeRef) { + CFBooleanRef isWhole = (CFBooleanRef)IORegistryEntryCreateCFProperty(volumeRef, CFSTR("Whole"), kCFAllocatorDefault, 0); + Boolean val = CFBooleanGetValue(isWhole); + CFRelease(isWhole); + return bool(val); + } + + class IOObject { + public: + IOObject(string name, io_object_t& obj) : name(name), object(obj) {} + virtual ~IOObject() { IOObjectRelease(object); } + private: + string name; + io_object_t &object; + }; + + void collect_disk(unordered_flat_map &disks, unordered_flat_map &mapping) { + io_registry_entry_t drive; + io_iterator_t drive_list; + + mach_port_t libtop_master_port; + if (IOMasterPort(bootstrap_port, &libtop_master_port)) { + Logger::error("errot getting master port"); + return; + } + /* Get the list of all drive objects. */ + if (IOServiceGetMatchingServices(libtop_master_port, + IOServiceMatching("IOMediaBSDClient"), &drive_list)) { + Logger::error("Error in IOServiceGetMatchingServices()"); + return; + } + auto d = IOObject("drive list", drive_list); // dummy var so it gets destroyed + while ((drive = IOIteratorNext(drive_list)) != 0) { + auto dr = IOObject("drive", drive); + io_registry_entry_t volumeRef; + IORegistryEntryGetParentEntry(drive, kIOServicePlane, &volumeRef); + if (volumeRef) { + if (!isWhole(volumeRef)) { + string bsdName = getCFString(volumeRef, CFSTR("BSD Name")); + string device = getCFString(volumeRef, CFSTR("VolGroupMntFromName")); + if (!mapping.contains(device)) { + device = "/dev/" + bsdName; // try again with BSD name - not all volumes seem to have VolGroupMntFromName property + } + if (device != "") { + if (mapping.contains(device)) { + string mountpoint = mapping.at(device); + if (disks.contains(mountpoint)) { + auto& disk = disks.at(mountpoint); + CFDictionaryRef properties; + IORegistryEntryCreateCFProperties(volumeRef, (CFMutableDictionaryRef *)&properties, kCFAllocatorDefault, 0); + if (properties) { + CFDictionaryRef statistics = (CFDictionaryRef)CFDictionaryGetValue(properties, CFSTR("Statistics")); + if (statistics) { + disk_ios++; + int64_t readBytes = getCFNumber(statistics, CFSTR("Bytes read from block device")); + if (disk.io_read.empty()) + disk.io_read.push_back(0); + else + disk.io_read.push_back(max((int64_t)0, (readBytes - disk.old_io.at(0)))); + disk.old_io.at(0) = readBytes; + while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); + + int64_t writeBytes = getCFNumber(statistics, CFSTR("Bytes written to block device")); + if (disk.io_write.empty()) + disk.io_write.push_back(0); + else + disk.io_write.push_back(max((int64_t)0, (writeBytes - disk.old_io.at(1)))); + disk.old_io.at(1) = writeBytes; + while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); + + // IOKit does not give us IO times, (use IO read + IO write with 1 MiB being 100% to get some activity indication) + if (disk.io_activity.empty()) + disk.io_activity.push_back(0); + else + disk.io_activity.push_back(clamp((long)round((double)(disk.io_write.back() + disk.io_read.back()) / (1 << 20)), 0l, 100l)); + while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); + } + } + CFRelease(properties); + } + } + } + } + } + } + } + + auto collect(const bool no_update) -> mem_info & { + if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty())) + return current_mem; + + auto &show_swap = Config::getB("show_swap"); + auto &show_disks = Config::getB("show_disks"); + auto &swap_disk = Config::getB("swap_disk"); + auto &mem = current_mem; + static const bool snapped = (getenv("BTOP_SNAPPED") != NULL); + + vm_statistics64 p; + mach_msg_type_number_t info_size = HOST_VM_INFO64_COUNT; + if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&p, &info_size) == 0) { + mem.stats.at("free") = p.free_count * Shared::pageSize; + mem.stats.at("cached") = p.external_page_count * Shared::pageSize; + mem.stats.at("used") = (p.active_count + p.inactive_count + p.wire_count) * Shared::pageSize; + mem.stats.at("available") = Shared::totalMem - mem.stats.at("used"); + } + + int mib[2] = {CTL_VM, VM_SWAPUSAGE}; + + struct xsw_usage swap; + size_t len = sizeof(struct xsw_usage); + if (sysctl(mib, 2, &swap, &len, NULL, 0) == 0) { + mem.stats.at("swap_total") = swap.xsu_total; + mem.stats.at("swap_free") = swap.xsu_avail; + mem.stats.at("swap_used") = swap.xsu_used; + } + + if (show_swap and mem.stats.at("swap_total") > 0) { + for (const auto &name : swap_names) { + mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / mem.stats.at("swap_total"))); + while (cmp_greater(mem.percent.at(name).size(), width * 2)) + mem.percent.at(name).pop_front(); + } + has_swap = true; + } else + has_swap = false; + //? Calculate percentages + for (const auto &name : mem_names) { + mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem)); + while (cmp_greater(mem.percent.at(name).size(), width * 2)) + mem.percent.at(name).pop_front(); + } + + if (show_disks) { + unordered_flat_map mapping; // keep mapping from device -> mountpoint, since IOKit doesn't give us the mountpoint + double uptime = system_uptime(); + auto &disks_filter = Config::getS("disks_filter"); + bool filter_exclude = false; + // auto &only_physical = Config::getB("only_physical"); + auto &disks = mem.disks; + vector filter; + if (not disks_filter.empty()) { + filter = ssplit(disks_filter); + if (filter.at(0).starts_with("exclude=")) { + filter_exclude = true; + filter.at(0) = filter.at(0).substr(8); + } + } + + struct statfs *stfs; + int count = getmntinfo(&stfs, MNT_WAIT); + vector found; + found.reserve(last_found.size()); + for (int i = 0; i < count; i++) { + std::error_code ec; + string mountpoint = stfs[i].f_mntonname; + string dev = stfs[i].f_mntfromname; + mapping[dev] = mountpoint; + + if (string(stfs[i].f_fstypename) == "autofs") { + continue; + } + + //? Match filter if not empty + if (not filter.empty()) { + bool match = v_contains(filter, mountpoint); + if ((filter_exclude and match) or (not filter_exclude and not match)) + continue; + } + + found.push_back(mountpoint); + if (not disks.contains(mountpoint)) { + disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; + + if (disks.at(mountpoint).dev.empty()) + disks.at(mountpoint).dev = dev; + + if (disks.at(mountpoint).name.empty()) + disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint); + } + + + if (not v_contains(last_found, mountpoint)) + redraw = true; + + disks.at(mountpoint).free = stfs[i].f_bfree; + disks.at(mountpoint).total = stfs[i].f_iosize; + } + + //? Remove disks no longer mounted or filtered out + if (swap_disk and has_swap) found.push_back("swap"); + for (auto it = disks.begin(); it != disks.end();) { + if (not v_contains(found, it->first)) + it = disks.erase(it); + else + it++; + } + if (found.size() != last_found.size()) redraw = true; + last_found = std::move(found); + + //? Get disk/partition stats + for (auto &[mountpoint, disk] : disks) { + if (std::error_code ec; not fs::exists(mountpoint, ec)) + continue; + struct statvfs vfs; + if (statvfs(mountpoint.c_str(), &vfs) < 0) { + Logger::warning("Failed to get disk/partition stats with statvfs() for: " + mountpoint); + continue; + } + disk.total = vfs.f_blocks * vfs.f_frsize; + disk.free = vfs.f_bfree * vfs.f_frsize; + disk.used = disk.total - disk.free; + disk.used_percent = round((double)disk.used * 100 / disk.total); + disk.free_percent = 100 - disk.used_percent; + } + + //? Setup disks order in UI and add swap if enabled + mem.disks_order.clear(); + if (snapped and disks.contains("/mnt")) + mem.disks_order.push_back("/mnt"); + else if (disks.contains("/")) + mem.disks_order.push_back("/"); + if (swap_disk and has_swap) { + mem.disks_order.push_back("swap"); + if (not disks.contains("swap")) + disks["swap"] = {"", "swap"}; + disks.at("swap").total = mem.stats.at("swap_total"); + disks.at("swap").used = mem.stats.at("swap_used"); + disks.at("swap").free = mem.stats.at("swap_free"); + disks.at("swap").used_percent = mem.percent.at("swap_used").back(); + disks.at("swap").free_percent = mem.percent.at("swap_free").back(); + } + for (const auto &name : last_found) + if (not is_in(name, "/", "swap", "/dev")) + mem.disks_order.push_back(name); + + disk_ios = 0; + collect_disk(disks, mapping); + + old_uptime = uptime; + } + return mem; + } + +} // namespace Mem + +namespace Net { + unordered_flat_map current_net; + net_info empty_net = {}; + vector interfaces; + string selected_iface; + int errors = 0; + unordered_flat_map graph_max = {{"download", {}}, {"upload", {}}}; + unordered_flat_map> max_count = {{"download", {}}, {"upload", {}}}; + bool rescale = true; + uint64_t timestamp = 0; + + //* RAII wrapper for getifaddrs + class getifaddr_wrapper { + struct ifaddrs *ifaddr; + + public: + int status; + getifaddr_wrapper() { status = getifaddrs(&ifaddr); } + ~getifaddr_wrapper() { freeifaddrs(ifaddr); } + auto operator()() -> struct ifaddrs * { return ifaddr; } + }; + + auto collect(const bool no_update) -> net_info & { + auto &net = current_net; + auto &config_iface = Config::getS("net_iface"); + auto &net_sync = Config::getB("net_sync"); + auto &net_auto = Config::getB("net_auto"); + auto new_timestamp = time_ms(); + + if (not no_update and errors < 3) { + //? Get interface list using getifaddrs() wrapper + getifaddr_wrapper if_wrap{}; + if (if_wrap.status != 0) { + errors++; + Logger::error("Net::collect() -> getifaddrs() failed with id " + to_string(if_wrap.status)); + redraw = true; + return empty_net; + } + int family = 0; + char ip[NI_MAXHOST]; + interfaces.clear(); + string ipv4, ipv6; + + //? Iteration over all items in getifaddrs() list + for (auto *ifa = if_wrap(); ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL) continue; + family = ifa->ifa_addr->sa_family; + const auto &iface = ifa->ifa_name; + //? Get IPv4 address + if (family == AF_INET) { + if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) + net[iface].ipv4 = ip; + } + //? Get IPv6 address + else if (family == AF_INET6) { + if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) + net[iface].ipv6 = ip; + } + + //? Update available interfaces vector and get status of interface + if (not v_contains(interfaces, iface)) { + interfaces.push_back(iface); + net[iface].connected = (ifa->ifa_flags & IFF_RUNNING); + } + } + + unordered_flat_map> ifstats; + int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST2, 0}; + size_t len; + if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { + Logger::error("failed getting network interfaces"); + } else { + std::unique_ptr buf(new char[len]); + if (sysctl(mib, 6, buf.get(), &len, NULL, 0) < 0) { + Logger::error("failed getting network interfaces"); + } else { + char *lim = buf.get() + len; + char *next = NULL; + for (next = buf.get(); next < lim;) { + struct if_msghdr *ifm = (struct if_msghdr *)next; + next += ifm->ifm_msglen; + if (ifm->ifm_type == RTM_IFINFO2) { + struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm; + struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); + char iface[32]; + strncpy(iface, sdl->sdl_data, sdl->sdl_nlen); + iface[sdl->sdl_nlen] = 0; + ifstats[iface] = std::tuple(if2m->ifm_data.ifi_ibytes, if2m->ifm_data.ifi_obytes); + } + } + } + } + + //? Get total recieved and transmitted bytes + device address if no ip was found + for (const auto &iface : interfaces) { + for (const string dir : {"download", "upload"}) { + auto &saved_stat = net.at(iface).stat.at(dir); + auto &bandwidth = net.at(iface).bandwidth.at(dir); + uint64_t val = dir == "download" ? std::get<0>(ifstats[iface]) : std::get<1>(ifstats[iface]); + + //? Update speed, total and top values + if (val < saved_stat.last) { + saved_stat.rollover += saved_stat.last; + saved_stat.last = 0; + } + if (cmp_greater((unsigned long long)saved_stat.rollover + (unsigned long long)val, numeric_limits::max())) { + saved_stat.rollover = 0; + saved_stat.last = 0; + } + saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000)); + if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed; + if (saved_stat.offset > val + saved_stat.rollover) saved_stat.offset = 0; + saved_stat.total = (val + saved_stat.rollover) - saved_stat.offset; + saved_stat.last = val; + + //? Add values to graph + bandwidth.push_back(saved_stat.speed); + while (cmp_greater(bandwidth.size(), width * 2)) bandwidth.pop_front(); + + //? Set counters for auto scaling + if (net_auto and selected_iface == iface) { + if (saved_stat.speed > graph_max[dir]) { + ++max_count[dir][0]; + if (max_count[dir][1] > 0) --max_count[dir][1]; + } else if (graph_max[dir] > 10 << 10 and saved_stat.speed < graph_max[dir] / 10) { + ++max_count[dir][1]; + if (max_count[dir][0] > 0) --max_count[dir][0]; + } + } + } + } + + //? Clean up net map if needed + if (net.size() > interfaces.size()) { + for (auto it = net.begin(); it != net.end();) { + if (not v_contains(interfaces, it->first)) + it = net.erase(it); + else + it++; + } + net.compact(); + } + + timestamp = new_timestamp; + } + //? Return empty net_info struct if no interfaces was found + if (net.empty()) + return empty_net; + + //? Find an interface to display if selected isn't set or valid + if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) { + max_count["download"][0] = max_count["download"][1] = max_count["upload"][0] = max_count["upload"][1] = 0; + redraw = true; + if (net_auto) rescale = true; + if (not config_iface.empty() and v_contains(interfaces, config_iface)) + selected_iface = config_iface; + else { + //? Sort interfaces by total upload + download bytes + auto sorted_interfaces = interfaces; + rng::sort(sorted_interfaces, [&](const auto &a, const auto &b) { + return cmp_greater(net.at(a).stat["download"].total + net.at(a).stat["upload"].total, + net.at(b).stat["download"].total + net.at(b).stat["upload"].total); + }); + selected_iface.clear(); + //? Try to set to a connected interface + for (const auto &iface : sorted_interfaces) { + if (net.at(iface).connected) selected_iface = iface; + break; + } + //? If no interface is connected set to first available + if (selected_iface.empty() and not sorted_interfaces.empty()) + selected_iface = sorted_interfaces.at(0); + else if (sorted_interfaces.empty()) + return empty_net; + } + } + + //? Calculate max scale for graphs if needed + if (net_auto) { + bool sync = false; + for (const auto &dir : {"download", "upload"}) { + for (const auto &sel : {0, 1}) { + if (rescale or max_count[dir][sel] >= 5) { + const uint64_t avg_speed = (net[selected_iface].bandwidth[dir].size() > 5 + ? std::accumulate(net.at(selected_iface).bandwidth.at(dir).rbegin(), net.at(selected_iface).bandwidth.at(dir).rbegin() + 5, 0) / 5 + : net[selected_iface].stat[dir].speed); + graph_max[dir] = max(uint64_t(avg_speed * (sel == 0 ? 1.3 : 3.0)), (uint64_t)10 << 10); + max_count[dir][0] = max_count[dir][1] = 0; + redraw = true; + if (net_sync) sync = true; + break; + } + } + //? Sync download/upload graphs if enabled + if (sync) { + const auto other = (string(dir) == "upload" ? "download" : "upload"); + graph_max[other] = graph_max[dir]; + max_count[other][0] = max_count[other][1] = 0; + break; + } + } + } + + rescale = false; + return net.at(selected_iface); + } +} // namespace Net + +namespace Proc { + + vector current_procs; + unordered_flat_map uid_user; + string current_sort; + string current_filter; + bool current_rev = false; + + fs::file_time_type passwd_time; + + uint64_t cputimes; + int collapse = -1, expand = -1; + uint64_t old_cputimes = 0; + atomic numpids = 0; + int filter_found = 0; + + detail_container detailed; + + string get_status(char s) { + if (s & SRUN) return "Running"; + if (s & SSLEEP) return "Sleeping"; + if (s & SIDL) return "Idle"; + if (s & SSTOP) return "Stopped"; + if (s & SZOMB) return "Zombie"; + return "Unknown"; + } + + //* Get detailed info for selected process + void _collect_details(const size_t pid, vector &procs) { + if (pid != detailed.last_pid) { + detailed = {}; + detailed.last_pid = pid; + detailed.skip_smaps = not Config::getB("proc_info_smaps"); + } + + //? Copy proc_info for process from proc vector + auto p_info = rng::find(procs, pid, &proc_info::pid); + detailed.entry = *p_info; + + //? Update cpu percent deque for process cpu graph + if (not Config::getB("proc_per_core")) detailed.entry.cpu_p *= Shared::coreCount; + detailed.cpu_percent.push_back(clamp((long long)round(detailed.entry.cpu_p), 0ll, 100ll)); + while (cmp_greater(detailed.cpu_percent.size(), width)) detailed.cpu_percent.pop_front(); + + //? Process runtime : current time - start time (both in unix time - seconds since epoch) + struct timeval currentTime; + gettimeofday(¤tTime, NULL); + detailed.elapsed = sec_to_dhms(currentTime.tv_sec - (detailed.entry.cpu_s / 1'000'000)); + if (detailed.elapsed.size() > 8) detailed.elapsed.resize(detailed.elapsed.size() - 3); + + //? Get parent process name + if (detailed.parent.empty()) { + auto p_entry = rng::find(procs, detailed.entry.ppid, &proc_info::pid); + if (p_entry != procs.end()) detailed.parent = p_entry->name; + } + + //? Expand process status from single char to explanative string + detailed.status = get_status(detailed.entry.state); + + detailed.mem_bytes.push_back(detailed.entry.mem); + detailed.memory = floating_humanizer(detailed.entry.mem); + + if (detailed.first_mem == -1 or detailed.first_mem < detailed.mem_bytes.back() / 2 or detailed.first_mem > detailed.mem_bytes.back() * 4) { + detailed.first_mem = min((uint64_t)detailed.mem_bytes.back() * 2, Mem::get_totalMem()); + redraw = true; + } + + while (cmp_greater(detailed.mem_bytes.size(), width)) detailed.mem_bytes.pop_front(); + + rusage_info_current rusage; + if (proc_pid_rusage(pid, RUSAGE_INFO_CURRENT, (void **)&rusage) == 0) { + // this fails for processes we don't own - same as in Linux + detailed.io_read = floating_humanizer(rusage.ri_diskio_bytesread); + detailed.io_write = floating_humanizer(rusage.ri_diskio_byteswritten); + } + } + + //* Collects and sorts process information from /proc + auto collect(const bool no_update) -> vector & { + const auto &sorting = Config::getS("proc_sorting"); + const auto &reverse = Config::getB("proc_reversed"); + const auto &filter = Config::getS("proc_filter"); + const auto &per_core = Config::getB("proc_per_core"); + const auto &tree = Config::getB("proc_tree"); + const auto &show_detailed = Config::getB("show_detailed"); + const size_t detailed_pid = Config::getI("detailed_pid"); + bool should_filter = current_filter != filter; + if (should_filter) current_filter = filter; + const bool sorted_change = (sorting != current_sort or reverse != current_rev or should_filter); + if (sorted_change) { + current_sort = sorting; + current_rev = reverse; + } + + const int cmult = (per_core) ? Shared::coreCount : 1; + bool got_detailed = false; + + static vector found; + + //* Use pids from last update if only changing filter, sorting or tree options + if (no_update and not current_procs.empty()) { + if (show_detailed and detailed_pid != detailed.last_pid) _collect_details(detailed_pid, current_procs); + } else { + //* ---------------------------------------------Collection start---------------------------------------------- + + { //* Get CPU totals + natural_t cpu_count; + kern_return_t error; + processor_cpu_load_info_data_t *cpu_load_info = NULL; + MachProcessorInfo info{}; + error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count); + if (error != KERN_SUCCESS) { + Logger::error("Failed getting CPU load info"); + } + cpu_load_info = (processor_cpu_load_info_data_t *)info.info_array; + cputimes = 0; + for (natural_t i = 0; i < cpu_count; i++) { + cputimes += (cpu_load_info[i].cpu_ticks[CPU_STATE_USER] + + cpu_load_info[i].cpu_ticks[CPU_STATE_NICE] + + cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM] + + cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE]); + } + } + + should_filter = true; + int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0}; + found.clear(); + size_t size = 0; + const auto timeNow = time_micros(); + + if (sysctl(mib, 4, NULL, &size, NULL, 0) < 0 || size == 0) { + Logger::error("Unable to get size of kproc_infos"); + } + uint64_t cpu_t = 0; + + std::unique_ptr processes(new kinfo_proc[size / sizeof(kinfo_proc)]); + if (sysctl(mib, 4, processes.get(), &size, NULL, 0) == 0) { + size_t count = size / sizeof(struct kinfo_proc); + for (size_t i = 0; i < count; i++) { //* iterate over all processes in kinfo_proc + struct kinfo_proc& kproc = processes.get()[i]; + const size_t pid = (size_t)kproc.kp_proc.p_pid; + if (pid < 1) continue; + found.push_back(pid); + + //? Check if pid already exists in current_procs + bool no_cache = false; + auto find_old = rng::find(current_procs, pid, &proc_info::pid); + if (find_old == current_procs.end()) { + current_procs.push_back({pid}); + find_old = current_procs.end() - 1; + no_cache = true; + } + + auto &new_proc = *find_old; + + //? Get program name, command, username, parent pid, nice and status + if (no_cache) { + char fullname[PROC_PIDPATHINFO_MAXSIZE]; + proc_pidpath(pid, fullname, sizeof(fullname)); + const string f_name = std::string(fullname); + size_t lastSlash = f_name.find_last_of('/'); + new_proc.name = f_name.substr(lastSlash + 1); + //? Get process arguments if possible, fallback to process path in case of failure + if (Shared::arg_max > 0) { + std::unique_ptr proc_chars(new char[Shared::arg_max]); + int mib[] = {CTL_KERN, KERN_PROCARGS2, (int)pid}; + size_t argmax = Shared::arg_max; + if (sysctl(mib, 3, proc_chars.get(), &argmax, NULL, 0) == 0) { + int argc = 0; + memcpy(&argc, &proc_chars.get()[0], sizeof(argc)); + std::string_view proc_args(proc_chars.get(), argmax); + if (size_t null_pos = proc_args.find('\0', sizeof(argc)); null_pos != string::npos) { + if (size_t start_pos = proc_args.find_first_not_of('\0', null_pos); start_pos != string::npos) { + while (argc-- > 0 and null_pos != string::npos and cmp_less(new_proc.cmd.size(), 1000)) { + null_pos = proc_args.find('\0', start_pos); + new_proc.cmd += (string)proc_args.substr(start_pos, null_pos - start_pos) + ' '; + start_pos = null_pos + 1; + } + } + } + if (not new_proc.cmd.empty()) new_proc.cmd.pop_back(); + } + } + if (new_proc.cmd.empty()) new_proc.cmd = f_name; + if (new_proc.cmd.size() > 1000) { + new_proc.cmd.resize(1000); + new_proc.cmd.shrink_to_fit(); + } + new_proc.ppid = kproc.kp_eproc.e_ppid; + new_proc.cpu_s = kproc.kp_proc.p_starttime.tv_sec * 1'000'000 + kproc.kp_proc.p_starttime.tv_usec; + struct passwd *pwd = getpwuid(kproc.kp_eproc.e_ucred.cr_uid); + new_proc.user = pwd->pw_name; + } + new_proc.p_nice = kproc.kp_proc.p_nice; + new_proc.state = kproc.kp_proc.p_stat; + + //? Get threads, mem and cpu usage + struct proc_taskinfo pti; + if (sizeof(pti) == proc_pidinfo(new_proc.pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti))) { + new_proc.threads = pti.pti_threadnum; + new_proc.mem = pti.pti_resident_size; + cpu_t = pti.pti_total_user + pti.pti_total_system; + + if (new_proc.cpu_t == 0) new_proc.cpu_t = cpu_t; + } + + //? Process cpu usage since last update + new_proc.cpu_p = clamp(round(((cpu_t - new_proc.cpu_t) * Shared::machTck) / ((cputimes - old_cputimes) * Shared::clkTck)) * cmult / 1000.0, 0.0, 100.0 * Shared::coreCount); + + //? Process cumulative cpu usage since process start + new_proc.cpu_c = (double)(cpu_t * Shared::machTck) / (timeNow - new_proc.cpu_s); + + //? Update cached value with latest cpu times + new_proc.cpu_t = cpu_t; + + if (show_detailed and not got_detailed and new_proc.pid == detailed_pid) { + got_detailed = true; + } + } + + // //? Clear dead processes from current_procs + auto eraser = rng::remove_if(current_procs, [&](const auto &element) { return not v_contains(found, element.pid); }); + current_procs.erase(eraser.begin(), eraser.end()); + + //? Update the details info box for process if active + if (show_detailed and got_detailed) { + _collect_details(detailed_pid, current_procs); + } else if (show_detailed and not got_detailed and detailed.status != "Dead") { + detailed.status = "Dead"; + redraw = true; + } + + old_cputimes = cputimes; + } + } + + //* ---------------------------------------------Collection done----------------------------------------------- + + //* Match filter if defined + if (should_filter) { + filter_found = 0; + for (auto &p : current_procs) { + if (not tree and not filter.empty()) { + if (not s_contains_ic(to_string(p.pid), filter) and not s_contains_ic(p.name, filter) and not s_contains_ic(p.cmd, filter) and not s_contains_ic(p.user, filter)) { + p.filtered = true; + filter_found++; + } else { + p.filtered = false; + } + } else { + p.filtered = false; + } + } + } + + //* Sort processes + if (sorted_change or not no_update) { + proc_sorter(current_procs, sorting, reverse, tree); + } + + //* Generate tree view if enabled + if (tree and (not no_update or should_filter or sorted_change)) { + bool locate_selection = false; + if (auto find_pid = (collapse != -1 ? collapse : expand); find_pid != -1) { + auto collapser = rng::find(current_procs, find_pid, &proc_info::pid); + if (collapser != current_procs.end()) { + if (collapse == expand) { + collapser->collapsed = not collapser->collapsed; + } + else if (collapse > -1) { + collapser->collapsed = true; + } + else if (expand > -1) { + collapser->collapsed = false; + } + if (Config::ints.at("proc_selected") > 0) locate_selection = true; + } + collapse = expand = -1; + } + if (should_filter or not filter.empty()) filter_found = 0; + + vector tree_procs; + tree_procs.reserve(current_procs.size()); + + for (auto& p : current_procs) { + if (not v_contains(found, p.ppid)) p.ppid = 0; + } + + //? Stable sort to retain selected sorting among processes with the same parent + rng::stable_sort(current_procs, rng::less{}, & proc_info::ppid); + + //? Start recursive iteration over processes with the lowest shared parent pids + for (auto& p : rng::equal_range(current_procs, current_procs.at(0).ppid, rng::less{}, &proc_info::ppid)) { + _tree_gen(p, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter); + } + + //? Recursive sort over tree structure to account for collapsed processes in the tree + int index = 0; + tree_sort(tree_procs, sorting, reverse, index, current_procs.size()); + + //? Add tree begin symbol to first item if childless + if (tree_procs.front().children.empty()) + tree_procs.front().entry.get().prefix.replace(tree_procs.front().entry.get().prefix.size() - 8, 8, " ┌─ "); + + //? Add tree terminator symbol to last item if childless + if (tree_procs.back().children.empty()) + tree_procs.back().entry.get().prefix.replace(tree_procs.back().entry.get().prefix.size() - 8, 8, " └─ "); + + //? Final sort based on tree index + rng::sort(current_procs, rng::less{}, & proc_info::tree_index); + + //? Move current selection/view to the selected process when collapsing/expanding in the tree + if (locate_selection) { + int loc = rng::find(current_procs, Proc::selected_pid, &proc_info::pid)->tree_index; + if (Config::ints.at("proc_start") >= loc or Config::ints.at("proc_start") <= loc - Proc::select_max) + Config::ints.at("proc_start") = max(0, loc - 1); + Config::ints.at("proc_selected") = loc - Config::ints.at("proc_start") + 1; + } + } + + numpids = (int)current_procs.size() - filter_found; + return current_procs; + } +} // namespace Proc + +namespace Tools { + double system_uptime() { + struct timeval ts, currTime; + std::size_t len = sizeof(ts); + int mib[2] = {CTL_KERN, KERN_BOOTTIME}; + if (sysctl(mib, 2, &ts, &len, NULL, 0) != -1) { + gettimeofday(&currTime, NULL); + return currTime.tv_sec - ts.tv_sec; + } + return 0.0; + } +} // namespace Tools diff --git a/src/osx/sensors.cpp b/src/osx/sensors.cpp new file mode 100644 index 0000000..e1f5f0e --- /dev/null +++ b/src/osx/sensors.cpp @@ -0,0 +1,111 @@ +/* Copyright 2021 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. + +indent = tab +tab-size = 4 +*/ + +#include "sensors.hpp" + +#include +#include + +#include +#include +#include + +extern "C" { +typedef struct __IOHIDEvent *IOHIDEventRef; +typedef struct __IOHIDServiceClient *IOHIDServiceClientRef; +#ifdef __LP64__ +typedef double IOHIDFloat; +#else +typedef float IOHIDFloat; +#endif + +#define IOHIDEventFieldBase(type) (type << 16) +#define kIOHIDEventTypeTemperature 15 + +IOHIDEventSystemClientRef IOHIDEventSystemClientCreate(CFAllocatorRef allocator); +int IOHIDEventSystemClientSetMatching(IOHIDEventSystemClientRef client, CFDictionaryRef match); +int IOHIDEventSystemClientSetMatchingMultiple(IOHIDEventSystemClientRef client, CFArrayRef match); +IOHIDEventRef IOHIDServiceClientCopyEvent(IOHIDServiceClientRef, int64_t, int32_t, int64_t); +CFStringRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFStringRef property); +IOHIDFloat IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field); + +// create a dict ref, like for temperature sensor {"PrimaryUsagePage":0xff00, "PrimaryUsage":0x5} +CFDictionaryRef matching(int page, int usage) { + CFNumberRef nums[2]; + CFStringRef keys[2]; + + keys[0] = CFStringCreateWithCString(0, "PrimaryUsagePage", 0); + keys[1] = CFStringCreateWithCString(0, "PrimaryUsage", 0); + nums[0] = CFNumberCreate(0, kCFNumberSInt32Type, &page); + nums[1] = CFNumberCreate(0, kCFNumberSInt32Type, &usage); + + CFDictionaryRef dict = CFDictionaryCreate(0, (const void **)keys, (const void **)nums, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFRelease(keys[0]); + CFRelease(keys[1]); + return dict; +} + +double getValue(IOHIDServiceClientRef sc) { + IOHIDEventRef event = IOHIDServiceClientCopyEvent(sc, kIOHIDEventTypeTemperature, 0, 0); // here we use ...CopyEvent + IOHIDFloat temp = 0.0; + if (event != 0) { + temp = IOHIDEventGetFloatValue(event, IOHIDEventFieldBase(kIOHIDEventTypeTemperature)); + CFRelease(event); + } + return temp; +} + +} // extern C + +long long Cpu::ThermalSensors::getSensors() { + CFDictionaryRef thermalSensors = matching(0xff00, 5); // 65280_10 = FF00_16 + // thermalSensors's PrimaryUsagePage should be 0xff00 for M1 chip, instead of 0xff05 + // can be checked by ioreg -lfx + IOHIDEventSystemClientRef system = IOHIDEventSystemClientCreate(kCFAllocatorDefault); + IOHIDEventSystemClientSetMatching(system, thermalSensors); + CFArrayRef matchingsrvs = IOHIDEventSystemClientCopyServices(system); + std::vector temps; + if (matchingsrvs) { + long count = CFArrayGetCount(matchingsrvs); + for (int i = 0; i < count; i++) { + IOHIDServiceClientRef sc = (IOHIDServiceClientRef)CFArrayGetValueAtIndex(matchingsrvs, i); + if (sc) { + CFStringRef name = IOHIDServiceClientCopyProperty(sc, CFSTR("Product")); // here we use ...CopyProperty + if (name) { + char buf[200]; + CFStringGetCString(name, buf, 200, kCFStringEncodingASCII); + std::string n(buf); + // this is just a guess, nobody knows which sensors mean what + // on my system PMU tdie 3 and 9 are missing... + // there is also PMU tdev1-8 but it has negative values?? + // there is also eACC for efficiency package but it only has 2 entries + // and pACC for performance but it has 7 entries (2 - 9) WTF + if (n.starts_with("eACC") or n.starts_with("pACC")) { + temps.push_back(getValue(sc)); + } + CFRelease(name); + } + } + } + CFRelease(matchingsrvs); + } + CFRelease(system); + CFRelease(thermalSensors); + if (temps.empty()) return 0ll; + return round(std::accumulate(temps.begin(), temps.end(), 0ll) / temps.size()); +} diff --git a/src/osx/sensors.hpp b/src/osx/sensors.hpp new file mode 100644 index 0000000..698fca6 --- /dev/null +++ b/src/osx/sensors.hpp @@ -0,0 +1,24 @@ +/* Copyright 2021 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. + +indent = tab +tab-size = 4 +*/ + +namespace Cpu { + class ThermalSensors { + public: + long long getSensors(); + }; +} // namespace Cpu diff --git a/src/osx/smc.cpp b/src/osx/smc.cpp new file mode 100644 index 0000000..a9c849b --- /dev/null +++ b/src/osx/smc.cpp @@ -0,0 +1,150 @@ +/* Copyright 2021 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. + +indent = tab +tab-size = 4 +*/ + +#include "smc.hpp" + +static UInt32 _strtoul(char *str, int size, int base) { + UInt32 total = 0; + int i; + + for (i = 0; i < size; i++) { + if (base == 16) { + total += str[i] << (size - 1 - i) * 8; + } else { + total += (unsigned char)(str[i] << (size - 1 - i) * 8); + } + } + return total; +} + +static void _ultostr(char *str, UInt32 val) { + str[0] = '\0'; + sprintf(str, "%c%c%c%c", + (unsigned int)val >> 24, + (unsigned int)val >> 16, + (unsigned int)val >> 8, + (unsigned int)val); +} + +namespace Cpu { + + SMCConnection::SMCConnection() { + IOMasterPort(kIOMasterPortDefault, &masterPort); + + CFMutableDictionaryRef matchingDictionary = IOServiceMatching("AppleSMC"); + result = IOServiceGetMatchingServices(masterPort, matchingDictionary, &iterator); + if (result != kIOReturnSuccess) { + throw std::runtime_error("failed to get AppleSMC"); + } + + device = IOIteratorNext(iterator); + IOObjectRelease(iterator); + if (device == 0) { + throw std::runtime_error("failed to get SMC device"); + } + + result = IOServiceOpen(device, mach_task_self(), 0, &conn); + IOObjectRelease(device); + if (result != kIOReturnSuccess) { + throw std::runtime_error("failed to get SMC connection"); + } + } + SMCConnection::~SMCConnection() { + IOServiceClose(conn); + } + + long long SMCConnection::getSMCTemp(char *key) { + SMCVal_t val; + kern_return_t result; + result = SMCReadKey(key, &val); + if (result == kIOReturnSuccess) { + if (val.dataSize > 0) { + if (strcmp(val.dataType, DATATYPE_SP78) == 0) { + // convert sp78 value to temperature + int intValue = val.bytes[0] * 256 + (unsigned char)val.bytes[1]; + return static_cast(intValue / 256.0); + } + } + } + return -1; + } + + // core means physical core in SMC, while in core map it's cpu threads :-/ Only an issue on hackintosh? + // this means we can only get the T per physical core + // another issue with the SMC API is that the key is always 4 chars -> what with systems with more than 9 physical cores? + // no Mac models with more than 18 threads are released, so no problem so far + // according to VirtualSMC docs (hackintosh fake SMC) the enumeration follows with alphabetic chars - not implemented yet here (nor in VirtualSMC) + long long SMCConnection::getTemp(int core) { + char key[] = SMC_KEY_CPU_TEMP; + if (core >= 0) { + snprintf(key, 5, "TC%1dc", core); + } + long long result = getSMCTemp(key); + if (result == -1) { + // try again with C + snprintf(key, 5, "TC%1dC", core); + result = getSMCTemp(key); + } + return result; + } + + kern_return_t SMCConnection::SMCReadKey(UInt32Char_t key, SMCVal_t *val) { + kern_return_t result; + SMCKeyData_t inputStructure; + SMCKeyData_t outputStructure; + + memset(&inputStructure, 0, sizeof(SMCKeyData_t)); + memset(&outputStructure, 0, sizeof(SMCKeyData_t)); + memset(val, 0, sizeof(SMCVal_t)); + + inputStructure.key = _strtoul(key, 4, 16); + inputStructure.data8 = SMC_CMD_READ_KEYINFO; + + result = SMCCall(KERNEL_INDEX_SMC, &inputStructure, &outputStructure); + if (result != kIOReturnSuccess) + return result; + + val->dataSize = outputStructure.keyInfo.dataSize; + _ultostr(val->dataType, outputStructure.keyInfo.dataType); + inputStructure.keyInfo.dataSize = val->dataSize; + inputStructure.data8 = SMC_CMD_READ_BYTES; + + result = SMCCall(KERNEL_INDEX_SMC, &inputStructure, &outputStructure); + if (result != kIOReturnSuccess) + return result; + + memcpy(val->bytes, outputStructure.bytes, sizeof(outputStructure.bytes)); + + return kIOReturnSuccess; + } + + kern_return_t SMCConnection::SMCCall(int index, SMCKeyData_t *inputStructure, SMCKeyData_t *outputStructure) { + size_t structureInputSize; + size_t structureOutputSize; + + structureInputSize = sizeof(SMCKeyData_t); + structureOutputSize = sizeof(SMCKeyData_t); + + return IOConnectCallStructMethod(conn, index, + // inputStructure + inputStructure, structureInputSize, + // ouputStructure + outputStructure, &structureOutputSize); + } + +} // namespace Cpu diff --git a/src/osx/smc.hpp b/src/osx/smc.hpp new file mode 100644 index 0000000..b7681ed --- /dev/null +++ b/src/osx/smc.hpp @@ -0,0 +1,117 @@ +/* Copyright 2021 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. + +indent = tab +tab-size = 4 +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +#define VERSION "0.01" + +#define KERNEL_INDEX_SMC 2 + +#define SMC_CMD_READ_BYTES 5 +#define SMC_CMD_WRITE_BYTES 6 +#define SMC_CMD_READ_INDEX 8 +#define SMC_CMD_READ_KEYINFO 9 +#define SMC_CMD_READ_PLIMIT 11 +#define SMC_CMD_READ_VERS 12 + +#define DATATYPE_FPE2 "fpe2" +#define DATATYPE_UINT8 "ui8 " +#define DATATYPE_UINT16 "ui16" +#define DATATYPE_UINT32 "ui32" +#define DATATYPE_SP78 "sp78" + +// key values +#define SMC_KEY_CPU_TEMP "TC0P" // proximity temp? +#define SMC_KEY_CPU_DIODE_TEMP "TC0D" // diode temp? +#define SMC_KEY_CPU_DIE_TEMP "TC0F" // die temp? +#define SMC_KEY_CPU1_TEMP "TC1C" +#define SMC_KEY_CPU2_TEMP "TC2C" // etc +#define SMC_KEY_FAN0_RPM_CUR "F0Ac" + +typedef struct { + char major; + char minor; + char build; + char reserved[1]; + UInt16 release; +} SMCKeyData_vers_t; + +typedef struct { + UInt16 version; + UInt16 length; + UInt32 cpuPLimit; + UInt32 gpuPLimit; + UInt32 memPLimit; +} SMCKeyData_pLimitData_t; + +typedef struct { + UInt32 dataSize; + UInt32 dataType; + char dataAttributes; +} SMCKeyData_keyInfo_t; + +typedef char SMCBytes_t[32]; + +typedef struct { + UInt32 key; + SMCKeyData_vers_t vers; + SMCKeyData_pLimitData_t pLimitData; + SMCKeyData_keyInfo_t keyInfo; + char result; + char status; + char data8; + UInt32 data32; + SMCBytes_t bytes; +} SMCKeyData_t; + +typedef char UInt32Char_t[5]; + +typedef struct { + UInt32Char_t key; + UInt32 dataSize; + UInt32Char_t dataType; + SMCBytes_t bytes; +} SMCVal_t; + +namespace Cpu { + class SMCConnection { + public: + SMCConnection(); + virtual ~SMCConnection(); + + long long getTemp(int core); + + private: + kern_return_t SMCReadKey(UInt32Char_t key, SMCVal_t *val); + long long getSMCTemp(char *key); + kern_return_t SMCCall(int index, SMCKeyData_t *inputStructure, SMCKeyData_t *outputStructure); + + io_connect_t conn; + kern_return_t result; + mach_port_t masterPort; + io_iterator_t iterator; + io_object_t device; + }; +} // namespace Cpu diff --git a/themes/HotPurpleTrafficLight.theme b/themes/HotPurpleTrafficLight.theme new file mode 100644 index 0000000..59f2c02 --- /dev/null +++ b/themes/HotPurpleTrafficLight.theme @@ -0,0 +1,94 @@ +#HotPurpleTrafficLight +#by Pete Allebone - mess with the best... you know the rest. +#Designed to flash up bright red with danger when loads are high and attention is needed. + +# Main background, empty for terminal default, need to be empty if you want transparent background +theme[main_bg]="#000000" + +# Main text color +theme[main_fg]="#d1d1e0" + +# Title color for boxes +theme[title]="#d1d1e0" + +# Highlight color for keyboard shortcuts +theme[hi_fg]="#9933ff" + +# Background color of selected item in processes box +theme[selected_bg]="#6666ff" + +# Foreground color of selected item in processes box +theme[selected_fg]="#d1d1e0" + +# Color of inactive/disabled text +theme[inactive_fg]="#9999ff" + +# Color of text appearing on top of graphs, i.e uptime and current network graph scaling +theme[graph_text]="#9933ff" + +# Background color of the percentage meters +theme[meter_bg]="#4d4dff" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#9933ff" + +# Cpu box outline color +theme[cpu_box]="#a64dff" + +# Memory/disks box outline color +theme[mem_box]="#a64dff" + +# Net up/down box outline color +theme[net_box]="#a64dff" + +# Processes box outline color +theme[proc_box]="#a64dff" + +# Box divider line and small boxes line color +theme[div_line]="#4d4dff" + +# Temperature graph colors +theme[temp_start]="#00ff00" +theme[temp_mid]="#ff9933" +theme[temp_end]="#ff0000" + +# CPU graph colors +theme[cpu_start]="#00ff00" +theme[cpu_mid]="#ccff66" +theme[cpu_end]="#ff0000" + +# Mem/Disk free meter +theme[free_end]="#00ff00" +theme[free_mid]="#ccff66" +theme[free_start]="#ff0000" + +# Mem/Disk cached meter +theme[cached_start]="#00ff00" +theme[cached_mid]="#ccff66" +theme[cached_end]="#ff0000" + +# Mem/Disk available meter +theme[available_start]="#ff0000" +theme[available_mid]="#ccff66" +theme[available_end]="#00ff00" + +# Mem/Disk used meter +theme[used_start]="#00ff00" +theme[used_mid]="#ccff66" +theme[used_end]="#ff0000" + +# Download graph colors +theme[download_start]="#00ff00" +theme[download_mid]="#ff9933" +theme[download_end]="#ff0000" + +# Upload graph colors +theme[upload_start]="#00ff00" +theme[upload_mid]="#ff9933" +theme[upload_end]="#ff0000" + +# Process box color gradient for threads, mem and cpu usage +theme[process_start]="#9999ff" +theme[process_mid]="#4d4dff" +theme[process_end]="#a64dff" + diff --git a/themes/ayu.theme b/themes/ayu.theme new file mode 100644 index 0000000..58d89b4 --- /dev/null +++ b/themes/ayu.theme @@ -0,0 +1,89 @@ +# Main background, empty for terminal default, need to be empty if you want transparent background +theme[main_bg]="#0B0E14" + +# Main text color +theme[main_fg]="#BFBDB6" + +# Title color for boxes +theme[title]="#BFBDB6" + +# Highlight color for keyboard shortcuts +theme[hi_fg]="#E6B450" + +# Background color of selected item in processes box +theme[selected_bg]="#E6B450" + +# Foreground color of selected item in processes box +theme[selected_fg]="#f8f8f2" + +# Color of inactive/disabled text +theme[inactive_fg]="#565B66" + +# Color of text appearing on top of graphs, i.e uptime and current network graph scaling +theme[graph_text]="#BFBDB6" + +# Background color of the percentage meters +theme[meter_bg]="#565B66" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#DFBFFF" + +# Cpu box outline color +theme[cpu_box]="#DFBFFF" + +# Memory/disks box outline color +theme[mem_box]="#95E6CB" + +# Net up/down box outline color +theme[net_box]="#F28779" + +# Processes box outline color +theme[proc_box]="#E6B673" + +# Box divider line and small boxes line color +theme[div_line]="#565B66" + +# Temperature graph colors +theme[temp_start]="#DFBFFF" +theme[temp_mid]="#D2A6FF" +theme[temp_end]="#A37ACC" + +# CPU graph colors +theme[cpu_start]="#DFBFFF" +theme[cpu_mid]="#D2A6FF" +theme[cpu_end]="#A37ACC" + +# Mem/Disk free meter +theme[free_start]="#95E6CB" +theme[free_mid]="#95E6CB" +theme[free_end]="#4CBF99" + +# Mem/Disk cached meter +theme[cached_start]="#95E6CB" +theme[cached_mid]="#95E6CB" +theme[cached_end]="#4CBF99" + +# Mem/Disk available meter +theme[available_start]="#95E6CB" +theme[available_mid]="#95E6CB" +theme[available_end]="#4CBF99" + +# Mem/Disk used meter +theme[used_start]="#95E6CB" +theme[used_mid]="#95E6CB" +theme[used_end]="#4CBF99" + +# Download graph colors +theme[download_start]="#F28779" +theme[download_mid]="#F07178" +theme[download_end]="#F07171" + +# Upload graph colors +theme[upload_start]="#73D0FF" +theme[upload_mid]="#59C2FF" +theme[upload_end]="#399EE6" + +# Process box color gradient for threads, mem and cpu usage +theme[process_start]="#FFCC66" +theme[process_mid]="#E6B450" +theme[process_end]="#FFAA33" diff --git a/themes/everforest-dark-hard.theme b/themes/everforest-dark-hard.theme new file mode 100644 index 0000000..51a5a03 --- /dev/null +++ b/themes/everforest-dark-hard.theme @@ -0,0 +1,93 @@ +# Btop everforest dark hard theme by u/archontop. + +# 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]="#2b3339" + +# Main text color +theme[main_fg]="#d3c6aa" + +# Title color for boxes +theme[title]="#d3c6aa" + +# Highlight color for keyboard shortcuts +theme[hi_fg]="#e67e80" + +# Background color of selected items +theme[selected_bg]="#4b565c" + +# Foreground color of selected items +theme[selected_fg]="#dbbc7f" + +# Color of inactive/disabled text +theme[inactive_fg]="#2b3339" + +# Color of text appearing on top of graphs, i.e uptime and current network graph scaling +theme[graph_text]="#d3c6aa" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#a7c080" + +# Cpu box outline color +theme[cpu_box]="#4b565c" + +# Memory/disks box outline color +theme[mem_box]="#4b565c" + +# Net up/down box outline color +theme[net_box]="#4b565c" + +# Processes box outline color +theme[proc_box]="#4b565c" + +# Box divider line and small boxes line color +theme[div_line]="#4b565c" + +# Temperature graph colors +theme[temp_start]="#a7c080" +theme[temp_mid]="#dbbc7f" +theme[temp_end]="#f85552" + +# CPU graph colors +theme[cpu_start]="#a7c080" +theme[cpu_mid]="#dbbc7f" +theme[cpu_end]="#f85552" + +# Mem/Disk free meter +theme[free_start]="#f85552" +theme[free_mid]="#dbbc7f" +theme[free_end]="#a7c080" + +# Mem/Disk cached meter +theme[cached_start]="#7fbbb3" +theme[cached_mid]="#83c092" +theme[cached_end]="#a7c080" + +# Mem/Disk available meter +theme[available_start]="#f85552" +theme[available_mid]="#dbbc7f" +theme[available_end]="#a7c080" + +# Mem/Disk used meter +theme[used_start]="#a7c080" +theme[used_mid]="#dbbc7f" +theme[used_end]="#f85552" + +# Download graph colors +theme[download_start]="#8da101" +theme[download_mid]="#83c092" +theme[download_end]="#a7c080" + +# Upload graph colors +theme[upload_start]="#f85552" +theme[upload_mid]="#dbbc7f" +theme[upload_end]="#a7c080" + +# Process box color gradient for threads, mem and cpu usage +theme[process_start]="#a7c080" +theme[process_mid]="#f85552" +theme[process_end]="#CC241D" diff --git a/themes/gruvbox_dark_v2.theme b/themes/gruvbox_dark_v2.theme new file mode 100644 index 0000000..a3a0c9e --- /dev/null +++ b/themes/gruvbox_dark_v2.theme @@ -0,0 +1,98 @@ +# Bashtop gruvbox (https://github.com/morhetz/gruvbox) theme +# First version created By BachoSeven +# Adjustments to proper colors by Pietryszak (https://github.com/pietryszak/) + +# 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]="#282828" + +# Main text color +theme[main_fg]="#EBDBB2" + +# Title color for boxes +theme[title]="#EBDBB2" + +# Highlight color for keyboard shortcuts +theme[hi_fg]="#CC241D" + +# Background color of selected items +theme[selected_bg]="#32302F" + +# Foreground color of selected items +theme[selected_fg]="#D3869B" + +# Color of inactive/disabled text +theme[inactive_fg]="#3C3836" + +# Color of text appearing on top of graphs, i.e uptime and current network graph scaling +theme[graph_text]="#A89984" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#98971A" + +# Cpu box outline color +theme[cpu_box]="#A89984" + +# Memory/disks box outline color +theme[mem_box]="#A89984" + +# Net up/down box outline color +theme[net_box]="#A89984" + +# Processes box outline color +theme[proc_box]="#A89984" + +# Box divider line and small boxes line color +theme[div_line]="#A89984" + +# Temperature graph colors +theme[temp_start]="#98971A" +theme[temp_mid]="" +theme[temp_end]="#CC241D" + +# CPU graph colors +theme[cpu_start]="#8EC07C" +theme[cpu_mid]="#D79921" +theme[cpu_end]="#CC241D" + +# Mem/Disk free meter +theme[free_start]="#CC241D" +theme[free_mid]="#D79921" +theme[free_end]="#8EC07C" + +# Mem/Disk cached meter +theme[cached_start]="#458588" +theme[cached_mid]="#83A598" +theme[cached_end]="#8EC07C" + +# Mem/Disk available meter +theme[available_start]="#CC241D" +theme[available_mid]="#D65D0E" +theme[available_end]="#FABD2F" + +# Mem/Disk used meter +theme[used_start]="#8EC07C" +theme[used_mid]="#D65D0E" +theme[used_end]="#CC241D" + +# Download graph colors +theme[download_start]="#98971A" +theme[download_mid]="#689d6A" +theme[download_end]="#B8BB26" + +# Upload graph colors +theme[upload_start]="#CC241D" +theme[upload_mid]="#D65d0E" +theme[upload_end]="#FABF2F" + +# Process box color gradient for threads, mem and cpu usage +theme[process_start]="#8EC07C" +theme[process_mid]="#FE8019" +theme[process_end]="#CC241D" diff --git a/themes/gruvbox_material_dark.theme b/themes/gruvbox_material_dark.theme new file mode 100644 index 0000000..749c7a7 --- /dev/null +++ b/themes/gruvbox_material_dark.theme @@ -0,0 +1,92 @@ +# Btop gruvbox material dark (https://github.com/sainnhe/gruvbox-material) theme +# by Marco Radocchia + +# 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]="#282828" + +# Main text color +theme[main_fg]="#d4be98" + +# Title color for boxes +theme[title]="#d4be98" + +# Higlight color for keyboard shortcuts +theme[hi_fg]="#ea6962" + +# Background color of selected items +theme[selected_bg]="#d8a657" + +# Foreground color of selected items +theme[selected_fg]="#282828" + +# Color of inactive/disabled text +theme[inactive_fg]="#282828" + +# Color of text appearing on top of graphs, i.e uptime and current network graph scaling +theme[graph_text]="#665c54" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#a9b665" + +# Cpu box outline color +theme[cpu_box]="#7c6f64" + +# Memory/disks box outline color +theme[mem_box]="#7c6f64" + +# Net up/down box outline color +theme[net_box]="#7c6f64" + +# Processes box outline color +theme[proc_box]="#7c6f64" + +# Box divider line and small boxes line color +theme[div_line]="#7c6f64" + +# Temperature graph colors +theme[temp_start]="#7daea3" +theme[temp_mid]="#e78a4e" +theme[temp_end]="#ea6962" + +# CPU graph colors +theme[cpu_start]="#a9b665" +theme[cpu_mid]="#d8a657" +theme[cpu_end]="#ea6962" + +# Mem/Disk free meter +theme[free_start]="#89b482" +theme[free_mid]="" +theme[free_end]="" + +# Mem/Disk cached meter +theme[cached_start]="#7daea3" +theme[cached_mid]="" +theme[cached_end]="" + +# Mem/Disk available meter +theme[available_start]="#d8a657" +theme[available_mid]="" +theme[available_end]="" + +# Mem/Disk used meter +theme[used_start]="#ea6962" +theme[used_mid]="" +theme[used_end]="" + +# Download graph colors +theme[download_start]="#e78a4e" +theme[download_mid]="" +theme[download_end]="" + +# Upload graph colors +theme[upload_start]="#d3869b" +theme[upload_mid]="" +theme[upload_end]="" diff --git a/themes/night-owl.theme b/themes/night-owl.theme new file mode 100644 index 0000000..7537fea --- /dev/null +++ b/themes/night-owl.theme @@ -0,0 +1,92 @@ +#Bashtop theme with night-owl colors +#by zkourouma + +# 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]="#011627" + +# Main text color +theme[main_fg]="#d6deeb" + +# Title color for boxes +theme[title]="#ffffff" + +# Higlight color for keyboard shortcuts +theme[hi_fg]="#addb67" + +# Background color of selected items +theme[selected_bg]="#000000" + +# Foreground color of selected items +theme[selected_fg]="#ffeb95" + +# Color of inactive/disabled text +theme[inactive_fg]="#575656" + +# Color of text appearing on top of graphs, i.e uptime and current network graph scaling +theme[graph_text]="#585858" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#22da6e" + +# Cpu box outline color +theme[cpu_box]="#ffffff" + +# Memory/disks box outline color +theme[mem_box]="#ffffff" + +# Net up/down box outline color +theme[net_box]="#ffffff" + +# Processes box outline color +theme[proc_box]="#ffffff" + +# Box divider line and small boxes line color +theme[div_line]="#ffffff" + +# Temperature graph colors +theme[temp_start]="#82aaff" +theme[temp_mid]="#c792ea" +theme[temp_end]="#fb4394" + +# CPU graph colors +theme[cpu_start]="#22da6e" +theme[cpu_mid]="#addb67" +theme[cpu_end]="#ef5350" + +# Mem/Disk free meter +theme[free_start]="#4e5900" +theme[free_mid]="" +theme[free_end]="#22da6e" + +# Mem/Disk cached meter +theme[cached_start]="#82aaff" +theme[cached_mid]="" +theme[cached_end]="#82aaff" + +# Mem/Disk available meter +theme[available_start]="#addb67" +theme[available_mid]="" +theme[available_end]="#ffeb95" + +# Mem/Disk used meter +theme[used_start]="#ef5350" +theme[used_mid]="" +theme[used_end]="#ef5350" + +# Download graph colors +theme[download_start]="#3d4070" +theme[download_mid]="#6c71c4" +theme[download_end]="#a3a8f7" + +# Upload graph colors +theme[upload_start]="#701c45" +theme[upload_mid]="#c792ea" +theme[upload_end]="#c792ea" diff --git a/themes/onedark.theme b/themes/onedark.theme new file mode 100644 index 0000000..f4441de --- /dev/null +++ b/themes/onedark.theme @@ -0,0 +1,81 @@ +# Theme: OneDark +# By: Vitor Melo + +# Main bg +theme[main_bg]="#282c34" + +# Main text color +theme[main_fg]="#abb2bf" + +# Title color for boxes +theme[title]="#abb2bf" + +# Higlight color for keyboard shortcuts +theme[hi_fg]="#61afef" + +# Background color of selected item in processes box +theme[selected_bg]="#2c313c" + +# Foreground color of selected item in processes box +theme[selected_fg]="#abb2bf" + +# Color of inactive/disabled text +theme[inactive_fg]="#5c6370" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#61afef" + +# Cpu box outline color +theme[cpu_box]="#5c6370" + +# Memory/disks box outline color +theme[mem_box]="#5c6370" + +# Net up/down box outline color +theme[net_box]="#5c6370" + +# Processes box outline color +theme[proc_box]="#5c6370" + +# Box divider line and small boxes line color +theme[div_line]="#5c6370" + +# Temperature graph colors +theme[temp_start]="#98c379" +theme[temp_mid]="#e5c07b" +theme[temp_end]="#e06c75" + +# CPU graph colors +theme[cpu_start]="#98c379" +theme[cpu_mid]="#e5c07b" +theme[cpu_end]="#e06c75" + +# Mem/Disk free meter +theme[free_start]="#98c379" +theme[free_mid]="#e5c07b" +theme[free_end]="#e06c75" + +# Mem/Disk cached meter +theme[cached_start]="#98c379" +theme[cached_mid]="#e5c07b" +theme[cached_end]="#e06c75" + +# Mem/Disk available meter +theme[available_start]="#98c379" +theme[available_mid]="#e5c07b" +theme[available_end]="#e06c75" + +# Mem/Disk used meter +theme[used_start]="#98c379" +theme[used_mid]="#e5c07b" +theme[used_end]="#e06c75" + +# Download graph colors +theme[download_start]="#98c379" +theme[download_mid]="#e5c07b" +theme[download_end]="#e06c75" + +# Upload graph colors +theme[upload_start]="#98c379" +theme[upload_mid]="#e5c07b" +theme[upload_end]="#e06c75" diff --git a/themes/tokyo-night.theme b/themes/tokyo-night.theme new file mode 100644 index 0000000..aae6a3b --- /dev/null +++ b/themes/tokyo-night.theme @@ -0,0 +1,81 @@ +# Theme: tokyo-night +# By: Pascal Jaeger + +# Main bg +theme[main_bg]="#1a1b26" + +# Main text color +theme[main_fg]="#cfc9c2" + +# Title color for boxes +theme[title]="#cfc9c2" + +# Higlight color for keyboard shortcuts +theme[hi_fg]="#7dcfff" + +# Background color of selected item in processes box +theme[selected_bg]="#414868" + +# Foreground color of selected item in processes box +theme[selected_fg]="#cfc9c2" + +# Color of inactive/disabled text +theme[inactive_fg]="#565f89" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#7dcfff" + +# Cpu box outline color +theme[cpu_box]="#565f89" + +# Memory/disks box outline color +theme[mem_box]="#565f89" + +# Net up/down box outline color +theme[net_box]="#565f89" + +# Processes box outline color +theme[proc_box]="#565f89" + +# Box divider line and small boxes line color +theme[div_line]="#565f89" + +# Temperature graph colors +theme[temp_start]="#9ece6a" +theme[temp_mid]="#e0af68" +theme[temp_end]="#f7768e" + +# CPU graph colors +theme[cpu_start]="#9ece6a" +theme[cpu_mid]="#e0af68" +theme[cpu_end]="#f7768e" + +# Mem/Disk free meter +theme[free_start]="#9ece6a" +theme[free_mid]="#e0af68" +theme[free_end]="#f7768e" + +# Mem/Disk cached meter +theme[cached_start]="#9ece6a" +theme[cached_mid]="#e0af68" +theme[cached_end]="#f7768e" + +# Mem/Disk available meter +theme[available_start]="#9ece6a" +theme[available_mid]="#e0af68" +theme[available_end]="#f7768e" + +# Mem/Disk used meter +theme[used_start]="#9ece6a" +theme[used_mid]="#e0af68" +theme[used_end]="#f7768e" + +# Download graph colors +theme[download_start]="#9ece6a" +theme[download_mid]="#e0af68" +theme[download_end]="#f7768e" + +# Upload graph colors +theme[upload_start]="#9ece6a" +theme[upload_mid]="#e0af68" +theme[upload_end]="#f7768e" diff --git a/themes/tokyo-storm.theme b/themes/tokyo-storm.theme new file mode 100644 index 0000000..a4edfcf --- /dev/null +++ b/themes/tokyo-storm.theme @@ -0,0 +1,81 @@ +# Theme: tokyo-storm +# By: Pascal Jaeger + +# Main bg +theme[main_bg]="#24283b" + +# Main text color +theme[main_fg]="#cfc9c2" + +# Title color for boxes +theme[title]="#cfc9c2" + +# Higlight color for keyboard shortcuts +theme[hi_fg]="#7dcfff" + +# Background color of selected item in processes box +theme[selected_bg]="#414868" + +# Foreground color of selected item in processes box +theme[selected_fg]="#cfc9c2" + +# Color of inactive/disabled text +theme[inactive_fg]="#565f89" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#7dcfff" + +# Cpu box outline color +theme[cpu_box]="#565f89" + +# Memory/disks box outline color +theme[mem_box]="#565f89" + +# Net up/down box outline color +theme[net_box]="#565f89" + +# Processes box outline color +theme[proc_box]="#565f89" + +# Box divider line and small boxes line color +theme[div_line]="#565f89" + +# Temperature graph colors +theme[temp_start]="#9ece6a" +theme[temp_mid]="#e0af68" +theme[temp_end]="#f7768e" + +# CPU graph colors +theme[cpu_start]="#9ece6a" +theme[cpu_mid]="#e0af68" +theme[cpu_end]="#f7768e" + +# Mem/Disk free meter +theme[free_start]="#9ece6a" +theme[free_mid]="#e0af68" +theme[free_end]="#f7768e" + +# Mem/Disk cached meter +theme[cached_start]="#9ece6a" +theme[cached_mid]="#e0af68" +theme[cached_end]="#f7768e" + +# Mem/Disk available meter +theme[available_start]="#9ece6a" +theme[available_mid]="#e0af68" +theme[available_end]="#f7768e" + +# Mem/Disk used meter +theme[used_start]="#9ece6a" +theme[used_mid]="#e0af68" +theme[used_end]="#f7768e" + +# Download graph colors +theme[download_start]="#9ece6a" +theme[download_mid]="#e0af68" +theme[download_end]="#f7768e" + +# Upload graph colors +theme[upload_start]="#9ece6a" +theme[upload_mid]="#e0af68" +theme[upload_end]="#f7768e" diff --git a/themes/tomorrow-night.theme b/themes/tomorrow-night.theme new file mode 100644 index 0000000..a745005 --- /dev/null +++ b/themes/tomorrow-night.theme @@ -0,0 +1,89 @@ +#Nord theme but using the Tomorrow Night palette +#by Appuchia + +# 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]="#1d1f21" + +# Main text color +theme[main_fg]="#c5c8c6" + +# Title color for boxes +theme[title]="#c5c8c6" + +# Higlight color for keyboard shortcuts +theme[hi_fg]="#81beb7" + +# Background color of selected item in processes box +theme[selected_bg]="#282a2e" + +# Foreground color of selected item in processes box +theme[selected_fg]="#c5c8c6" + +# Color of inactive/disabled text +theme[inactive_fg]="#373b41" + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]="#969896" + +# Cpu box outline color +theme[cpu_box]="#81a2be" + +# Memory/disks box outline color +theme[mem_box]="#81a2be" + +# Net up/down box outline color +theme[net_box]="#81a2be" + +# Processes box outline color +theme[proc_box]="#81a2be" + +# Box divider line and small boxes line color +theme[div_line]="#81a2be" + +# Temperature graph colors +theme[temp_start]="#b5bd68" +theme[temp_mid]="#f0c674" +theme[temp_end]="#cc6666" + +# CPU graph colors +theme[cpu_start]="#b5bd68" +theme[cpu_mid]="#f0c674" +theme[cpu_end]="#cc6666" + +# Mem/Disk free meter +theme[free_start]="#b5bd68" +theme[free_mid]="#f0c674" +theme[free_end]="#cc6666" + +# Mem/Disk cached meter +theme[cached_start]="#b5bd68" +theme[cached_mid]="#f0c674" +theme[cached_end]="#cc6666" + +# Mem/Disk available meter +theme[available_start]="#b5bd68" +theme[available_mid]="#f0c674" +theme[available_end]="#cc6666" + +# Mem/Disk used meter +theme[used_start]="#b5bd68" +theme[used_mid]="#f0c674" +theme[used_end]="#cc6666" + +# Download graph colors +theme[download_start]="#b5bd68" +theme[download_mid]="#f0c674" +theme[download_end]="#cc6666" + +# Upload graph colors +theme[upload_start]="#b5bd68" +theme[upload_mid]="#f0c674" +theme[upload_end]="#cc6666"