other: fix merge conflicts

This commit is contained in:
ClementTsang 2021-09-26 00:58:18 -04:00
commit b6ca3e0a22
27 changed files with 3661 additions and 512 deletions

View File

@ -189,6 +189,24 @@
"doc",
"platform"
]
},
{
"login": "adiabatic",
"name": "adiabatic",
"avatar_url": "https://avatars.githubusercontent.com/u/101246?v=4",
"profile": "https://www.frogorbits.com/",
"contributions": [
"doc"
]
},
{
"login": "bowlofeggs",
"name": "Randy Barlow",
"avatar_url": "https://avatars.githubusercontent.com/u/354506?v=4",
"profile": "https://electronsweatshop.com",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@ -6,7 +6,7 @@ labels: "bug"
assignees: ""
---
Before you report, please take a look at [Troubleshooting](https://clementtsang.github.io/bottom/nightly/troubleshooting) to see if there's already an answer to your problem. Also check out [existing issues](https://github.com/ClementTsang/bottom/issues) and [known problems](https://clementtsang.github.io/bottom/nightly/support/#known-problems) to see if your problem is already reported/known.
Before you report, please take a look at [Troubleshooting](https://clementtsang.github.io/bottom/nightly/troubleshooting) to see if there's already an answer to your problem. Also check out [existing issues](https://github.com/ClementTsang/bottom/issues) and [known problems](https://clementtsang.github.io/bottom/nightly/support/#known-problems) to see if your problem is already reported/known/fixed.
Also, **please fill in all fields if possible** - if the issue is too hard to reproduce or vague, it may not be fixed!
@ -24,8 +24,7 @@ Please mention what terminal/terminal emulator you are using `bottom` on (ex: Ko
## What version are you on?
Please ensure that the bug still exists on the [latest stable release](https://github.com/ClementTsang/bottom/releases/latest) or newer (i.e. nightly). If so, mention
what version you are using here.
Please mention which version of bottom you're running (`btm -V`, nightly, etc.)!
## How did you install `bottom`?

View File

@ -1,7 +1,7 @@
name: audit
on:
schedule:
- cron: "0 0 * * *"
- cron: "0 0 * * 1"
jobs:
audit:
runs-on: ubuntu-latest

View File

@ -10,21 +10,20 @@ on:
workflow_dispatch:
pull_request:
paths-ignore:
- 'README.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- "README.md"
- "docs/**"
- ".github/ISSUE_TEMPLATE/**"
push:
branches:
- master
paths-ignore:
- 'README.md'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
- 'CHANGELOG.md'
- 'CONTRIBUTING.md'
- "README.md"
- "docs/**"
- ".github/ISSUE_TEMPLATE/**"
- "CHANGELOG.md"
- "CONTRIBUTING.md"
jobs:
# Check rustfmt
rustfmt:
runs-on: ${{ matrix.os }}
strategy:
@ -44,10 +43,9 @@ jobs:
components: rustfmt
- uses: Swatinem/rust-cache@v1
- run: cargo fmt --all -- --check
# Check clippy. Note that this doesn't check ARM.
clippy:
runs-on: ${{ matrix.os }}
strategy:
@ -69,8 +67,6 @@ jobs:
- uses: Swatinem/rust-cache@v1
# TODO: Can probably put cache here in the future; I'm worried if this will cause issues with clippy though since cargo check breaks it; maybe wait until 1.52, when fix lands.
- run: cargo clippy --all-targets --workspace -- -D warnings
# Compile/check/test.
@ -174,6 +170,14 @@ jobs:
rust: stable,
}
# Risc-V 64gc
- {
os: "ubuntu-latest",
target: "riscv64gc-unknown-linux-gnu",
cross: true,
rust: stable,
}
# macOS ARM
- {
os: "macOS-latest",
@ -194,12 +198,22 @@ jobs:
target: ${{ matrix.triple.target }}
- uses: Swatinem/rust-cache@v1
with:
key: ${{ matrix.triple.target }}
- name: Check
uses: actions-rs/cargo@v1
with:
command: check
args: --all-targets --verbose --target=${{ matrix.triple.target }} --no-default-features
args: --all-targets --verbose --target=${{ matrix.triple.target }} --features "battery"
use-cross: ${{ matrix.triple.cross }}
- name: Check without battery feature on the main 3
if: matrix.triple.toTest == 'true'
uses: actions-rs/cargo@v1
with:
command: check
args: --all-targets --verbose --target=${{ matrix.triple.target }}
use-cross: ${{ matrix.triple.cross }}
- name: Run tests

View File

@ -1,4 +1,4 @@
# How we deploy a release. Covers binary builds. Also manages packaging for winget, choco, and homebrew.
# How we deploy a release. Covers binary builds. Also manages packaging for winget and choco.
#
# Based on https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/release.yml
@ -75,6 +75,7 @@ jobs:
target: "x86_64-unknown-linux-gnu",
cross: false,
artifact: true,
strip: true,
}
- {
os: "ubuntu-18.04",
@ -82,31 +83,46 @@ jobs:
cross: false,
container: quay.io/pypa/manylinux2014_x86_64,
suffix: "2-17",
strip: true,
}
- {
os: "ubuntu-18.04",
target: "i686-unknown-linux-gnu",
cross: true,
strip: true,
}
- {
os: "ubuntu-18.04",
target: "x86_64-unknown-linux-musl",
cross: false,
artifact: true
artifact: true,
strip: true,
}
- {
os: "ubuntu-18.04",
target: "i686-unknown-linux-musl",
cross: true,
strip: true,
}
- {
os: "macOS-latest",
target: "x86_64-apple-darwin",
cross: false,
artifact: true,
strip: true,
}
- { os: "macOS-latest", target: "x86_64-apple-darwin", cross: false, artifact: true }
- {
os: "windows-2019",
target: "x86_64-pc-windows-msvc",
cross: false,
artifact: true,
}
- { os: "windows-2019", target: "i686-pc-windows-msvc", cross: false, artifact: true }
- {
os: "windows-2019",
target: "i686-pc-windows-msvc",
cross: false,
artifact: true,
}
- {
os: "windows-2019",
target: "x86_64-pc-windows-gnu",
@ -118,7 +134,7 @@ jobs:
os: "ubuntu-18.04",
target: "aarch64-unknown-linux-gnu",
cross: true,
artifact: true
artifact: true,
}
# armv7
@ -126,7 +142,7 @@ jobs:
os: "ubuntu-18.04",
target: "armv7-unknown-linux-gnueabihf",
cross: true,
artifact: true
artifact: true,
}
# PowerPC 64 LE
@ -136,14 +152,19 @@ jobs:
cross: true,
}
# Risc-V 64gc
- {
os: "ubuntu-18.04",
target: "riscv64gc-unknown-linux-gnu",
cross: true,
}
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- uses: actions/setup-python@v2
- name: Get release download URL
uses: actions/download-artifact@v2
with:
@ -163,22 +184,6 @@ jobs:
echo "Release upload url: ${{ env.RELEASE_UPLOAD_URL }}"
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Install Net-Framework-Core (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
shell: powershell
run: Install-WindowsFeature Net-Framework-Core
- name: Install wixtoolset (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
uses: crazy-max/ghaction-chocolatey@v1.4.0
with:
args: install -y wixtoolset
# - name: Export wixtoolset to path (Windows x86-64 MSVC)
# if: matrix.triple.target == 'x86_64-pc-windows-msvc'
# shell: powershell
# run: export PATH=${PATH}:"/c/Program Files (x86)/WiX Toolset v3.11/bin"
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
@ -188,21 +193,24 @@ jobs:
target: ${{ matrix.triple.target }}
- uses: Swatinem/rust-cache@v1
with:
key: ${{ matrix.triple.target }}
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
args: --release --verbose --target=${{ matrix.triple.target }} --no-default-features
args: --release --verbose --target=${{ matrix.triple.target }} --features "battery"
use-cross: ${{ matrix.triple.cross }}
- name: Move autocomplete to working directory
shell: bash
run: |
cp -r ./target/${{ matrix.triple.target }}/release/build/bottom-*/out completion
mkdir completion
cp -r ./target/${{ matrix.triple.target }}/release/build/bottom-*/out/. completion
- name: Strip release binary (macOS or Linux x86-64/i686)
if: matrix.triple.os != 'windows-2019' && matrix.triple.target != 'aarch64-unknown-linux-gnu' && matrix.triple.target != 'armv7-unknown-linux-gnueabihf' && matrix.triple.target != 'powerpc64le-unknown-linux-gnu'
if: matrix.triple.strip == true
run: |
strip target/${{ matrix.triple.target }}/release/btm
@ -246,16 +254,85 @@ jobs:
name: artifacts
path: artifacts
- name: Build msi file (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
- name: Compress completion files (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == ''
shell: bash
run: |
tar -C ./completion -czvf completion.tar.gz .
- name: Release completion files (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == ''
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
asset_path: completion.tar.gz
asset_name: completion.tar.gz
asset_content_type: application/octet-stream
build-msi:
name: build-msi
needs: [create-github-release]
runs-on: "windows-2019"
env:
RUST_BACKTRACE: 1
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- uses: actions/setup-python@v2
- name: Get release download URL
uses: actions/download-artifact@v2
with:
name: artifacts
path: artifacts
- name: Set release upload URL and release version
shell: bash
run: |
release_upload_url="$(cat ./artifacts/release-upload-url)"
echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV
release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release upload url: ${{ env.RELEASE_UPLOAD_URL }}"
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Install Net-Framework-Core (Windows x86-64 MSVC)
shell: powershell
run: Install-WindowsFeature Net-Framework-Core
- name: Install wixtoolset (Windows x86-64 MSVC)
uses: crazy-max/ghaction-chocolatey@v1.4.0
with:
args: install -y wixtoolset
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
target: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@v1
with:
key: x86_64-pc-windows-msvc-msi
- name: Build msi file
shell: powershell
run: |
cargo install cargo-wix --version 0.3.1 --locked
cargo wix init
cargo wix
- name: Upload msi file (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
- name: Upload msi file
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -265,15 +342,13 @@ jobs:
asset_name: bottom_x86_64_installer.msi
asset_content_type: application/octet-stream
- name: Build winget (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
- name: Build winget
run: |
python "./deployment/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/windows/winget/winget.yaml.template" "Clement.bottom.yaml" "SHA256" "./bottom_x86_64_installer.msi"
$Code = powershell ./deployment/windows/winget/get_product_code.ps1 ./bottom_x86_64_installer.msi
python "./deployment/windows/winget/product_code.py" Clement.bottom.yaml $Code
- name: Upload winget file (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
- name: Upload winget file
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -283,15 +358,56 @@ jobs:
asset_name: Clement.bottom.yaml
asset_content_type: application/octet-stream
build-deb:
name: build-deb
needs: [create-github-release]
runs-on: "ubuntu-18.04"
env:
RUST_BACKTRACE: 1
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Get release download URL
uses: actions/download-artifact@v2
with:
name: artifacts
path: artifacts
- name: Set release upload URL and release version
shell: bash
run: |
release_upload_url="$(cat ./artifacts/release-upload-url)"
echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV
release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release upload url: ${{ env.RELEASE_UPLOAD_URL }}"
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
target: x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@v1
with:
key: x86_64-unknown-linux-gnu-deb
- name: Build Debian release (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == ''
run: |
cargo install cargo-deb --version 1.29.0 --locked
cargo deb
cp ./target/debian/bottom_*.deb ./bottom_${{ env.RELEASE_VERSION }}_amd64.deb
- name: Upload Debian file (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == ''
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -346,34 +462,3 @@ jobs:
asset_path: choco.zip
asset_name: choco.zip
asset_content_type: application/octet-stream
- name: Execute Homebrew packaging script
run: |
python "./deployment/packager.py" ${{ env.RELEASE_VERSION }} "./deployment/macos/homebrew/bottom.rb.template" "./bottom.rb" "SHA256" "./artifacts/bottom_x86_64-apple-darwin.tar.gz" "./artifacts/bottom_x86_64-unknown-linux-musl.tar.gz";
- name: Upload bottom.rb to release
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
asset_path: bottom.rb
asset_name: bottom.rb
asset_content_type: application/octet-stream
- name: Compress completion files (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == ''
shell: bash
run: |
tar -C ./completion -czvf completion.tar.gz .
- name: Release completion files (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == ''
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
asset_path: completion.tar.gz
asset_name: completion.tar.gz
asset_content_type: application/octet-stream

View File

@ -26,11 +26,11 @@ jobs:
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: pip install mkdocs-material==7.2.6
- run: pip install mdx_truly_sane_lists
- run: pip install mdx_truly_sane_lists==1.2
- run: pip install mike
- run: pip install mike==1.1.0
- name: Configure git user and email
run: |

View File

@ -1,5 +1,5 @@
# Creates nightly deployment builds for main targets. Note this does not cover package distribution channels,
# such as choco, Homebrew, etc.
# such as choco.
name: nightly
@ -84,6 +84,7 @@ jobs:
os: "ubuntu-18.04",
target: "x86_64-unknown-linux-gnu",
cross: false,
strip: true,
}
- {
os: "ubuntu-18.04",
@ -91,23 +92,32 @@ jobs:
cross: false,
container: quay.io/pypa/manylinux2014_x86_64,
suffix: "2-17",
strip: true,
}
- {
os: "ubuntu-18.04",
target: "i686-unknown-linux-gnu",
cross: true,
strip: true,
}
- {
os: "ubuntu-18.04",
target: "x86_64-unknown-linux-musl",
cross: false,
strip: true,
}
- {
os: "ubuntu-18.04",
target: "i686-unknown-linux-musl",
cross: true,
strip: true,
}
- {
os: "macOS-latest",
target: "x86_64-apple-darwin",
cross: false,
strip: true,
}
- { os: "macOS-latest", target: "x86_64-apple-darwin", cross: false }
- {
os: "windows-2019",
target: "x86_64-pc-windows-msvc",
@ -141,14 +151,19 @@ jobs:
cross: true,
}
# Risc-V 64gc
- {
os: "ubuntu-18.04",
target: "riscv64gc-unknown-linux-gnu",
cross: true,
}
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- uses: actions/setup-python@v2
- name: Get release download URL
uses: actions/download-artifact@v2
with:
@ -168,17 +183,6 @@ jobs:
echo "Release upload url: ${{ env.RELEASE_UPLOAD_URL }}"
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Install Net-Framework-Core (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
shell: powershell
run: Install-WindowsFeature Net-Framework-Core
- name: Install wixtoolset (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
uses: crazy-max/ghaction-chocolatey@v1.4.0
with:
args: install -y wixtoolset
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
@ -188,21 +192,24 @@ jobs:
target: ${{ matrix.triple.target }}
- uses: Swatinem/rust-cache@v1
with:
key: ${{ matrix.triple.target }}
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
args: --release --verbose --target=${{ matrix.triple.target }} --no-default-features
args: --release --verbose --target=${{ matrix.triple.target }} --features "battery"
use-cross: ${{ matrix.triple.cross }}
- name: Move autocomplete to working directory
shell: bash
run: |
cp -r ./target/${{ matrix.triple.target }}/release/build/bottom-*/out completion
mkdir completion
cp -r ./target/${{ matrix.triple.target }}/release/build/bottom-*/out/. completion
- name: Strip release binary (macOS or Linux x86-64/i686)
if: matrix.triple.os != 'windows-2019' && matrix.triple.target != 'aarch64-unknown-linux-gnu' && matrix.triple.target != 'armv7-unknown-linux-gnueabihf' && matrix.triple.target != 'powerpc64le-unknown-linux-gnu'
if: matrix.triple.strip == true
run: |
strip target/${{ matrix.triple.target }}/release/btm
@ -225,8 +232,6 @@ jobs:
tar -czvf bottom_${{ matrix.triple.target }}${{ matrix.triple.suffix }}.tar.gz btm completion
echo "ASSET=bottom_${{ matrix.triple.target }}${{ matrix.triple.suffix }}.tar.gz" >> $GITHUB_ENV
# TODO: Move this elsewhere; do this all at once, and do not continue if any fails. Store artifacts. Do the same for deployment.
- name: Upload main release
if: github.event.inputs.isMock != 'mock'
uses: actions/upload-release-asset@v1.0.1
@ -239,43 +244,6 @@ jobs:
asset_name: ${{ env.ASSET }}
asset_content_type: application/octet-stream
- name: Build msi file (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc'
shell: powershell
run: |
cargo install cargo-wix --version 0.3.1 --locked
cargo wix init
cargo wix
- name: Upload msi file (Windows x86-64 MSVC)
if: matrix.triple.target == 'x86_64-pc-windows-msvc' && github.event.inputs.isMock != 'mock'
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
asset_path: bottom_x86_64_installer.msi
asset_name: bottom_x86_64_installer.msi
asset_content_type: application/octet-stream
- name: Build Debian release (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == ''
run: |
cargo install cargo-deb --version 1.29.0 --locked
cargo deb
cp ./target/debian/bottom_*.deb ./bottom_${{ env.RELEASE_VERSION }}_amd64.deb
- name: Upload Debian file (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == '' && github.event.inputs.isMock != 'mock'
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
asset_path: bottom_${{ env.RELEASE_VERSION }}_amd64.deb
asset_name: bottom_${{ env.RELEASE_VERSION }}_amd64.deb
asset_content_type: application/octet-stream
- name: Compress completion files (Linux x86-64 GNU)
if: matrix.triple.target == 'x86_64-unknown-linux-gnu' && matrix.triple.container == ''
shell: bash
@ -292,3 +260,133 @@ jobs:
asset_path: completion.tar.gz
asset_name: completion.tar.gz
asset_content_type: application/octet-stream
build-msi:
name: build-msi
needs: [create-github-release]
runs-on: "windows-2019"
env:
RUST_BACKTRACE: 1
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Get release download URL
uses: actions/download-artifact@v2
with:
name: artifacts
path: artifacts
- name: Set release upload URL and release version
shell: bash
run: |
release_upload_url="$(cat ./artifacts/release-upload-url)"
echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV
release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release upload url: ${{ env.RELEASE_UPLOAD_URL }}"
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Install Net-Framework-Core
shell: powershell
run: Install-WindowsFeature Net-Framework-Core
- name: Install wixtoolset
uses: crazy-max/ghaction-chocolatey@v1.4.0
with:
args: install -y wixtoolset
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
target: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@v1
with:
key: x86_64-pc-windows-msvc-msi
- name: Build msi file
shell: powershell
run: |
cargo install cargo-wix --version 0.3.1 --locked
cargo wix init
cargo wix
- name: Upload msi file
if: github.event.inputs.isMock != 'mock'
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
asset_path: bottom_x86_64_installer.msi
asset_name: bottom_x86_64_installer.msi
asset_content_type: application/octet-stream
build-deb:
name: build-deb
needs: [create-github-release]
runs-on: "ubuntu-18.04"
env:
RUST_BACKTRACE: 1
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Get release download URL
uses: actions/download-artifact@v2
with:
name: artifacts
path: artifacts
- name: Set release upload URL and release version
shell: bash
run: |
release_upload_url="$(cat ./artifacts/release-upload-url)"
echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV
release_version="$(cat ./artifacts/release-version)"
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Validate release environment variables
run: |
echo "Release upload url: ${{ env.RELEASE_UPLOAD_URL }}"
echo "Release version: ${{ env.RELEASE_VERSION }}"
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
target: x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@v1
with:
key: x86_64-unknown-linux-gnu-deb
- name: Build Debian release
run: |
cargo install cargo-deb --version 1.29.0 --locked
cargo deb
cp ./target/debian/bottom_*.deb ./bottom_${{ env.RELEASE_VERSION }}_amd64.deb
- name: Upload Debian file if not mock
if: github.event.inputs.isMock != 'mock'
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
asset_path: bottom_${{ env.RELEASE_VERSION }}_amd64.deb
asset_name: bottom_${{ env.RELEASE_VERSION }}_amd64.deb
asset_content_type: application/octet-stream

View File

@ -7,8 +7,8 @@ on:
env:
# Assign commit authorship to official Github Actions bot when pushing to the `gh-pages` branch:
GIT_USER: 'github-actions[bot]'
GIT_EMAIL: '41898282+github-actions[bot]@users.noreply.github.com'
GIT_USER: "github-actions[bot]"
GIT_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
jobs:
docs:
@ -43,11 +43,11 @@ jobs:
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: pip install mkdocs-material==7.2.6
- run: pip install mdx_truly_sane_lists
- run: pip install mdx_truly_sane_lists==1.2
- run: pip install mike
- run: pip install mike==1.1.0
- name: Configure git user and email
run: |
@ -85,13 +85,6 @@ jobs:
exit 1
fi
- name: Trigger homebrew
run: |
curl -X POST https://api.github.com/repos/ClementTsang/homebrew-bottom/dispatches \
-H 'Accept: application/vnd.github.everest-preview+json' \
-u ${{ secrets.BOTTOM_PACKAGE_DEPLOYMENT }} \
--data '{ "event_type": "update", "client_payload": { "version": "'"$RELEASE_VERSION"'" } }'
- name: Trigger choco
run: |
curl -X POST https://api.github.com/repos/ClementTsang/choco-bottom/dispatches \

View File

@ -5,15 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.4]/[0.7.0] - Unreleased
## [0.6.5]/[0.7.0] - Unreleased
## [0.6.4] - 2021-09-12
## Changes
- [#557](https://github.com/ClementTsang/bottom/pull/557): Add '/s' to network usage legend to indicate "per second".
## Bug Fixes
- [#575](https://github.com/ClementTsang/bottom/pull/575): Updates the procfs library to not crash on kernel version >255.
## Internal Changes
- [#551](https://github.com/ClementTsang/bottom/pull/551): Disable AUR package generation in release pipeline since it's now in community.
- [#570](https://github.com/ClementTsang/bottom/pull/570): Make battery features optional in compilation.
## [0.6.3] - 2021-07-18

464
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -31,12 +31,11 @@ opt-level = 3
codegen-units = 1
[features]
default = ["fern", "log"]
default = ["fern", "log", "battery"]
[dependencies]
anyhow = "1.0.40"
backtrace = "0.3.59"
battery = "0.7.8"
chrono = "0.4.19"
crossterm = "0.20.0"
ctrlc = { version = "3.1.9", features = ["termination"] }
@ -64,16 +63,17 @@ typed-builder = "0.9.0"
unicode-segmentation = "1.8.0"
unicode-width = "0.1"
# For debugging only... disable on release builds with --no-default-target for no? TODO: Redo this.
# For debugging only... disable on release builds with for now? TODO: Redo this.
fern = { version = "0.6.0", optional = true }
log = { version = "0.4.14", optional = true }
battery = { version = "0.7.8", optional = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2.86"
[target.'cfg(target_os = "linux")'.dependencies]
heim = { version = "0.1.0-rc.1", features = ["cpu", "disk", "net", "sensors"] }
procfs = "0.9.1"
procfs = "0.10.1"
smol = "1.2.5"
[target.'cfg(target_os = "macos")'.dependencies]
@ -96,16 +96,20 @@ assets = [
["target/release/btm", "usr/bin/", "755"],
["LICENSE", "usr/share/doc/btm/", "644"],
[
"completion/btm.bash",
"target/release/build/bottom-*/out/btm.bash",
"usr/share/bash-completion/completions/btm",
"644",
],
[
"completion/btm.fish",
"target/release/build/bottom-*/out/btm.fish",
"usr/share/fish/vendor_completions.d/btm.fish",
"644",
],
["completion/_btm", "usr/share/zsh/vendor-completions/", "644"],
[
"target/release/build/bottom-*/out/_btm",
"usr/share/zsh/vendor-completions/",
"644",
],
]
extended-description = """\
A cross-platform graphical process/system monitor with a customizable interface and a multitude of

View File

@ -97,8 +97,8 @@ sudo pacman -Syu bottom
A `.deb` file is provided on each [release](https://github.com/ClementTsang/bottom/releases/latest):
```bash
curl -LO https://github.com/ClementTsang/bottom/releases/download/0.6.3/bottom_0.6.3_amd64.deb
sudo dpkg -i bottom_0.6.3_amd64.deb
curl -LO https://github.com/ClementTsang/bottom/releases/download/0.6.4/bottom_0.6.4_amd64.deb
sudo dpkg -i bottom_0.6.4_amd64.deb
```
### Fedora/CentOS
@ -144,11 +144,7 @@ sudo eopkg it bottom
### Homebrew
```bash
brew tap clementtsang/bottom
brew install bottom
# If you need to be more specific, use:
brew install clementtsang/bottom/bottom
```
### MacPorts
@ -173,7 +169,7 @@ Since validation of the package takes time, it may take a while to become availa
choco install bottom
# The version number may be required for newer releases during the approval process:
choco install bottom --version=0.6.3
choco install bottom --version=0.6.4
```
### winget
@ -190,34 +186,35 @@ and installing via the `.msi` file.
You can uninstall via Control Panel, Options, or `winget --uninstall bottom`.
### Building
### Manually
There are a few ways to go about doing this manually. Note that in all cases, you would want to build using the most recent version of stable Rust:
There are a few ways to go about doing this manually. Note that you probably want
to do so using the most recent version of stable Rust, which is how the binaries are built:
```bash
# If required, update Rust on the stable channel
# If required, update Rust on the stable channel first
rustup update stable
# Download from releases and install
curl -LO https://github.com/ClementTsang/bottom/archive/0.6.3.tar.gz
tar -xzvf 0.6.3.tar.gz
# Option 1 - Download from releases and install
curl -LO https://github.com/ClementTsang/bottom/archive/0.6.4.tar.gz
tar -xzvf 0.6.4.tar.gz
cargo install --path .
# Clone from master and install manually
# Option 2 - Clone from master and install manually
git clone https://github.com/ClementTsang/bottom
cd bottom
cargo install --path .
# Clone and install the newest master version all via Cargo
# Option 3 - Clone and install directly from the repo all via Cargo
cargo install --git https://github.com/ClementTsang/bottom
```
### Binaries
You can also try to use the generated release binaries and manually install them:
You can also try to use the generated release binaries and manually install on your system:
- [Latest stable release](https://github.com/ClementTsang/bottom/releases/latest), generated off of the release branch
- [Latest nightly version](https://github.com/ClementTsang/bottom/releases/tag/nightly), which is generated daily off of the master branch at 00:00 UTC
- [Latest nightly release](https://github.com/ClementTsang/bottom/releases/tag/nightly), generated daily off of the master branch at 00:00 UTC
#### Auto-completion
@ -234,9 +231,9 @@ The release binaries are packaged with shell auto-completion files for bash, fis
You can run bottom using `btm`.
- For help on flags, use `btm -h` for a quick overview or `btm --help` for more details.
- For info on key and mouse bindings, refer to the [documentation](https://clementtsang.github.io/bottom/nightly/) or press `?` inside bottom.
- For info on key and mouse bindings, press `?` inside bottom or refer to the [documentation](https://clementtsang.github.io/bottom/nightly/).
You can generally find more information on usage in the [documentation](https://clementtsang.github.io/bottom/nightly/).
You can find more information on usage in the [documentation](https://clementtsang.github.io/bottom/nightly/).
## Configuration
@ -283,6 +280,10 @@ Thanks to all contributors ([emoji key](https://allcontributors.org/docs/en/emoj
<td align="center"><a href="https://github.com/yellowsquid"><img src="https://avatars.githubusercontent.com/u/46519298?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Greg Brown</b></sub></a><br /><a href="https://github.com/ClementTsang/bottom/commits?author=yellowsquid" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/TotalCaesar659"><img src="https://avatars.githubusercontent.com/u/14265316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>TotalCaesar659</b></sub></a><br /><a href="https://github.com/ClementTsang/bottom/commits?author=TotalCaesar659" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/grawlinson"><img src="https://avatars.githubusercontent.com/u/4408051?v=4?s=100" width="100px;" alt=""/><br /><sub><b>George Rawlinson</b></sub></a><br /><a href="https://github.com/ClementTsang/bottom/commits?author=grawlinson" title="Documentation">📖</a> <a href="#platform-grawlinson" title="Packaging/porting to new platform">📦</a></td>
<td align="center"><a href="https://www.frogorbits.com/"><img src="https://avatars.githubusercontent.com/u/101246?v=4?s=100" width="100px;" alt=""/><br /><sub><b>adiabatic</b></sub></a><br /><a href="https://github.com/ClementTsang/bottom/commits?author=adiabatic" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://electronsweatshop.com"><img src="https://avatars.githubusercontent.com/u/354506?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Randy Barlow</b></sub></a><br /><a href="https://github.com/ClementTsang/bottom/commits?author=bowlofeggs" title="Code">💻</a></td>
</tr>
</table>

View File

@ -31,7 +31,7 @@ The second column is split into two rows with a 1:1 ratio. The first is the temp
This is what the layout would look like when run:
![Sample layout](/assets/screenshots/config/layout/sample_layout.webp)
![Sample layout](../../../assets/screenshots/config/layout/sample_layout.webp)
Each `[[row]]` represents a _row_ in the layout. A row can have any number of `child` values. Each `[[row.child]]`
represents either a _column or a widget_. A column can have any number of `child` values as well. Each `[[row.child.child]]`

View File

@ -8,7 +8,7 @@ This site serves as extended documentation for bottom alongside the [`README.md`
!!! Warning
Some areas of this documentation are still in progress, and should get better in time. Feel free to suggest/contribute changes!
Some areas of this documentation are still in progress. Feel free to suggest/contribute changes!
## Installation

View File

@ -29,13 +29,15 @@ bottom from the repo/source, then please try that as well.
## Unofficial support
Systems and architectures that aren't officially supported may still work, but there are no guarantees on how much will work. Furthermore,
while it will depend on the problem at the end of the day, _issues on unsupported platforms are likely to go unfixed_.
Systems and architectures that aren't officially supported may still work, but there are no guarantees on how much will work. For example, it might only compile, or it might run with bugs/broken features.
Furthermore, while it will depend on the problem at the end of the day, _issues on unsupported platforms are likely to go unfixed_.
!!! note
Unofficially supported platforms known to compile/work:
bottom is tested to build on other ARM and PowerPC architectures through [CI](https://github.com/ClementTsang/bottom/blob/master/.github/workflows/ci.yml),
but since they are not officially tested to work on a sample platform, they are only unofficially supported.
- Linux on ARMv7 and ARMv6 (tested to compile in [CI](https://github.com/ClementTsang/bottom/blob/master/.github/workflows/ci.yml))
- macOS on AArch64 (tested to compile in [CI](https://github.com/ClementTsang/bottom/blob/master/.github/workflows/ci.yml))
- Linux on PowerPC 64 LE (tested to compile in [CI](https://github.com/ClementTsang/bottom/blob/master/.github/workflows/ci.yml))
- Linux on an RISC-V (tested to compile in [CI](https://github.com/ClementTsang/bottom/blob/master/.github/workflows/ci.yml), tested to run on an [Allwinner D1 Nezha](https://github.com/ClementTsang/bottom/issues/564))
### Known problems

View File

@ -1,5 +1,9 @@
# Battery Widget
!!! Warning
The battery features are unavailable if the binary is compiled with the `battery` feature disabled!
The battery widget provides information about batteries on the system.
<figure>

View File

@ -9,6 +9,7 @@ docs_dir: "content/"
# Project information
repo_name: ClementTsang/bottom
repo_url: https://github.com/ClementTsang/bottom
edit_uri: "edit/master/docs/content/"
# Theming
theme:

View File

@ -16,8 +16,11 @@ use once_cell::sync::Lazy;
use std::{collections::HashMap, time::Instant, vec::Vec};
#[cfg(feature = "battery")]
use crate::data_harvester::batteries;
use crate::{
data_harvester::{batteries, cpu, disks, memory, network, processes, temperature, Data},
data_harvester::{cpu, disks, memory, network, processes, temperature, Data},
utils::gen_util::{get_decimal_bytes, GIGA_LIMIT},
Pid,
};
@ -63,6 +66,7 @@ pub struct DataCollection {
pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>,
pub io_labels: Vec<(String, String)>,
pub temp_harvest: Vec<temperature::TempHarvest>,
#[cfg(feature = "battery")]
pub battery_harvest: Vec<batteries::BatteryHarvest>,
}
@ -84,6 +88,7 @@ impl Default for DataCollection {
io_labels_and_prev: Vec::default(),
io_labels: Vec::default(),
temp_harvest: Vec::default(),
#[cfg(feature = "battery")]
battery_harvest: Vec::default(),
}
}
@ -103,7 +108,10 @@ impl DataCollection {
self.io_harvest = Default::default();
self.io_labels_and_prev = Default::default();
self.temp_harvest = Default::default();
self.battery_harvest = Default::default();
#[cfg(feature = "battery")]
{
self.battery_harvest = Vec::default();
}
}
pub fn clean_data(&mut self, max_time_millis: u64) {
@ -168,9 +176,12 @@ impl DataCollection {
self.eat_proc(list_of_processes);
}
// Battery
if let Some(list_of_batteries) = harvested_data.list_of_batteries {
self.eat_battery(list_of_batteries);
#[cfg(feature = "battery")]
{
// Battery
if let Some(list_of_batteries) = harvested_data.list_of_batteries {
self.eat_battery(list_of_batteries);
}
}
// And we're done eating. Update time and push the new entry!
@ -332,6 +343,7 @@ impl DataCollection {
self.process_harvest = list_of_processes;
}
#[cfg(feature = "battery")]
fn eat_battery(&mut self, list_of_batteries: Vec<batteries::BatteryHarvest>) {
self.battery_harvest = list_of_batteries;
}

View File

@ -8,12 +8,14 @@ use fxhash::FxHashMap;
#[cfg(not(target_os = "linux"))]
use sysinfo::{System, SystemExt};
#[cfg(feature = "battery")]
use battery::{Battery, Manager};
use futures::join;
use super::{DataFilters, UsedWidgets};
#[cfg(feature = "battery")]
pub mod batteries;
pub mod cpu;
pub mod disks;
@ -34,6 +36,7 @@ pub struct Data {
pub list_of_processes: Option<Vec<processes::ProcessHarvest>>,
pub disks: Option<Vec<disks::DiskHarvest>>,
pub io: Option<disks::IoHarvest>,
#[cfg(feature = "battery")]
pub list_of_batteries: Option<Vec<batteries::BatteryHarvest>>,
}
@ -50,6 +53,7 @@ impl Default for Data {
disks: None,
io: None,
network: None,
#[cfg(feature = "battery")]
list_of_batteries: None,
}
}
@ -93,7 +97,9 @@ pub struct DataCollector {
total_tx: u64,
show_average_cpu: bool,
widgets_to_harvest: UsedWidgets,
#[cfg(feature = "battery")]
battery_manager: Option<Manager>,
#[cfg(feature = "battery")]
battery_list: Option<Vec<Battery>>,
filters: DataFilters,
@ -123,7 +129,9 @@ impl DataCollector {
total_tx: 0,
show_average_cpu: false,
widgets_to_harvest: UsedWidgets::default(),
#[cfg(feature = "battery")]
battery_manager: None,
#[cfg(feature = "battery")]
battery_list: None,
filters,
#[cfg(target_family = "unix")]
@ -153,13 +161,16 @@ impl DataCollector {
}
}
if self.widgets_to_harvest.use_battery {
if let Ok(battery_manager) = Manager::new() {
if let Ok(batteries) = battery_manager.batteries() {
let battery_list: Vec<Battery> = batteries.filter_map(Result::ok).collect();
if !battery_list.is_empty() {
self.battery_list = Some(battery_list);
self.battery_manager = Some(battery_manager);
#[cfg(feature = "battery")]
{
if self.widgets_to_harvest.use_battery {
if let Ok(battery_manager) = Manager::new() {
if let Ok(batteries) = battery_manager.batteries() {
let battery_list: Vec<Battery> = batteries.filter_map(Result::ok).collect();
if !battery_list.is_empty() {
self.battery_list = Some(battery_list);
self.battery_manager = Some(battery_manager);
}
}
}
}
@ -238,10 +249,13 @@ impl DataCollector {
}
// Batteries
if let Some(battery_manager) = &self.battery_manager {
if let Some(battery_list) = &mut self.battery_list {
self.data.list_of_batteries =
Some(batteries::refresh_batteries(battery_manager, battery_list));
#[cfg(feature = "battery")]
{
if let Some(battery_manager) = &self.battery_manager {
if let Some(battery_list) = &mut self.battery_list {
self.data.list_of_batteries =
Some(batteries::refresh_batteries(battery_manager, battery_list));
}
}
}

View File

@ -22,7 +22,7 @@ pub async fn get_network_data(
let to_keep = if let Some(filter) = filter {
let mut ret = filter.is_list_ignored;
for r in &filter.list {
if r.is_match(&name) {
if r.is_match(name) {
ret = !filter.is_list_ignored;
break;
}

View File

@ -184,13 +184,15 @@ impl std::str::FromStr for BottomWidgetType {
"temp" | "temperature" => Ok(BottomWidgetType::Temp),
"disk" => Ok(BottomWidgetType::Disk),
"empty" => Ok(BottomWidgetType::Empty),
"battery" | "batt" => Ok(BottomWidgetType::Battery),
"battery" | "batt" if cfg!(feature = "battery") => Ok(BottomWidgetType::Battery),
"bcpu" => Ok(BottomWidgetType::BasicCpu),
"bmem" => Ok(BottomWidgetType::BasicMem),
"bnet" => Ok(BottomWidgetType::BasicNet),
_ => Err(BottomError::ConfigError(format!(
"\"{}\" is an invalid widget name.
_ => {
if cfg!(feature = "battery") {
Err(BottomError::ConfigError(format!(
"\"{}\" is an invalid widget name.
Supported widget names:
+--------------------------+
| cpu |
@ -208,8 +210,30 @@ Supported widget names:
| batt, battery |
+--------------------------+
",
s
))),
s
)))
} else {
Err(BottomError::ConfigError(format!(
"\"{}\" is an invalid widget name.
Supported widget names:
+--------------------------+
| cpu |
+--------------------------+
| mem, memory |
+--------------------------+
| net, network |
+--------------------------+
| proc, process, processes |
+--------------------------+
| temp, temperature |
+--------------------------+
| disk |
+--------------------------+
",
s
)))
}
}
}
}
}

940
src/app/states.rs Normal file
View File

@ -0,0 +1,940 @@
use std::{collections::HashMap, time::Instant};
use unicode_segmentation::GraphemeCursor;
use tui::widgets::TableState;
use crate::{
app::{layout_manager::BottomWidgetType, query::*},
constants,
data_harvester::processes::{self, ProcessSorting},
};
use ProcessSorting::*;
#[derive(Debug)]
pub enum ScrollDirection {
// UP means scrolling up --- this usually DECREMENTS
Up,
// DOWN means scrolling down --- this usually INCREMENTS
Down,
}
impl Default for ScrollDirection {
fn default() -> Self {
ScrollDirection::Down
}
}
#[derive(Debug)]
pub enum CursorDirection {
Left,
Right,
}
/// AppScrollWidgetState deals with fields for a scrollable app's current state.
#[derive(Default)]
pub struct AppScrollWidgetState {
pub current_scroll_position: usize,
pub previous_scroll_position: usize,
pub scroll_direction: ScrollDirection,
pub table_state: TableState,
}
#[derive(PartialEq)]
pub enum KillSignal {
Cancel,
Kill(usize),
}
impl Default for KillSignal {
#[cfg(target_family = "unix")]
fn default() -> Self {
KillSignal::Kill(15)
}
#[cfg(target_os = "windows")]
fn default() -> Self {
KillSignal::Kill(1)
}
}
#[derive(Default)]
pub struct AppDeleteDialogState {
pub is_showing_dd: bool,
pub selected_signal: KillSignal,
/// tl x, tl y, br x, br y, index/signal
pub button_positions: Vec<(u16, u16, u16, u16, usize)>,
pub keyboard_signal_select: usize,
pub last_number_press: Option<Instant>,
pub scroll_pos: usize,
}
pub struct AppHelpDialogState {
pub is_showing_help: bool,
pub scroll_state: ParagraphScrollState,
pub index_shortcuts: Vec<u16>,
}
impl Default for AppHelpDialogState {
fn default() -> Self {
AppHelpDialogState {
is_showing_help: false,
scroll_state: ParagraphScrollState::default(),
index_shortcuts: vec![0; constants::HELP_TEXT.len()],
}
}
}
/// AppSearchState deals with generic searching (I might do this in the future).
pub struct AppSearchState {
pub is_enabled: bool,
pub current_search_query: String,
pub is_blank_search: bool,
pub is_invalid_search: bool,
pub grapheme_cursor: GraphemeCursor,
pub cursor_direction: CursorDirection,
pub cursor_bar: usize,
/// This represents the position in terms of CHARACTERS, not graphemes
pub char_cursor_position: usize,
/// The query
pub query: Option<Query>,
pub error_message: Option<String>,
}
impl Default for AppSearchState {
fn default() -> Self {
AppSearchState {
is_enabled: false,
current_search_query: String::default(),
is_invalid_search: false,
is_blank_search: true,
grapheme_cursor: GraphemeCursor::new(0, 0, true),
cursor_direction: CursorDirection::Right,
cursor_bar: 0,
char_cursor_position: 0,
query: None,
error_message: None,
}
}
}
impl AppSearchState {
/// Returns a reset but still enabled app search state
pub fn reset(&mut self) {
*self = AppSearchState {
is_enabled: self.is_enabled,
..AppSearchState::default()
}
}
pub fn is_invalid_or_blank_search(&self) -> bool {
self.is_blank_search || self.is_invalid_search
}
}
/// Meant for canvas operations involving table column widths.
#[derive(Default)]
pub struct CanvasTableWidthState {
pub desired_column_widths: Vec<u16>,
pub calculated_column_widths: Vec<u16>,
}
/// ProcessSearchState only deals with process' search's current settings and state.
pub struct ProcessSearchState {
pub search_state: AppSearchState,
pub is_ignoring_case: bool,
pub is_searching_whole_word: bool,
pub is_searching_with_regex: bool,
}
impl Default for ProcessSearchState {
fn default() -> Self {
ProcessSearchState {
search_state: AppSearchState::default(),
is_ignoring_case: true,
is_searching_whole_word: false,
is_searching_with_regex: false,
}
}
}
impl ProcessSearchState {
pub fn search_toggle_ignore_case(&mut self) {
self.is_ignoring_case = !self.is_ignoring_case;
}
pub fn search_toggle_whole_word(&mut self) {
self.is_searching_whole_word = !self.is_searching_whole_word;
}
pub fn search_toggle_regex(&mut self) {
self.is_searching_with_regex = !self.is_searching_with_regex;
}
}
pub struct ColumnInfo {
pub enabled: bool,
pub shortcut: Option<&'static str>,
// FIXME: Move column width logic here!
// pub hard_width: Option<u16>,
// pub max_soft_width: Option<f64>,
}
pub struct ProcColumn {
pub ordered_columns: Vec<ProcessSorting>,
/// The y location of headers. Since they're all aligned, it's just one value.
pub column_header_y_loc: Option<u16>,
/// The x start and end bounds for each header.
pub column_header_x_locs: Option<Vec<(u16, u16)>>,
pub column_mapping: HashMap<ProcessSorting, ColumnInfo>,
pub longest_header_len: u16,
pub column_state: TableState,
pub scroll_direction: ScrollDirection,
pub current_scroll_position: usize,
pub previous_scroll_position: usize,
pub backup_prev_scroll_position: usize,
}
impl Default for ProcColumn {
fn default() -> Self {
let ordered_columns = vec![
Count,
Pid,
ProcessName,
Command,
CpuPercent,
Mem,
MemPercent,
ReadPerSecond,
WritePerSecond,
TotalRead,
TotalWrite,
User,
State,
];
let mut column_mapping = HashMap::new();
let mut longest_header_len = 0;
for column in ordered_columns.clone() {
longest_header_len = std::cmp::max(longest_header_len, column.to_string().len());
match column {
CpuPercent => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: Some("c"),
// hard_width: None,
// max_soft_width: None,
},
);
}
MemPercent => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: Some("m"),
// hard_width: None,
// max_soft_width: None,
},
);
}
Mem => {
column_mapping.insert(
column,
ColumnInfo {
enabled: false,
shortcut: Some("m"),
// hard_width: None,
// max_soft_width: None,
},
);
}
ProcessName => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: Some("n"),
// hard_width: None,
// max_soft_width: None,
},
);
}
Command => {
column_mapping.insert(
column,
ColumnInfo {
enabled: false,
shortcut: Some("n"),
// hard_width: None,
// max_soft_width: None,
},
);
}
Pid => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: Some("p"),
// hard_width: None,
// max_soft_width: None,
},
);
}
Count => {
column_mapping.insert(
column,
ColumnInfo {
enabled: false,
shortcut: None,
// hard_width: None,
// max_soft_width: None,
},
);
}
User => {
column_mapping.insert(
column,
ColumnInfo {
enabled: cfg!(target_family = "unix"),
shortcut: None,
},
);
}
_ => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: None,
// hard_width: None,
// max_soft_width: None,
},
);
}
}
}
let longest_header_len = longest_header_len as u16;
ProcColumn {
ordered_columns,
column_mapping,
longest_header_len,
column_state: TableState::default(),
scroll_direction: ScrollDirection::default(),
current_scroll_position: 0,
previous_scroll_position: 0,
backup_prev_scroll_position: 0,
column_header_y_loc: None,
column_header_x_locs: None,
}
}
}
impl ProcColumn {
/// Returns its new status.
pub fn toggle(&mut self, column: &ProcessSorting) -> Option<bool> {
if let Some(mapping) = self.column_mapping.get_mut(column) {
mapping.enabled = !(mapping.enabled);
Some(mapping.enabled)
} else {
None
}
}
pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option<bool> {
if let Some(mapping) = self.column_mapping.get_mut(column) {
mapping.enabled = setting;
Some(mapping.enabled)
} else {
None
}
}
pub fn try_enable(&mut self, column: &ProcessSorting) -> Option<bool> {
if let Some(mapping) = self.column_mapping.get_mut(column) {
mapping.enabled = true;
Some(mapping.enabled)
} else {
None
}
}
pub fn try_disable(&mut self, column: &ProcessSorting) -> Option<bool> {
if let Some(mapping) = self.column_mapping.get_mut(column) {
mapping.enabled = false;
Some(mapping.enabled)
} else {
None
}
}
pub fn is_enabled(&self, column: &ProcessSorting) -> bool {
if let Some(mapping) = self.column_mapping.get(column) {
mapping.enabled
} else {
false
}
}
pub fn get_enabled_columns_len(&self) -> usize {
self.ordered_columns
.iter()
.filter_map(|column_type| {
if let Some(col_map) = self.column_mapping.get(column_type) {
if col_map.enabled {
Some(1)
} else {
None
}
} else {
None
}
})
.sum()
}
/// NOTE: ALWAYS call this when opening the sorted window.
pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) {
// TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT!
let mut true_index = 0;
for column in &self.ordered_columns {
if *column == *proc_sorting_type {
break;
}
if self.column_mapping.get(column).unwrap().enabled {
true_index += 1;
}
}
self.current_scroll_position = true_index;
self.backup_prev_scroll_position = self.previous_scroll_position;
}
/// This function sets the scroll position based on the index.
pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) {
self.current_scroll_position = visual_index;
self.backup_prev_scroll_position = self.previous_scroll_position;
}
pub fn get_column_headers(
&self, proc_sorting_type: &ProcessSorting, sort_reverse: bool,
) -> Vec<String> {
const DOWN_ARROW: char = '▼';
const UP_ARROW: char = '▲';
// TODO: Gonna have to figure out how to do left/right GUI notation if we add it.
self.ordered_columns
.iter()
.filter_map(|column_type| {
let mapping = self.column_mapping.get(column_type).unwrap();
let mut command_str = String::default();
if let Some(command) = mapping.shortcut {
command_str = format!("({})", command);
}
if mapping.enabled {
Some(format!(
"{}{}{}",
column_type.to_string(),
command_str.as_str(),
if proc_sorting_type == column_type {
if sort_reverse {
DOWN_ARROW
} else {
UP_ARROW
}
} else {
' '
}
))
} else {
None
}
})
.collect()
}
}
pub struct ProcWidgetState {
pub process_search_state: ProcessSearchState,
pub is_grouped: bool,
pub scroll_state: AppScrollWidgetState,
pub process_sorting_type: processes::ProcessSorting,
pub is_process_sort_descending: bool,
pub is_using_command: bool,
pub current_column_index: usize,
pub is_sort_open: bool,
pub columns: ProcColumn,
pub is_tree_mode: bool,
pub table_width_state: CanvasTableWidthState,
pub requires_redraw: bool,
}
impl ProcWidgetState {
pub fn init(
is_case_sensitive: bool, is_match_whole_word: bool, is_use_regex: bool, is_grouped: bool,
show_memory_as_values: bool, is_tree_mode: bool, is_using_command: bool,
) -> Self {
let mut process_search_state = ProcessSearchState::default();
if is_case_sensitive {
// By default it's off
process_search_state.search_toggle_ignore_case();
}
if is_match_whole_word {
process_search_state.search_toggle_whole_word();
}
if is_use_regex {
process_search_state.search_toggle_regex();
}
let (process_sorting_type, is_process_sort_descending) = if is_tree_mode {
(processes::ProcessSorting::Pid, false)
} else {
(processes::ProcessSorting::CpuPercent, true)
};
// TODO: If we add customizable columns, this should pull from config
let mut columns = ProcColumn::default();
columns.set_to_sorted_index_from_type(&process_sorting_type);
if is_grouped {
// Normally defaults to showing by PID, toggle count on instead.
columns.toggle(&ProcessSorting::Count);
columns.toggle(&ProcessSorting::Pid);
}
if show_memory_as_values {
// Normally defaults to showing by percent, toggle value on instead.
columns.toggle(&ProcessSorting::Mem);
columns.toggle(&ProcessSorting::MemPercent);
}
ProcWidgetState {
process_search_state,
is_grouped,
scroll_state: AppScrollWidgetState::default(),
process_sorting_type,
is_process_sort_descending,
is_using_command,
current_column_index: 0,
is_sort_open: false,
columns,
is_tree_mode,
table_width_state: CanvasTableWidthState::default(),
requires_redraw: false,
}
}
/// Updates sorting when using the column list.
/// ...this really should be part of the ProcColumn struct (along with the sorting fields),
/// but I'm too lazy.
///
/// Sorry, future me, you're gonna have to refactor this later. Too busy getting
/// the feature to work in the first place! :)
pub fn update_sorting_with_columns(&mut self) {
let mut true_index = 0;
let mut enabled_index = 0;
let target_itx = self.columns.current_scroll_position;
for column in &self.columns.ordered_columns {
let enabled = self.columns.column_mapping.get(column).unwrap().enabled;
if enabled_index == target_itx && enabled {
break;
}
if enabled {
enabled_index += 1;
}
true_index += 1;
}
if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) {
if *new_sort_type == self.process_sorting_type {
// Just reverse the search if we're reselecting!
self.is_process_sort_descending = !(self.is_process_sort_descending);
} else {
self.process_sorting_type = new_sort_type.clone();
match self.process_sorting_type {
ProcessSorting::State
| ProcessSorting::Pid
| ProcessSorting::ProcessName
| ProcessSorting::Command => {
// Also invert anything that uses alphabetical sorting by default.
self.is_process_sort_descending = false;
}
_ => {
self.is_process_sort_descending = true;
}
}
}
}
}
pub fn toggle_command_and_name(&mut self, is_using_command: bool) {
if let Some(pn) = self
.columns
.column_mapping
.get_mut(&ProcessSorting::ProcessName)
{
pn.enabled = !is_using_command;
}
if let Some(c) = self
.columns
.column_mapping
.get_mut(&ProcessSorting::Command)
{
c.enabled = is_using_command;
}
}
pub fn get_search_cursor_position(&self) -> usize {
self.process_search_state
.search_state
.grapheme_cursor
.cur_cursor()
}
pub fn get_char_cursor_position(&self) -> usize {
self.process_search_state.search_state.char_cursor_position
}
pub fn is_search_enabled(&self) -> bool {
self.process_search_state.search_state.is_enabled
}
pub fn get_current_search_query(&self) -> &String {
&self.process_search_state.search_state.current_search_query
}
pub fn update_query(&mut self) {
if self
.process_search_state
.search_state
.current_search_query
.is_empty()
{
self.process_search_state.search_state.is_blank_search = true;
self.process_search_state.search_state.is_invalid_search = false;
self.process_search_state.search_state.error_message = None;
} else {
let parsed_query = self.parse_query();
// debug!("Parsed query: {:#?}", parsed_query);
if let Ok(parsed_query) = parsed_query {
self.process_search_state.search_state.query = Some(parsed_query);
self.process_search_state.search_state.is_blank_search = false;
self.process_search_state.search_state.is_invalid_search = false;
self.process_search_state.search_state.error_message = None;
} else if let Err(err) = parsed_query {
self.process_search_state.search_state.is_blank_search = false;
self.process_search_state.search_state.is_invalid_search = true;
self.process_search_state.search_state.error_message = Some(err.to_string());
}
}
self.scroll_state.previous_scroll_position = 0;
self.scroll_state.current_scroll_position = 0;
}
pub fn clear_search(&mut self) {
self.process_search_state.search_state.reset();
}
pub fn search_walk_forward(&mut self, start_position: usize) {
self.process_search_state
.search_state
.grapheme_cursor
.next_boundary(
&self.process_search_state.search_state.current_search_query[start_position..],
start_position,
)
.unwrap();
}
pub fn search_walk_back(&mut self, start_position: usize) {
self.process_search_state
.search_state
.grapheme_cursor
.prev_boundary(
&self.process_search_state.search_state.current_search_query[..start_position],
0,
)
.unwrap();
}
}
pub struct ProcState {
pub widget_states: HashMap<u64, ProcWidgetState>,
pub force_update: Option<u64>,
pub force_update_all: bool,
}
impl ProcState {
pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self {
ProcState {
widget_states,
force_update: None,
force_update_all: false,
}
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> {
self.widget_states.get(&widget_id)
}
}
pub struct NetWidgetState {
pub current_display_time: u64,
pub autohide_timer: Option<Instant>,
// pub draw_max_range_cache: f64,
// pub draw_labels_cache: Vec<String>,
// pub draw_time_start_cache: f64,
// TODO: Re-enable these when we move net details state-side!
// pub unit_type: DataUnitTypes,
// pub scale_type: AxisScaling,
}
impl NetWidgetState {
pub fn init(
current_display_time: u64,
autohide_timer: Option<Instant>,
// unit_type: DataUnitTypes,
// scale_type: AxisScaling,
) -> Self {
NetWidgetState {
current_display_time,
autohide_timer,
// draw_max_range_cache: 0.0,
// draw_labels_cache: vec![],
// draw_time_start_cache: 0.0,
// unit_type,
// scale_type,
}
}
}
pub struct NetState {
pub force_update: Option<u64>,
pub widget_states: HashMap<u64, NetWidgetState>,
}
impl NetState {
pub fn init(widget_states: HashMap<u64, NetWidgetState>) -> Self {
NetState {
force_update: None,
widget_states,
}
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut NetWidgetState> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&NetWidgetState> {
self.widget_states.get(&widget_id)
}
}
pub struct CpuWidgetState {
pub current_display_time: u64,
pub is_legend_hidden: bool,
pub autohide_timer: Option<Instant>,
pub scroll_state: AppScrollWidgetState,
pub is_multi_graph_mode: bool,
pub table_width_state: CanvasTableWidthState,
}
impl CpuWidgetState {
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
CpuWidgetState {
current_display_time,
is_legend_hidden: false,
autohide_timer,
scroll_state: AppScrollWidgetState::default(),
is_multi_graph_mode: false,
table_width_state: CanvasTableWidthState::default(),
}
}
}
pub struct CpuState {
pub force_update: Option<u64>,
pub widget_states: HashMap<u64, CpuWidgetState>,
}
impl CpuState {
pub fn init(widget_states: HashMap<u64, CpuWidgetState>) -> Self {
CpuState {
force_update: None,
widget_states,
}
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut CpuWidgetState> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&CpuWidgetState> {
self.widget_states.get(&widget_id)
}
}
pub struct MemWidgetState {
pub current_display_time: u64,
pub autohide_timer: Option<Instant>,
}
impl MemWidgetState {
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
MemWidgetState {
current_display_time,
autohide_timer,
}
}
}
pub struct MemState {
pub force_update: Option<u64>,
pub widget_states: HashMap<u64, MemWidgetState>,
}
impl MemState {
pub fn init(widget_states: HashMap<u64, MemWidgetState>) -> Self {
MemState {
force_update: None,
widget_states,
}
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut MemWidgetState> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&MemWidgetState> {
self.widget_states.get(&widget_id)
}
}
pub struct TempWidgetState {
pub scroll_state: AppScrollWidgetState,
pub table_width_state: CanvasTableWidthState,
}
impl TempWidgetState {
pub fn init() -> Self {
TempWidgetState {
scroll_state: AppScrollWidgetState::default(),
table_width_state: CanvasTableWidthState::default(),
}
}
}
pub struct TempState {
pub widget_states: HashMap<u64, TempWidgetState>,
}
impl TempState {
pub fn init(widget_states: HashMap<u64, TempWidgetState>) -> Self {
TempState { widget_states }
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut TempWidgetState> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&TempWidgetState> {
self.widget_states.get(&widget_id)
}
}
pub struct DiskWidgetState {
pub scroll_state: AppScrollWidgetState,
pub table_width_state: CanvasTableWidthState,
}
impl DiskWidgetState {
pub fn init() -> Self {
DiskWidgetState {
scroll_state: AppScrollWidgetState::default(),
table_width_state: CanvasTableWidthState::default(),
}
}
}
pub struct DiskState {
pub widget_states: HashMap<u64, DiskWidgetState>,
}
impl DiskState {
pub fn init(widget_states: HashMap<u64, DiskWidgetState>) -> Self {
DiskState { widget_states }
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskWidgetState> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&DiskWidgetState> {
self.widget_states.get(&widget_id)
}
}
pub struct BasicTableWidgetState {
// Since this is intended (currently) to only be used for ONE widget, that's
// how it's going to be written. If we want to allow for multiple of these,
// then we can expand outwards with a normal BasicTableState and a hashmap
pub currently_displayed_widget_type: BottomWidgetType,
pub currently_displayed_widget_id: u64,
pub widget_id: i64,
pub left_tlc: Option<(u16, u16)>,
pub left_brc: Option<(u16, u16)>,
pub right_tlc: Option<(u16, u16)>,
pub right_brc: Option<(u16, u16)>,
}
#[derive(Default)]
pub struct BatteryWidgetState {
pub currently_selected_battery_index: usize,
pub tab_click_locs: Option<Vec<((u16, u16), (u16, u16))>>,
}
pub struct BatteryState {
pub widget_states: HashMap<u64, BatteryWidgetState>,
}
impl BatteryState {
pub fn init(widget_states: HashMap<u64, BatteryWidgetState>) -> Self {
BatteryState { widget_states }
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut BatteryWidgetState> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&BatteryWidgetState> {
self.widget_states.get(&widget_id)
}
}
#[derive(Default)]
pub struct ParagraphScrollState {
pub current_scroll_index: u16,
pub max_scroll_index: u16,
}
#[derive(Default)]
pub struct ConfigState {
pub current_category_index: usize,
pub category_list: Vec<ConfigCategory>,
}
#[derive(Default)]
pub struct ConfigCategory {
pub category_name: &'static str,
pub options_list: Vec<ConfigOption>,
}
pub struct ConfigOption {
pub set_function: Box<dyn Fn() -> anyhow::Result<()>>,
}

View File

@ -0,0 +1,249 @@
use crate::{
app::App,
canvas::{drawing_utils::interpolate_points, Painter},
constants::*,
};
use tui::{
backend::Backend,
layout::{Constraint, Rect},
symbols::Marker,
terminal::Frame,
text::Span,
text::Spans,
widgets::{Axis, Block, Borders, Chart, Dataset},
};
use unicode_segmentation::UnicodeSegmentation;
pub trait MemGraphWidget {
fn draw_memory_graph<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
);
}
impl MemGraphWidget for Painter {
fn draw_memory_graph<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
if let Some(mem_widget_state) = app_state.mem_state.widget_states.get_mut(&widget_id) {
let mem_data: &mut [(f64, f64)] = &mut app_state.canvas_data.mem_data;
let swap_data: &mut [(f64, f64)] = &mut app_state.canvas_data.swap_data;
let time_start = -(mem_widget_state.current_display_time as f64);
let display_time_labels = vec![
Span::styled(
format!("{}s", mem_widget_state.current_display_time / 1000),
self.colours.graph_style,
),
Span::styled("0s".to_string(), self.colours.graph_style),
];
let y_axis_label = vec![
Span::styled(" 0%", self.colours.graph_style),
Span::styled("100%", self.colours.graph_style),
];
let x_axis = if app_state.app_config_fields.hide_time
|| (app_state.app_config_fields.autohide_time
&& mem_widget_state.autohide_timer.is_none())
{
Axis::default().bounds([time_start, 0.0])
} else if let Some(time) = mem_widget_state.autohide_timer {
if std::time::Instant::now().duration_since(time).as_millis()
< AUTOHIDE_TIMEOUT_MILLISECONDS as u128
{
Axis::default()
.bounds([time_start, 0.0])
.style(self.colours.graph_style)
.labels(display_time_labels)
} else {
mem_widget_state.autohide_timer = None;
Axis::default().bounds([time_start, 0.0])
}
} else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT {
Axis::default().bounds([time_start, 0.0])
} else {
Axis::default()
.bounds([time_start, 0.0])
.style(self.colours.graph_style)
.labels(display_time_labels)
};
let y_axis = Axis::default()
.style(self.colours.graph_style)
.bounds([0.0, 100.5])
.labels(y_axis_label);
// Interpolate values to avoid ugly gaps
let interpolated_mem_point = if let Some(end_pos) = mem_data
.iter()
.position(|(time, _data)| *time >= time_start)
{
if end_pos > 1 {
let start_pos = end_pos - 1;
let outside_point = mem_data.get(start_pos);
let inside_point = mem_data.get(end_pos);
if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point)
{
let old = *outside_point;
let new_point = (
time_start,
interpolate_points(outside_point, inside_point, time_start),
);
if let Some(to_replace) = mem_data.get_mut(start_pos) {
*to_replace = new_point;
Some((start_pos, old))
} else {
None // Failed to get mutable reference.
}
} else {
None // Point somehow doesn't exist in our data
}
} else {
None // Point is already "leftmost", no need to interpolate.
}
} else {
None // There is no point.
};
let interpolated_swap_point = if let Some(end_pos) = swap_data
.iter()
.position(|(time, _data)| *time >= time_start)
{
if end_pos > 1 {
let start_pos = end_pos - 1;
let outside_point = swap_data.get(start_pos);
let inside_point = swap_data.get(end_pos);
if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point)
{
let old = *outside_point;
let new_point = (
time_start,
interpolate_points(outside_point, inside_point, time_start),
);
if let Some(to_replace) = swap_data.get_mut(start_pos) {
*to_replace = new_point;
Some((start_pos, old))
} else {
None // Failed to get mutable reference.
}
} else {
None // Point somehow doesn't exist in our data
}
} else {
None // Point is already "leftmost", no need to interpolate.
}
} else {
None // There is no point.
};
let mut mem_canvas_vec: Vec<Dataset<'_>> = vec![];
if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels {
let mem_label = format!("RAM:{}{}", label_percent, label_frac);
mem_canvas_vec.push(
Dataset::default()
.name(mem_label)
.marker(if app_state.app_config_fields.use_dot {
Marker::Dot
} else {
Marker::Braille
})
.style(self.colours.ram_style)
.data(mem_data)
.graph_type(tui::widgets::GraphType::Line),
);
}
if let Some((label_percent, label_frac)) = &app_state.canvas_data.swap_labels {
let swap_label = format!("SWP:{}{}", label_percent, label_frac);
mem_canvas_vec.push(
Dataset::default()
.name(swap_label)
.marker(if app_state.app_config_fields.use_dot {
Marker::Dot
} else {
Marker::Braille
})
.style(self.colours.swap_style)
.data(swap_data)
.graph_type(tui::widgets::GraphType::Line),
);
}
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let border_style = if is_on_widget {
self.colours.highlighted_border_style
} else {
self.colours.border_style
};
let title = if app_state.is_expanded {
const TITLE_BASE: &str = " Memory ── Esc to go back ";
Spans::from(vec![
Span::styled(" Memory ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to go back ",
"".repeat(usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2
))
),
border_style,
),
])
} else {
Spans::from(Span::styled(
" Memory ".to_string(),
self.colours.widget_title_style,
))
};
f.render_widget(
Chart::new(mem_canvas_vec)
.block(
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(if app_state.current_widget.widget_id == widget_id {
self.colours.highlighted_border_style
} else {
self.colours.border_style
}),
)
.x_axis(x_axis)
.y_axis(y_axis)
.hidden_legend_constraints((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))),
draw_loc,
);
// Now if you're done, reset any interpolated points!
if let Some((index, old_value)) = interpolated_mem_point {
if let Some(to_replace) = mem_data.get_mut(index) {
*to_replace = old_value;
}
}
if let Some((index, old_value)) = interpolated_swap_point {
if let Some(to_replace) = swap_data.get_mut(index) {
*to_replace = old_value;
}
}
}
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
}
}

View File

@ -0,0 +1,770 @@
use once_cell::sync::Lazy;
use std::cmp::max;
use unicode_segmentation::UnicodeSegmentation;
use crate::{
app::{App, AxisScaling},
canvas::{
drawing_utils::{get_column_widths, interpolate_points},
Painter,
},
constants::*,
units::data_units::DataUnit,
utils::gen_util::*,
};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
symbols::Marker,
terminal::Frame,
text::Span,
text::{Spans, Text},
widgets::{Axis, Block, Borders, Chart, Dataset, Row, Table},
};
const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"];
static NETWORK_HEADERS_LENS: Lazy<Vec<u16>> = Lazy::new(|| {
NETWORK_HEADERS
.iter()
.map(|entry| entry.len() as u16)
.collect::<Vec<_>>()
});
pub trait NetworkGraphWidget {
fn draw_network<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
);
fn draw_network_graph<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
hide_legend: bool,
);
fn draw_network_labels<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
);
}
impl NetworkGraphWidget for Painter {
fn draw_network<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
if app_state.app_config_fields.use_old_network_legend {
let network_chunk = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([
Constraint::Length(max(draw_loc.height as i64 - 5, 0) as u16),
Constraint::Length(5),
])
.split(draw_loc);
self.draw_network_graph(f, app_state, network_chunk[0], widget_id, true);
self.draw_network_labels(f, app_state, network_chunk[1], widget_id);
} else {
self.draw_network_graph(f, app_state, draw_loc, widget_id, false);
}
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
// Note that in both cases, we always go to the same widget id so it's fine to do it like
// this lol.
if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) {
network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
network_widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
}
fn draw_network_graph<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
hide_legend: bool,
) {
/// Point is of time, data
type Point = (f64, f64);
/// Returns the max data point and time given a time.
fn get_max_entry(
rx: &[Point], tx: &[Point], time_start: f64, network_scale_type: &AxisScaling,
network_use_binary_prefix: bool,
) -> (f64, f64) {
/// Determines a "fake" max value in circumstances where we couldn't find one from the data.
fn calculate_missing_max(
network_scale_type: &AxisScaling, network_use_binary_prefix: bool,
) -> f64 {
match network_scale_type {
AxisScaling::Log => {
if network_use_binary_prefix {
LOG_KIBI_LIMIT
} else {
LOG_KILO_LIMIT
}
}
AxisScaling::Linear => {
if network_use_binary_prefix {
KIBI_LIMIT_F64
} else {
KILO_LIMIT_F64
}
}
}
}
// First, let's shorten our ranges to actually look. We can abuse the fact that our rx and tx arrays
// are sorted, so we can short-circuit our search to filter out only the relevant data points...
let filtered_rx = if let (Some(rx_start), Some(rx_end)) = (
rx.iter().position(|(time, _data)| *time >= time_start),
rx.iter().rposition(|(time, _data)| *time <= 0.0),
) {
Some(&rx[rx_start..=rx_end])
} else {
None
};
let filtered_tx = if let (Some(tx_start), Some(tx_end)) = (
tx.iter().position(|(time, _data)| *time >= time_start),
tx.iter().rposition(|(time, _data)| *time <= 0.0),
) {
Some(&tx[tx_start..=tx_end])
} else {
None
};
// Then, find the maximal rx/tx so we know how to scale, and return it.
match (filtered_rx, filtered_tx) {
(None, None) => (
time_start,
calculate_missing_max(network_scale_type, network_use_binary_prefix),
),
(None, Some(filtered_tx)) => {
match filtered_tx
.iter()
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
{
Some((best_time, max_val)) => {
if *max_val == 0.0 {
(
time_start,
calculate_missing_max(
network_scale_type,
network_use_binary_prefix,
),
)
} else {
(*best_time, *max_val)
}
}
None => (
time_start,
calculate_missing_max(network_scale_type, network_use_binary_prefix),
),
}
}
(Some(filtered_rx), None) => {
match filtered_rx
.iter()
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
{
Some((best_time, max_val)) => {
if *max_val == 0.0 {
(
time_start,
calculate_missing_max(
network_scale_type,
network_use_binary_prefix,
),
)
} else {
(*best_time, *max_val)
}
}
None => (
time_start,
calculate_missing_max(network_scale_type, network_use_binary_prefix),
),
}
}
(Some(filtered_rx), Some(filtered_tx)) => {
match filtered_rx
.iter()
.chain(filtered_tx)
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
{
Some((best_time, max_val)) => {
if *max_val == 0.0 {
(
*best_time,
calculate_missing_max(
network_scale_type,
network_use_binary_prefix,
),
)
} else {
(*best_time, *max_val)
}
}
None => (
time_start,
calculate_missing_max(network_scale_type, network_use_binary_prefix),
),
}
}
}
}
/// Returns the required max data point and labels.
fn adjust_network_data_point(
max_entry: f64, network_scale_type: &AxisScaling, network_unit_type: &DataUnit,
network_use_binary_prefix: bool,
) -> (f64, Vec<String>) {
// So, we're going with an approach like this for linear data:
// - Main goal is to maximize the amount of information displayed given a specific height.
// We don't want to drown out some data if the ranges are too far though! Nor do we want to filter
// out too much data...
// - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max load.
//
// The idea is we take the top value, build our scale such that each "point" is a scaled version of that.
// So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and
// probably something like 438.75?
//
// So, how do we do this in tui-rs? Well, if we are using intervals that tie in perfectly to the max
// value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will
// properly space them all out... we just work with that and space it out properly.
//
// Dynamic chart idea based off of FreeNAS's chart design.
//
// ===
//
// For log data, we just use the old method of log intervals (kilo/mega/giga/etc.). Keep it nice and simple.
// Now just check the largest unit we correspond to... then proceed to build some entries from there!
let unit_char = match network_unit_type {
DataUnit::Byte => "B",
DataUnit::Bit => "b",
};
match network_scale_type {
AxisScaling::Linear => {
let (k_limit, m_limit, g_limit, t_limit) = if network_use_binary_prefix {
(
KIBI_LIMIT_F64,
MEBI_LIMIT_F64,
GIBI_LIMIT_F64,
TEBI_LIMIT_F64,
)
} else {
(
KILO_LIMIT_F64,
MEGA_LIMIT_F64,
GIGA_LIMIT_F64,
TERA_LIMIT_F64,
)
};
let bumped_max_entry = max_entry * 1.5; // We use the bumped up version to calculate our unit type.
let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) =
if bumped_max_entry < k_limit {
(max_entry, "", unit_char)
} else if bumped_max_entry < m_limit {
(
max_entry / k_limit,
if network_use_binary_prefix { "Ki" } else { "K" },
unit_char,
)
} else if bumped_max_entry < g_limit {
(
max_entry / m_limit,
if network_use_binary_prefix { "Mi" } else { "M" },
unit_char,
)
} else if bumped_max_entry < t_limit {
(
max_entry / g_limit,
if network_use_binary_prefix { "Gi" } else { "G" },
unit_char,
)
} else {
(
max_entry / t_limit,
if network_use_binary_prefix { "Ti" } else { "T" },
unit_char,
)
};
// Finally, build an acceptable range starting from there, using the given height!
// Note we try to put more of a weight on the bottom section vs. the top, since the top has less data.
let base_unit = max_value_scaled;
let labels: Vec<String> = vec![
format!("0{}{}", unit_prefix, unit_type),
format!("{:.1}", base_unit * 0.5),
format!("{:.1}", base_unit),
format!("{:.1}", base_unit * 1.5),
]
.into_iter()
.map(|s| format!("{:>5}", s)) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second)
.collect();
(bumped_max_entry, labels)
}
AxisScaling::Log => {
let (m_limit, g_limit, t_limit) = if network_use_binary_prefix {
(LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT)
} else {
(LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT)
};
fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String {
format!(
"{}0{}",
if network_use_binary_prefix { " " } else { " " },
unit_char
)
}
fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String {
format!(
"1{}{}",
if network_use_binary_prefix { "Ki" } else { "K" },
unit_char
)
}
fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String {
format!(
"1{}{}",
if network_use_binary_prefix { "Mi" } else { "M" },
unit_char
)
}
fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String {
format!(
"1{}{}",
if network_use_binary_prefix { "Gi" } else { "G" },
unit_char
)
}
fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String {
format!(
"1{}{}",
if network_use_binary_prefix { "Ti" } else { "T" },
unit_char
)
}
fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String {
format!(
"1{}{}",
if network_use_binary_prefix { "Pi" } else { "P" },
unit_char
)
}
if max_entry < m_limit {
(
m_limit,
vec![
get_zero(network_use_binary_prefix, unit_char),
get_k(network_use_binary_prefix, unit_char),
get_m(network_use_binary_prefix, unit_char),
],
)
} else if max_entry < g_limit {
(
g_limit,
vec![
get_zero(network_use_binary_prefix, unit_char),
get_k(network_use_binary_prefix, unit_char),
get_m(network_use_binary_prefix, unit_char),
get_g(network_use_binary_prefix, unit_char),
],
)
} else if max_entry < t_limit {
(
t_limit,
vec![
get_zero(network_use_binary_prefix, unit_char),
get_k(network_use_binary_prefix, unit_char),
get_m(network_use_binary_prefix, unit_char),
get_g(network_use_binary_prefix, unit_char),
get_t(network_use_binary_prefix, unit_char),
],
)
} else {
// I really doubt anyone's transferring beyond petabyte speeds...
(
if network_use_binary_prefix {
LOG_PEBI_LIMIT
} else {
LOG_PETA_LIMIT
},
vec![
get_zero(network_use_binary_prefix, unit_char),
get_k(network_use_binary_prefix, unit_char),
get_m(network_use_binary_prefix, unit_char),
get_g(network_use_binary_prefix, unit_char),
get_t(network_use_binary_prefix, unit_char),
get_p(network_use_binary_prefix, unit_char),
],
)
}
}
}
}
if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) {
let network_data_rx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_rx;
let network_data_tx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_tx;
let time_start = -(network_widget_state.current_display_time as f64);
let display_time_labels = vec![
Span::styled(
format!("{}s", network_widget_state.current_display_time / 1000),
self.colours.graph_style,
),
Span::styled("0s".to_string(), self.colours.graph_style),
];
let x_axis = if app_state.app_config_fields.hide_time
|| (app_state.app_config_fields.autohide_time
&& network_widget_state.autohide_timer.is_none())
{
Axis::default().bounds([time_start, 0.0])
} else if let Some(time) = network_widget_state.autohide_timer {
if std::time::Instant::now().duration_since(time).as_millis()
< AUTOHIDE_TIMEOUT_MILLISECONDS as u128
{
Axis::default()
.bounds([time_start, 0.0])
.style(self.colours.graph_style)
.labels(display_time_labels)
} else {
network_widget_state.autohide_timer = None;
Axis::default().bounds([time_start, 0.0])
}
} else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT {
Axis::default().bounds([time_start, 0.0])
} else {
Axis::default()
.bounds([time_start, 0.0])
.style(self.colours.graph_style)
.labels(display_time_labels)
};
// Interpolate a point for rx and tx between the last value outside of the left bounds and the first value
// inside it.
// Because we assume it is all in order for... basically all our code, we can't just append it,
// and insertion in the middle seems. So instead, we swap *out* the value that is outside with our
// interpolated point, draw and do whatever calculations, then swap back in the old value!
//
// Note there is some re-used work here! For potential optimizations, we could re-use some work here in/from
// get_max_entry...
let interpolated_rx_point = if let Some(rx_end_pos) = network_data_rx
.iter()
.position(|(time, _data)| *time >= time_start)
{
if rx_end_pos > 1 {
let rx_start_pos = rx_end_pos - 1;
let outside_rx_point = network_data_rx.get(rx_start_pos);
let inside_rx_point = network_data_rx.get(rx_end_pos);
if let (Some(outside_rx_point), Some(inside_rx_point)) =
(outside_rx_point, inside_rx_point)
{
let old = *outside_rx_point;
let new_point = (
time_start,
interpolate_points(outside_rx_point, inside_rx_point, time_start),
);
// debug!(
// "Interpolated between {:?} and {:?}, got rx for time {:?}: {:?}",
// outside_rx_point, inside_rx_point, time_start, new_point
// );
if let Some(to_replace) = network_data_rx.get_mut(rx_start_pos) {
*to_replace = new_point;
Some((rx_start_pos, old))
} else {
None // Failed to get mutable reference.
}
} else {
None // Point somehow doesn't exist in our network_data_rx
}
} else {
None // Point is already "leftmost", no need to interpolate.
}
} else {
None // There is no point.
};
let interpolated_tx_point = if let Some(tx_end_pos) = network_data_tx
.iter()
.position(|(time, _data)| *time >= time_start)
{
if tx_end_pos > 1 {
let tx_start_pos = tx_end_pos - 1;
let outside_tx_point = network_data_tx.get(tx_start_pos);
let inside_tx_point = network_data_tx.get(tx_end_pos);
if let (Some(outside_tx_point), Some(inside_tx_point)) =
(outside_tx_point, inside_tx_point)
{
let old = *outside_tx_point;
let new_point = (
time_start,
interpolate_points(outside_tx_point, inside_tx_point, time_start),
);
if let Some(to_replace) = network_data_tx.get_mut(tx_start_pos) {
*to_replace = new_point;
Some((tx_start_pos, old))
} else {
None // Failed to get mutable reference.
}
} else {
None // Point somehow doesn't exist in our network_data_tx
}
} else {
None // Point is already "leftmost", no need to interpolate.
}
} else {
None // There is no point.
};
// TODO: Cache network results: Only update if:
// - Force update (includes time interval change)
// - Old max time is off screen
// - A new time interval is better and does not fit (check from end of vector to last checked; we only want to update if it is TOO big!)
// Find the maximal rx/tx so we know how to scale, and return it.
let (_best_time, max_entry) = get_max_entry(
network_data_rx,
network_data_tx,
time_start,
&app_state.app_config_fields.network_scale_type,
app_state.app_config_fields.network_use_binary_prefix,
);
let (max_range, labels) = adjust_network_data_point(
max_entry,
&app_state.app_config_fields.network_scale_type,
&app_state.app_config_fields.network_unit_type,
app_state.app_config_fields.network_use_binary_prefix,
);
// Cache results.
// network_widget_state.draw_max_range_cache = max_range;
// network_widget_state.draw_time_start_cache = best_time;
// network_widget_state.draw_labels_cache = labels;
let y_axis_labels = labels
.iter()
.map(|label| Span::styled(label, self.colours.graph_style))
.collect::<Vec<_>>();
let y_axis = Axis::default()
.style(self.colours.graph_style)
.bounds([0.0, max_range])
.labels(y_axis_labels);
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let border_style = if is_on_widget {
self.colours.highlighted_border_style
} else {
self.colours.border_style
};
let title = if app_state.is_expanded {
const TITLE_BASE: &str = " Network ── Esc to go back ";
Spans::from(vec![
Span::styled(" Network ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to go back ",
"".repeat(usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(TITLE_BASE, true).count() + 2
))
),
border_style,
),
])
} else {
Spans::from(Span::styled(" Network ", self.colours.widget_title_style))
};
let legend_constraints = if hide_legend {
(Constraint::Ratio(0, 1), Constraint::Ratio(0, 1))
} else {
(Constraint::Ratio(1, 1), Constraint::Ratio(3, 4))
};
// TODO: Add support for clicking on legend to only show that value on chart.
let dataset = if app_state.app_config_fields.use_old_network_legend && !hide_legend {
vec![
Dataset::default()
.name(format!("RX: {:7}", app_state.canvas_data.rx_display))
.marker(if app_state.app_config_fields.use_dot {
Marker::Dot
} else {
Marker::Braille
})
.style(self.colours.rx_style)
.data(network_data_rx)
.graph_type(tui::widgets::GraphType::Line),
Dataset::default()
.name(format!("TX: {:7}", app_state.canvas_data.tx_display))
.marker(if app_state.app_config_fields.use_dot {
Marker::Dot
} else {
Marker::Braille
})
.style(self.colours.tx_style)
.data(network_data_tx)
.graph_type(tui::widgets::GraphType::Line),
Dataset::default()
.name(format!(
"Total RX: {:7}",
app_state.canvas_data.total_rx_display
))
.style(self.colours.total_rx_style),
Dataset::default()
.name(format!(
"Total TX: {:7}",
app_state.canvas_data.total_tx_display
))
.style(self.colours.total_tx_style),
]
} else {
vec![
Dataset::default()
.name(&app_state.canvas_data.rx_display)
.marker(if app_state.app_config_fields.use_dot {
Marker::Dot
} else {
Marker::Braille
})
.style(self.colours.rx_style)
.data(network_data_rx)
.graph_type(tui::widgets::GraphType::Line),
Dataset::default()
.name(&app_state.canvas_data.tx_display)
.marker(if app_state.app_config_fields.use_dot {
Marker::Dot
} else {
Marker::Braille
})
.style(self.colours.tx_style)
.data(network_data_tx)
.graph_type(tui::widgets::GraphType::Line),
]
};
f.render_widget(
Chart::new(dataset)
.block(
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(if app_state.current_widget.widget_id == widget_id {
self.colours.highlighted_border_style
} else {
self.colours.border_style
}),
)
.x_axis(x_axis)
.y_axis(y_axis)
.hidden_legend_constraints(legend_constraints),
draw_loc,
);
// Now if you're done, reset any interpolated points!
if let Some((index, old_value)) = interpolated_rx_point {
if let Some(to_replace) = network_data_rx.get_mut(index) {
*to_replace = old_value;
}
}
if let Some((index, old_value)) = interpolated_tx_point {
if let Some(to_replace) = network_data_tx.get_mut(index) {
*to_replace = old_value;
}
}
}
}
fn draw_network_labels<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
) {
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let rx_display = &app_state.canvas_data.rx_display;
let tx_display = &app_state.canvas_data.tx_display;
let total_rx_display = &app_state.canvas_data.total_rx_display;
let total_tx_display = &app_state.canvas_data.total_tx_display;
// Gross but I need it to work...
let total_network = vec![vec![
Text::raw(rx_display),
Text::raw(tx_display),
Text::raw(total_rx_display),
Text::raw(total_tx_display),
]];
let mapped_network = total_network
.into_iter()
.map(|val| Row::new(val).style(self.colours.text_style));
// Calculate widths
let intrinsic_widths = get_column_widths(
draw_loc.width,
&[None, None, None, None],
&(NETWORK_HEADERS_LENS
.iter()
.map(|s| Some(*s))
.collect::<Vec<_>>()),
&[Some(0.25); 4],
&(NETWORK_HEADERS_LENS
.iter()
.map(|s| Some(*s))
.collect::<Vec<_>>()),
true,
);
// Draw
f.render_widget(
Table::new(mapped_network)
.header(
Row::new(NETWORK_HEADERS.to_vec())
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.block(Block::default().borders(Borders::ALL).border_style(
if app_state.current_widget.widget_id == widget_id {
self.colours.highlighted_border_style
} else {
self.colours.border_style
},
))
.style(self.colours.text_style)
.widths(
&(intrinsic_widths
.iter()
.map(|calculated_width| Constraint::Length(*calculated_width as u16))
.collect::<Vec<_>>()),
),
draw_loc,
);
}
}

View File

@ -0,0 +1,911 @@
use crate::{
app::App,
canvas::{
drawing_utils::{get_column_widths, get_search_start_position, get_start_position},
Painter,
},
constants::*,
};
use tui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
terminal::Frame,
text::{Span, Spans, Text},
widgets::{Block, Borders, Paragraph, Row, Table},
};
use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
use unicode_width::UnicodeWidthStr;
use once_cell::sync::Lazy;
static PROCESS_HEADERS_HARD_WIDTH_NO_GROUP: Lazy<Vec<Option<u16>>> = Lazy::new(|| {
vec![
Some(7),
None,
Some(8),
Some(8),
Some(8),
Some(8),
Some(7),
Some(8),
#[cfg(target_family = "unix")]
None,
None,
]
});
static PROCESS_HEADERS_HARD_WIDTH_GROUPED: Lazy<Vec<Option<u16>>> = Lazy::new(|| {
vec![
Some(7),
None,
Some(8),
Some(8),
Some(8),
Some(8),
Some(7),
Some(8),
]
});
static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: Lazy<Vec<Option<f64>>> =
Lazy::new(|| vec![None, Some(0.7), None, None, None, None, None, None]);
static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: Lazy<Vec<Option<f64>>> =
Lazy::new(|| vec![None, Some(0.3), None, None, None, None, None, None]);
static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND: Lazy<Vec<Option<f64>>> = Lazy::new(|| {
vec![
None,
Some(0.7),
None,
None,
None,
None,
None,
None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2),
]
});
static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE: Lazy<Vec<Option<f64>>> = Lazy::new(|| {
vec![
None,
Some(0.5),
None,
None,
None,
None,
None,
None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2),
]
});
static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: Lazy<Vec<Option<f64>>> = Lazy::new(|| {
vec![
None,
Some(0.3),
None,
None,
None,
None,
None,
None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2),
]
});
pub trait ProcessTableWidget {
/// Draws and handles all process-related drawing. Use this.
/// - `widget_id` here represents the widget ID of the process widget itself!
fn draw_process_features<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
);
/// Draws the process sort box.
/// - `widget_id` represents the widget ID of the process widget itself.
///
/// This should not be directly called.
fn draw_processes_table<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
);
/// Draws the process search field.
/// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget
/// state that is stored.
///
/// This should not be directly called.
fn draw_search_field<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
);
/// Draws the process sort box.
/// - `widget_id` represents the widget ID of the sort box itself --- NOT the process widget
/// state that is stored.
///
/// This should not be directly called.
fn draw_process_sort<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
);
}
impl ProcessTableWidget for Painter {
fn draw_process_features<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
) {
if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
let search_height = if draw_border { 5 } else { 3 };
let is_sort_open = process_widget_state.is_sort_open;
let header_len = process_widget_state.columns.longest_header_len;
let mut proc_draw_loc = draw_loc;
if process_widget_state.is_search_enabled() {
let processes_chunk = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(search_height)])
.split(draw_loc);
proc_draw_loc = processes_chunk[0];
self.draw_search_field(
f,
app_state,
processes_chunk[1],
draw_border,
widget_id + 1,
);
}
if is_sort_open {
let processes_chunk = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(header_len + 4), Constraint::Min(0)])
.split(proc_draw_loc);
proc_draw_loc = processes_chunk[1];
self.draw_process_sort(
f,
app_state,
processes_chunk[0],
draw_border,
widget_id + 2,
);
}
self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id);
}
}
fn draw_processes_table<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
) {
let should_get_widget_bounds = app_state.should_get_widget_bounds();
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
let recalculate_column_widths =
should_get_widget_bounds || proc_widget_state.requires_redraw;
if proc_widget_state.requires_redraw {
proc_widget_state.requires_redraw = false;
}
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc)[0];
let (border_style, highlight_style) = if is_on_widget {
(
self.colours.highlighted_border_style,
self.colours.currently_selected_text_style,
)
} else {
(self.colours.border_style, self.colours.text_style)
};
let title_base = if app_state.app_config_fields.show_table_scroll_position {
if let Some(finalized_process_data) = app_state
.canvas_data
.finalized_process_data_map
.get(&widget_id)
{
let title = format!(
" Processes ({} of {}) ",
proc_widget_state
.scroll_state
.current_scroll_position
.saturating_add(1),
finalized_process_data.len()
);
if title.len() <= draw_loc.width as usize {
title
} else {
" Processes ".to_string()
}
} else {
" Processes ".to_string()
}
} else {
" Processes ".to_string()
};
let title = if app_state.is_expanded
&& !proc_widget_state
.process_search_state
.search_state
.is_enabled
&& !proc_widget_state.is_sort_open
{
const ESCAPE_ENDING: &str = "── Esc to go back ";
let (chosen_title_base, expanded_title_base) = {
let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING);
if temp_title_base.len() > draw_loc.width as usize {
(
" Processes ".to_string(),
format!("{}{}", " Processes ".to_string(), ESCAPE_ENDING),
)
} else {
(title_base, temp_title_base)
}
};
Spans::from(vec![
Span::styled(chosen_title_base, self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to go back ",
"".repeat(
usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(
expanded_title_base.as_str(),
true
)
.count()
+ 2
)
)
),
border_style,
),
])
} else {
Spans::from(Span::styled(title_base, self.colours.widget_title_style))
};
let process_block = if draw_border {
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(border_style)
} else if is_on_widget {
Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style)
} else {
Block::default().borders(Borders::NONE)
};
if let Some(process_data) = &app_state
.canvas_data
.stringified_process_data_map
.get(&widget_id)
{
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap))
.saturating_sub(self.table_height_offset),
),
&proc_widget_state.scroll_state.scroll_direction,
&mut proc_widget_state.scroll_state.previous_scroll_position,
proc_widget_state.scroll_state.current_scroll_position,
app_state.is_force_redraw,
);
// Sanity check
let start_position = if position >= process_data.len() {
process_data.len().saturating_sub(1)
} else {
position
};
let sliced_vec = &process_data[start_position..];
let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| {
(
data.iter()
.map(|(entry, _alternative)| entry)
.collect::<Vec<_>>(),
disabled,
)
});
let proc_table_state = &mut proc_widget_state.scroll_state.table_state;
proc_table_state.select(Some(
proc_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
// Draw!
let process_headers = proc_widget_state.columns.get_column_headers(
&proc_widget_state.process_sorting_type,
proc_widget_state.is_process_sort_descending,
);
// Calculate widths
// FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths
let hard_widths = if proc_widget_state.is_grouped {
&*PROCESS_HEADERS_HARD_WIDTH_GROUPED
} else {
&*PROCESS_HEADERS_HARD_WIDTH_NO_GROUP
};
if recalculate_column_widths {
let mut column_widths = process_headers
.iter()
.map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16)
.collect::<Vec<_>>();
let soft_widths_min = column_widths
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>();
proc_widget_state.table_width_state.desired_column_widths = {
for (row, _disabled) in processed_sliced_vec.clone() {
for (col, entry) in row.iter().enumerate() {
if let Some(col_width) = column_widths.get_mut(col) {
let grapheme_len = UnicodeWidthStr::width(entry.as_str());
if grapheme_len as u16 > *col_width {
*col_width = grapheme_len as u16;
}
}
}
}
column_widths
};
proc_widget_state.table_width_state.desired_column_widths = proc_widget_state
.table_width_state
.desired_column_widths
.iter()
.zip(hard_widths)
.map(|(current, hard)| {
if let Some(hard) = hard {
if *hard > *current {
*hard
} else {
*current
}
} else {
*current
}
})
.collect::<Vec<_>>();
let soft_widths_max = if proc_widget_state.is_grouped {
// Note grouped trees are not a thing.
if proc_widget_state.is_using_command {
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND
} else {
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE
}
} else if proc_widget_state.is_using_command {
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND
} else if proc_widget_state.is_tree_mode {
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE
} else {
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE
};
proc_widget_state.table_width_state.calculated_column_widths =
get_column_widths(
draw_loc.width,
hard_widths,
&soft_widths_min,
soft_widths_max,
&(proc_widget_state
.table_width_state
.desired_column_widths
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>()),
true,
);
// debug!(
// "DCW: {:?}",
// proc_widget_state.table_width_state.desired_column_widths
// );
// debug!(
// "CCW: {:?}",
// proc_widget_state.table_width_state.calculated_column_widths
// );
}
let dcw = &proc_widget_state.table_width_state.desired_column_widths;
let ccw = &proc_widget_state.table_width_state.calculated_column_widths;
let process_rows = sliced_vec.iter().map(|(data, disabled)| {
let truncated_data = data.iter().zip(hard_widths).enumerate().map(
|(itx, ((entry, alternative), width))| {
if let (Some(desired_col_width), Some(calculated_col_width)) =
(dcw.get(itx), ccw.get(itx))
{
if width.is_none() {
if *desired_col_width > *calculated_col_width
&& *calculated_col_width > 0
{
let graphemes =
UnicodeSegmentation::graphemes(entry.as_str(), true)
.collect::<Vec<&str>>();
if let Some(alternative) = alternative {
Text::raw(alternative)
} else if graphemes.len() > *calculated_col_width as usize
&& *calculated_col_width > 1
{
// Truncate with ellipsis
let first_n = graphemes
[..(*calculated_col_width as usize - 1)]
.concat();
Text::raw(format!("{}", first_n))
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
},
);
if *disabled {
Row::new(truncated_data).style(self.colours.disabled_text_style)
} else {
Row::new(truncated_data)
}
});
f.render_stateful_widget(
Table::new(process_rows)
.header(
Row::new(process_headers)
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.block(process_block)
.highlight_style(highlight_style)
.style(self.colours.text_style)
.widths(
&(proc_widget_state
.table_width_state
.calculated_column_widths
.iter()
.map(|calculated_width| {
Constraint::Length(*calculated_width as u16)
})
.collect::<Vec<_>>()),
),
margined_draw_loc,
proc_table_state,
);
} else {
f.render_widget(process_block, margined_draw_loc);
}
// Check if we need to update columnar bounds...
if recalculate_column_widths
|| proc_widget_state.columns.column_header_x_locs.is_none()
|| proc_widget_state.columns.column_header_y_loc.is_none()
{
// y location is just the y location of the widget + border size (1 normally, 0 in basic)
proc_widget_state.columns.column_header_y_loc =
Some(draw_loc.y + if draw_border { 1 } else { 0 });
// x location is determined using the x locations of the widget; just offset from the left bound
// as appropriate, and use the right bound as limiter.
let mut current_x_left = draw_loc.x + 1;
let max_x_right = draw_loc.x + draw_loc.width - 1;
let mut x_locs = vec![];
for width in proc_widget_state
.table_width_state
.calculated_column_widths
.iter()
{
let right_bound = current_x_left + width;
if right_bound < max_x_right {
x_locs.push((current_x_left, right_bound));
current_x_left = right_bound + 1;
} else {
x_locs.push((current_x_left, max_x_right));
break;
}
}
proc_widget_state.columns.column_header_x_locs = Some(x_locs);
}
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
fn draw_search_field<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
) {
fn build_query<'a>(
is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize,
cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style,
text_style: tui::style::Style,
) -> Vec<Span<'a>> {
let mut current_grapheme_posn = 0;
if is_on_widget {
let mut res = grapheme_indices
.filter_map(|grapheme| {
current_grapheme_posn += UnicodeWidthStr::width(grapheme.1);
if current_grapheme_posn <= start_position {
None
} else {
let styled = if grapheme.0 == cursor_position {
Span::styled(grapheme.1, currently_selected_text_style)
} else {
Span::styled(grapheme.1, text_style)
};
Some(styled)
}
})
.collect::<Vec<_>>();
if cursor_position == query.len() {
res.push(Span::styled(" ", currently_selected_text_style))
}
res
} else {
// This is easier - we just need to get a range of graphemes, rather than
// dealing with possibly inserting a cursor (as none is shown!)
vec![Span::styled(query.to_string(), text_style)]
}
}
// TODO: Make the cursor scroll back if there's space!
if let Some(proc_widget_state) =
app_state.proc_state.widget_states.get_mut(&(widget_id - 1))
{
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let num_columns = usize::from(draw_loc.width);
let search_title = "> ";
let num_chars_for_text = search_title.len();
let cursor_position = proc_widget_state.get_search_cursor_position();
let current_cursor_position = proc_widget_state.get_char_cursor_position();
let start_position: usize = get_search_start_position(
num_columns - num_chars_for_text - 5,
&proc_widget_state
.process_search_state
.search_state
.cursor_direction,
&mut proc_widget_state
.process_search_state
.search_state
.cursor_bar,
current_cursor_position,
app_state.is_force_redraw,
);
let query = proc_widget_state.get_current_search_query().as_str();
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true);
// TODO: [CURSOR] blank cursor if not selected
// TODO: [CURSOR] blinking cursor?
let query_with_cursor = build_query(
is_on_widget,
grapheme_indices,
start_position,
cursor_position,
query,
self.colours.currently_selected_text_style,
self.colours.text_style,
);
let mut search_text = vec![Spans::from({
let mut search_vec = vec![Span::styled(
search_title,
if is_on_widget {
self.colours.table_header_style
} else {
self.colours.text_style
},
)];
search_vec.extend(query_with_cursor);
search_vec
})];
// Text options shamelessly stolen from VS Code.
let case_style = if !proc_widget_state.process_search_state.is_ignoring_case {
self.colours.currently_selected_text_style
} else {
self.colours.text_style
};
let whole_word_style = if proc_widget_state
.process_search_state
.is_searching_whole_word
{
self.colours.currently_selected_text_style
} else {
self.colours.text_style
};
let regex_style = if proc_widget_state
.process_search_state
.is_searching_with_regex
{
self.colours.currently_selected_text_style
} else {
self.colours.text_style
};
// FIXME: [MOUSE] Mouse support for these in search
// FIXME: [MOVEMENT] Movement support for these in search
let option_text = Spans::from(vec![
Span::styled(
format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }),
case_style,
),
Span::raw(" "),
Span::styled(
format!("Whole({})", if self.is_mac_os { "F2" } else { "Alt+W" }),
whole_word_style,
),
Span::raw(" "),
Span::styled(
format!("Regex({})", if self.is_mac_os { "F3" } else { "Alt+R" }),
regex_style,
),
]);
search_text.push(Spans::from(Span::styled(
if let Some(err) = &proc_widget_state
.process_search_state
.search_state
.error_message
{
err.as_str()
} else {
""
},
self.colours.invalid_query_style,
)));
search_text.push(option_text);
let current_border_style = if proc_widget_state
.process_search_state
.search_state
.is_invalid_search
{
self.colours.invalid_query_style
} else if is_on_widget {
self.colours.highlighted_border_style
} else {
self.colours.border_style
};
let title = Span::styled(
if draw_border {
const TITLE_BASE: &str = " Esc to close ";
let repeat_num =
usize::from(draw_loc.width).saturating_sub(TITLE_BASE.chars().count() + 2);
format!("{} Esc to close ", "".repeat(repeat_num))
} else {
String::new()
},
current_border_style,
);
let process_search_block = if draw_border {
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(current_border_style)
} else if is_on_widget {
Block::default()
.borders(*SIDE_BORDERS)
.border_style(current_border_style)
} else {
Block::default().borders(Borders::NONE)
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc)[0];
f.render_widget(
Paragraph::new(search_text)
.block(process_search_block)
.style(self.colours.text_style)
.alignment(Alignment::Left),
margined_draw_loc,
);
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
fn draw_process_sort<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
) {
let is_on_widget = widget_id == app_state.current_widget.widget_id;
if let Some(proc_widget_state) =
app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
{
let current_scroll_position = proc_widget_state.columns.current_scroll_position;
let sort_string = proc_widget_state
.columns
.ordered_columns
.iter()
.filter(|column_type| {
proc_widget_state
.columns
.column_mapping
.get(column_type)
.unwrap()
.enabled
})
.map(|column_type| column_type.to_string())
.collect::<Vec<_>>();
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&proc_widget_state.columns.scroll_direction,
&mut proc_widget_state.columns.previous_scroll_position,
current_scroll_position,
app_state.is_force_redraw,
);
// Sanity check
let start_position = if position >= sort_string.len() {
sort_string.len().saturating_sub(1)
} else {
position
};
let sliced_vec = &sort_string[start_position..];
let sort_options = sliced_vec
.iter()
.map(|column| Row::new(vec![column.as_str()]));
let column_state = &mut proc_widget_state.columns.column_state;
column_state.select(Some(
proc_widget_state
.columns
.current_scroll_position
.saturating_sub(start_position),
));
let current_border_style = if proc_widget_state
.process_search_state
.search_state
.is_invalid_search
{
self.colours.invalid_query_style
} else if is_on_widget {
self.colours.highlighted_border_style
} else {
self.colours.border_style
};
let process_sort_block = if draw_border {
Block::default()
.borders(Borders::ALL)
.border_style(current_border_style)
} else if is_on_widget {
Block::default()
.borders(*SIDE_BORDERS)
.border_style(current_border_style)
} else {
Block::default().borders(Borders::NONE)
};
let highlight_style = if is_on_widget {
self.colours.currently_selected_text_style
} else {
self.colours.text_style
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc)[0];
f.render_stateful_widget(
Table::new(sort_options)
.header(
Row::new(vec!["Sort By"])
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.block(process_sort_block)
.highlight_style(highlight_style)
.style(self.colours.text_style)
.widths(&[Constraint::Percentage(100)]),
margined_draw_loc,
column_state,
);
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
}
}

View File

@ -14,6 +14,72 @@ FLAGS:
const USAGE: &str = "
btm [FLAG]";
const DEFAULT_WIDGET_TYPE_STR: &str = if cfg!(feature = "battery") {
"\
Sets which widget type to use as the default widget.
For the default layout, this defaults to the 'process' widget.
For a custom layout, it defaults to the first widget it sees.
For example, suppose we have a layout that looks like:
+-------------------+-----------------------+
| CPU (1) | CPU (2) |
+---------+---------+-------------+---------+
| Process | CPU (3) | Temperature | CPU (4) |
+---------+---------+-------------+---------+
Setting '--default_widget_type Temp' will make the Temperature
widget selected by default.
Supported widget names:
+--------------------------+
| cpu |
+--------------------------+
| mem, memory |
+--------------------------+
| net, network |
+--------------------------+
| proc, process, processes |
+--------------------------+
| temp, temperature |
+--------------------------+
| disk |
+--------------------------+
| batt, battery |
+--------------------------+
\n\n"
} else {
"\
Sets which widget type to use as the default widget.
For the default layout, this defaults to the 'process' widget.
For a custom layout, it defaults to the first widget it sees.
For example, suppose we have a layout that looks like:
+-------------------+-----------------------+
| CPU (1) | CPU (2) |
+---------+---------+-------------+---------+
| Process | CPU (3) | Temperature | CPU (4) |
+---------+---------+-------------+---------+
Setting '--default_widget_type Temp' will make the Temperature
widget selected by default.
Supported widget names:
+--------------------------+
| cpu |
+--------------------------+
| mem, memory |
+--------------------------+
| net, network |
+--------------------------+
| proc, process, processes |
+--------------------------+
| temp, temperature |
+--------------------------+
| disk |
+--------------------------+
\n\n"
};
pub fn get_matches() -> clap::ArgMatches<'static> {
build_app().get_matches()
}
@ -66,14 +132,6 @@ disabled via --hide_time then this will have no effect.\n\n\n",
Hides graphs and uses a more basic look. Design is largely
inspired by htop's.\n\n",
);
let battery = Arg::with_name("battery")
.long("battery")
.help("Shows the battery widget.")
.long_help(
"\
Shows the battery widget in default or basic mode. No effect on
custom layouts.\n\n",
);
let case_sensitive = Arg::with_name("case_sensitive")
.short("S")
.long("case_sensitive")
@ -318,40 +376,7 @@ use CPU (3) as the default instead.
.takes_value(true)
.value_name("WIDGET TYPE")
.help("Sets the default widget type, use --help for more info.")
.long_help(
"\
Sets which widget type to use as the default widget.
For the default layout, this defaults to the 'process' widget.
For a custom layout, it defaults to the first widget it sees.
For example, suppose we have a layout that looks like:
+-------------------+-----------------------+
| CPU (1) | CPU (2) |
+---------+---------+-------------+---------+
| Process | CPU (3) | Temperature | CPU (4) |
+---------+---------+-------------+---------+
Setting '--default_widget_type Temp' will make the Temperature
widget selected by default.
Supported widget names:
+--------------------------+
| cpu |
+--------------------------+
| mem, memory |
+--------------------------+
| net, network |
+--------------------------+
| proc, process, processes |
+--------------------------+
| temp, temperature |
+--------------------------+
| disk |
+--------------------------+
| batt, battery |
+--------------------------+
\n\n",
);
.long_help(DEFAULT_WIDGET_TYPE_STR);
let rate = Arg::with_name("rate")
.short("r")
.long("rate")
@ -408,7 +433,7 @@ Displays the network widget with a log scale. Defaults to a non-log scale.\n\n"
Displays the network widget with binary prefixes (i.e. kibibits, mebibits) rather than a decimal prefix (i.e. kilobits, megabits). Defaults to decimal prefixes.\n\n\n",
);
App::new(crate_name!())
let app = App::new(crate_name!())
.setting(AppSettings::UnifiedHelpMessage)
.version(crate_version!())
.author(crate_authors!())
@ -423,12 +448,10 @@ Displays the network widget with binary prefixes (i.e. kibibits, mebibits) rathe
.group(ArgGroup::with_name("TEMPERATURE_TYPE").args(&["kelvin", "fahrenheit", "celsius"]))
.arg(autohide_time)
.arg(basic)
.arg(battery)
.arg(case_sensitive)
.arg(process_command)
.arg(config_location)
.arg(color)
// .arg(debug)
.arg(mem_as_value)
.arg(default_time_value)
.arg(default_widget_count)
@ -442,7 +465,6 @@ Displays the network widget with binary prefixes (i.e. kibibits, mebibits) rathe
.arg(show_table_scroll_position)
.arg(left_legend)
.arg(disable_advanced_kill)
// .arg(no_write)
.arg(rate)
.arg(regex)
.arg(time_delta)
@ -452,5 +474,21 @@ Displays the network widget with binary prefixes (i.e. kibibits, mebibits) rathe
.arg(network_use_binary_prefix)
.arg(current_usage)
.arg(use_old_network_legend)
.arg(whole_word)
.arg(whole_word);
let app = if cfg!(feature = "battery") {
let battery = Arg::with_name("battery")
.long("battery")
.help("Shows the battery widget.")
.long_help(
"\
Shows the battery widget in default or basic mode. No effect on
custom layouts.\n\n",
);
app.arg(battery)
} else {
app
};
app
}

View File

@ -1337,6 +1337,7 @@ fn group_process_data(
.collect::<Vec<_>>()
}
#[cfg(feature = "battery")]
pub fn convert_battery_harvest(current_data: &DataCollection) -> Vec<ConvertedBatteryData> {
current_data
.battery_harvest