init commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/rust/.devcontainer/base.Dockerfile
|
||||||
|
|
||||||
|
# [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye
|
||||||
|
ARG VARIANT="buster"
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT}
|
||||||
|
|
||||||
|
RUN printf "alias cls='clear'\nalias ll='ls -l --human-readable --color=auto --group-directories-first --classify --time-style=long-iso -all'" >> /etc/bash.bashrc
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get -y install upx-ucl
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||||
|
// https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/rust
|
||||||
|
{
|
||||||
|
"name": "Rust",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile",
|
||||||
|
"args": {
|
||||||
|
// Use the VARIANT arg to pick a Debian OS version: buster, bullseye
|
||||||
|
// Use bullseye when on local on arm64/Apple Silicon.
|
||||||
|
"VARIANT": "bullseye"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runArgs": [
|
||||||
|
"--cap-add=SYS_PTRACE",
|
||||||
|
"--security-opt",
|
||||||
|
"seccomp=unconfined",
|
||||||
|
],
|
||||||
|
|
||||||
|
"mounts": [
|
||||||
|
"source=/etc/timezone,target=/etc/timezone,type=bind,readonly",
|
||||||
|
"source=/ramdrive,target=/ramdrive,type=bind",
|
||||||
|
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly",
|
||||||
|
"source=${localEnv:HOME}/.cargo/bin/cargo-watch,target=/usr/local/cargo/bin/cargo-watch,type=bind,readonly",
|
||||||
|
"source=${localEnv:HOME}/.cargo/bin/cross,target=/usr/local/cargo/bin/cross,type=bind,readonly",
|
||||||
|
],
|
||||||
|
|
||||||
|
// Set *default* container specific settings.json values on container create.
|
||||||
|
"settings": {
|
||||||
|
"lldb.executable": "/usr/bin/lldb",
|
||||||
|
// VS Code don't watch files under ./target
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/target/**": true
|
||||||
|
},
|
||||||
|
"rust-analyzer.checkOnSave.command": "clippy"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"vadimcn.vscode-lldb",
|
||||||
|
"mutantdino.resourcemonitor",
|
||||||
|
"matklad.rust-analyzer",
|
||||||
|
"tamasfe.even-better-toml",
|
||||||
|
"serayuzgur.crates",
|
||||||
|
"christian-kohler.path-intellisense",
|
||||||
|
"timonwong.shellcheck",
|
||||||
|
"ms-vscode.live-server",
|
||||||
|
"rangav.vscode-thunder-client",
|
||||||
|
"bmuskalla.vscode-tldr"
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
|
"remoteUser": "vscode",
|
||||||
|
"features": {
|
||||||
|
"docker-in-docker": "latest",
|
||||||
|
"git": "os-provided"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
*.sh eol=lf
|
||||||
|
*.md eol=CRLF
|
||||||
|
*.txt eol=lf
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. windows 11]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: New Feature
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[NEW FEATURE] "
|
||||||
|
labels: 'new feature'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Refactor
|
||||||
|
about: Refactor a component
|
||||||
|
title: "[REFACTOR] "
|
||||||
|
labels: 'refactor'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Component to refactor.**
|
||||||
|
What component(s) needs to be refactored?
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
@@ -0,0 +1,88 @@
|
|||||||
|
name: Release CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
|
# cache some rust data?
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
# Build for linux x86_64
|
||||||
|
- name: build release linux_x86_64
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: --release
|
||||||
|
- name: compress oxker_linux_x86_64 binary
|
||||||
|
run: tar -C target/release -czf ./oxker_linux_x86_64.tar.gz oxker
|
||||||
|
|
||||||
|
# Build for linux aarch64, aka 64 bit pi 4
|
||||||
|
- name: build release armv7
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
target: aarch64-unknown-linux-musl
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
use-cross: true
|
||||||
|
command: build
|
||||||
|
args: --target aarch64-unknown-linux-musl --release
|
||||||
|
- name: compress aarch64 binary
|
||||||
|
run: tar -C target/aarch64-unknown-linux-musl/release -czf ./oxker_linux_aarch64.tar.gz oxker
|
||||||
|
|
||||||
|
# Build for linux armv6, aka 32 bit pi zero w
|
||||||
|
- name: build release armv6
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
target: arm-unknown-linux-musleabihf
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
use-cross: true
|
||||||
|
command: build
|
||||||
|
args: --target arm-unknown-linux-musleabihf --release
|
||||||
|
- name: compress armv6 binary
|
||||||
|
run: tar -C target/arm-unknown-linux-musleabihf/release -czf ./oxker_linux_armv6.tar.gz oxker
|
||||||
|
|
||||||
|
# Build for windows
|
||||||
|
- name: build release windows_x86_64
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-pc-windows-gnu
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
use-cross: true
|
||||||
|
command: build
|
||||||
|
args: --target x86_64-pc-windows-gnu --release
|
||||||
|
- name: compress windows_x86_64 binary
|
||||||
|
run: zip -j ./oxker_windows_x86_64.zip target/x86_64-pc-windows-gnu/release/oxker.exe
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
name: ${{ github.ref_name }}
|
||||||
|
body_path: ".github/release-body.md"
|
||||||
|
draft: false
|
||||||
|
files: |
|
||||||
|
oxker_linux_x86_64.tar.gz
|
||||||
|
oxker_linux_aarch64.tar.gz
|
||||||
|
oxker_linux_armv6.tar.gz
|
||||||
|
oxker_windows_x86_64.zip
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
/releases
|
||||||
|
Cargo.lock
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
+ init commit
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
[package]
|
||||||
|
name = "oxker"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Jack Wills <email@mrjackwills.com>"]
|
||||||
|
description = "a simple tui to view & control docker containers"
|
||||||
|
repository = "https://github.com/mrjackwills/oxker"
|
||||||
|
license = "MIT"
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
bollard = "0.12.0"
|
||||||
|
cansi = "2.1.1"
|
||||||
|
clap={version="3.1.0", features = ["derive", "unicode"] }
|
||||||
|
crossterm = "0.23.2"
|
||||||
|
futures-util = "0.3.21"
|
||||||
|
parking_lot = {version= "0.12.0"}
|
||||||
|
tokio = {version = "1.17.0", features=["full"]}
|
||||||
|
tracing = "0.1.32"
|
||||||
|
tracing-subscriber = "0.3.9"
|
||||||
|
tui = "0.17"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = 'abort'
|
||||||
|
strip=true
|
||||||
|
debug = false
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Jack Wills
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src='./.github/logo.svg' width='200px'/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<h1 align="center">oxker</h1>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
A simple tui to view and control docker containers"
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Built in <a href='https://www.rust-lang.org/' target='_blank' rel='noopener noreferrer'>Rust</a>, making heavy use of <a href='https://github.com/fdehau/tui-rs' target='_blank' rel='noopener noreferrer'>tui-rs</a> & <a href='https://github.com/fussybeaver/bollard' target='_blank' rel='noopener noreferrer'>Bollard</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://raw.githubusercontent.com/mrjackwills/oxker/main/.github/screenshot_01.jpg" target='_blank' rel='noopener noreferrer'>
|
||||||
|
<img src='./.github/screenshot_01.jpg' width='100%'/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
See <a href="https://github.com/mrjackwills/oxker/releases" target='_blank' rel='noopener noreferrer'>releases</a>
|
||||||
|
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```./oxker```
|
||||||
|
|
||||||
|
available command line arguments
|
||||||
|
| argument|result|
|
||||||
|
|--|--|
|
||||||
|
|```-d [number > 0]```| set the update interval for docker information, is ms |
|
||||||
|
|```-r```| Show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) |
|
||||||
|
|```-c```| Attempt to color the logs (conflicts with -r) |
|
||||||
|
|```-t```| Remove timestamps from each log entry |
|
||||||
|
|```-g```| No tui, basically a pointless debugging mode, for now |
|
||||||
|
|
||||||
|
## Build step
|
||||||
|
|
||||||
|
### x86_64
|
||||||
|
|
||||||
|
```cargo build --release```
|
||||||
|
|
||||||
|
### Raspberry pi
|
||||||
|
|
||||||
|
requires docker & <a href='https://github.com/cross-rs/cross' target='_blank' rel='noopener noreferrer'>cross-rs</a>
|
||||||
|
|
||||||
|
|
||||||
|
#### 64bit pi (pi 4, pi zero w 2)
|
||||||
|
|
||||||
|
```cross build --target aarch64-unknown-linux-gnu --release```
|
||||||
|
|
||||||
|
#### 32bit pi (pi zero w)
|
||||||
|
|
||||||
|
Tested, and fully working on pi zero w, running Raspberry Pi OS 32 bit, the initial logs parsing can take an extended period of time if thousands of lines long
|
||||||
|
|
||||||
|
```cross build --target arm-unknown-linux-musleabihf --release```
|
||||||
|
|
||||||
|
If no memory information available, try appending ```/boot/cmdline.txt``` with
|
||||||
|
|
||||||
|
```cgroup_enable=cpuset cgroup_enable=memory```
|
||||||
|
|
||||||
|
see <a href="https://forums.raspberrypi.com/viewtopic.php?t=203128" target='_blank' rel='noopener noreferrer'>https://forums.raspberrypi.com/viewtopic.php?t=203128</a> and <a href="https://github.com/docker/for-linux/issues/1112" target='_blank' rel='noopener noreferrer'>https://github.com/docker/for-linux/issues/1112</a>
|
||||||
|
|
||||||
|
### Compress executable
|
||||||
|
|
||||||
|
compress output from \~3mb to ~1mb
|
||||||
|
|
||||||
|
```upx --best --lzma target/release/oxker -o ./oxker```
|
||||||
|
|
||||||
|
### Untested on other platforms
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
As of yet untested, needs work
|
||||||
|
|
||||||
|
```cargo test -- --test-threads=1```
|
||||||
|
|
||||||
|
Run some example docker images
|
||||||
|
|
||||||
|
```docker run --name redis -d redis:alpine3.15```
|
||||||
|
|
||||||
|
```docker run --name postgres -e POSTGRES_PASSWORD=never_use_this_password_in_production -d postgres:alpine```
|
||||||
|
|
||||||
|
```docker run -d --hostname my-rabbit --name rabbitmq rabbitmq:3```
|
||||||
|
|
||||||
|
|
||||||
Executable
+248
@@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# rust create_release
|
||||||
|
# v0.0.14
|
||||||
|
|
||||||
|
PACKAGE_NAME='oxker'
|
||||||
|
STAR_LINE='****************************************'
|
||||||
|
CWD=$(pwd)
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
PURPLE='\033[0;35m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
|
||||||
|
# $1 string - error message
|
||||||
|
error_close() {
|
||||||
|
echo -e "\n${RED}ERROR - EXITED: ${YELLOW}$1${RESET}\n";
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if [ -z "$PACKAGE_NAME" ]
|
||||||
|
then
|
||||||
|
error_close "No package name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# $1 string - question to ask
|
||||||
|
ask_yn () {
|
||||||
|
printf "%b%s? [y/N]:%b " "${GREEN}" "$1" "${RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# return user input
|
||||||
|
user_input() {
|
||||||
|
read -r data
|
||||||
|
echo "$data"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_major () {
|
||||||
|
local bumped_major
|
||||||
|
bumped_major=$((MAJOR + 1))
|
||||||
|
echo "${bumped_major}.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_minor () {
|
||||||
|
local bumped_minor
|
||||||
|
bumped_minor=$((MINOR + 1))
|
||||||
|
echo "${MAJOR}.${bumped_minor}.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_patch () {
|
||||||
|
local bumped_patch
|
||||||
|
bumped_patch=$((PATCH + 1))
|
||||||
|
echo "${MAJOR}.${MINOR}.${bumped_patch}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the url of the github repo, strip .git from the end of it
|
||||||
|
get_git_remote_url() {
|
||||||
|
REMOTE_ORIGIN=$(git config --get remote.origin.url)
|
||||||
|
TO_REMOVE=".git"
|
||||||
|
GIT_REPO_URL="${REMOTE_ORIGIN//$TO_REMOVE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that git status is clean
|
||||||
|
check_git_clean() {
|
||||||
|
GIT_CLEAN=$(git status --porcelain)
|
||||||
|
if [[ -n $GIT_CLEAN ]]
|
||||||
|
then
|
||||||
|
error_close "git dirty"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check currently on dev branch
|
||||||
|
check_git() {
|
||||||
|
CURRENT_GIT_BRANCH=$(git branch --show-current)
|
||||||
|
check_git_clean
|
||||||
|
if [[ ! "$CURRENT_GIT_BRANCH" =~ ^dev$ ]]
|
||||||
|
then
|
||||||
|
error_close "not on dev branch"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ask user if current changelog is acceptable
|
||||||
|
ask_changelog_update() {
|
||||||
|
echo "${STAR_LINE}"
|
||||||
|
RELEASE_BODY_TEXT=$(sed '/# <a href=/Q' CHANGELOG.md)
|
||||||
|
printf "%s" "$RELEASE_BODY_TEXT"
|
||||||
|
printf "\n%s\n" "${STAR_LINE}"
|
||||||
|
ask_yn "accept release body"
|
||||||
|
if [[ "$(user_input)" =~ ^y$ ]]
|
||||||
|
then
|
||||||
|
update_release_body_and_changelog "$RELEASE_BODY_TEXT"
|
||||||
|
else
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Edit the release-body to include new liens from changelog
|
||||||
|
# add commit urls to changelog
|
||||||
|
# $1 RELEASE_BODY
|
||||||
|
update_release_body_and_changelog () {
|
||||||
|
echo -e
|
||||||
|
DATE_SUBHEADING="### $(date +'%Y-%m-%d')\n\n"
|
||||||
|
RELEASE_BODY_ADDITION="${DATE_SUBHEADING}$1"
|
||||||
|
echo -e "${RELEASE_BODY_ADDITION}\n\nsee <a href='${GIT_REPO_URL}/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details" > .github/release-body.md
|
||||||
|
echo -e "# <a href='${GIT_REPO_URL}/releases/tag/${NEW_TAG_VERSION}'>${NEW_TAG_VERSION}</a>\n${DATE_SUBHEADING}${CHANGELOG_ADDITION}$(cat CHANGELOG.md)" > CHANGELOG.md
|
||||||
|
sed -i -E "s=(\s)\[([0-9a-f]{40})\](\n|\s|\,|\r)= [\2](${GIT_REPO_URL}/commit/\2),=g" ./CHANGELOG.md
|
||||||
|
}
|
||||||
|
|
||||||
|
# update version in cargo.toml, to match selected current version/tag
|
||||||
|
update_cargo_toml () {
|
||||||
|
sed -i "s|^version = .*|version = \"${NEW_TAG_VERSION:1}\"|" Cargo.toml
|
||||||
|
}
|
||||||
|
|
||||||
|
# Work out the current version, based on git tags
|
||||||
|
# create new semver version based on user input
|
||||||
|
check_tag () {
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0 --always)
|
||||||
|
echo -e "\nCurrent tag: ${PURPLE}${LATEST_TAG}${RESET}\n"
|
||||||
|
echo -e "${YELLOW}Choose new tag version:${RESET}\n"
|
||||||
|
if [[ $LATEST_TAG =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]
|
||||||
|
then
|
||||||
|
IFS="." read -r MAJOR MINOR PATCH <<< "${LATEST_TAG:1}"
|
||||||
|
else
|
||||||
|
MAJOR="0"
|
||||||
|
MINOR="0"
|
||||||
|
PATCH="0"
|
||||||
|
fi
|
||||||
|
MAJOR_TAG=v$(update_major)
|
||||||
|
MINOR_TAG=v$(update_minor)
|
||||||
|
PATCH_TAG=v$(update_patch)
|
||||||
|
OP_MAJOR="major___$MAJOR_TAG"
|
||||||
|
OP_MINOR="minor___$MINOR_TAG"
|
||||||
|
OP_PATCH="patch___$PATCH_TAG"
|
||||||
|
OPTIONS=("$OP_MAJOR" "$OP_MINOR" "$OP_PATCH")
|
||||||
|
select choice in "${OPTIONS[@]}"
|
||||||
|
do
|
||||||
|
case $choice in
|
||||||
|
"$OP_MAJOR" )
|
||||||
|
NEW_TAG_VERSION="$MAJOR_TAG"
|
||||||
|
break;;
|
||||||
|
"$OP_MINOR")
|
||||||
|
NEW_TAG_VERSION="$MINOR_TAG"
|
||||||
|
break;;
|
||||||
|
"$OP_PATCH")
|
||||||
|
NEW_TAG_VERSION="$PATCH_TAG"
|
||||||
|
break;;
|
||||||
|
*)
|
||||||
|
error_close "invalid option $REPLY"
|
||||||
|
break;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ask continue, or quit
|
||||||
|
ask_continue () {
|
||||||
|
ask_yn "continue"
|
||||||
|
if [[ ! "$(user_input)" =~ ^y$ ]]
|
||||||
|
then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# run all tests
|
||||||
|
cargo_test () {
|
||||||
|
cargo test -- --test-threads=1
|
||||||
|
ask_continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build for linux, pi 32, pi 64, and windows
|
||||||
|
cargo_build_all() {
|
||||||
|
cargo build --release
|
||||||
|
cross build --target aarch64-unknown-linux-musl --release
|
||||||
|
cross build --target arm-unknown-linux-musleabihf --release
|
||||||
|
cross build --target x86_64-pc-windows-gnu --release
|
||||||
|
tar -C target/arm-unknown-linux-musleabihf/release -czf ./releases/oxker_linux_armv6.tar.gz oxker
|
||||||
|
tar -C target/aarch64-unknown-linux-musl/release -czf ./releases/oxker_linux_aarch64.tar.gz oxker
|
||||||
|
zip -j ./releases/oxker_windows_x86_64.zip target/x86_64-pc-windows-gnu/release/oxker.exe
|
||||||
|
tar -C target/release -czf ./releases/oxker_linux_x86_64.tar.gz oxker
|
||||||
|
}
|
||||||
|
|
||||||
|
# Full flow to create a new release
|
||||||
|
release_flow() {
|
||||||
|
check_git
|
||||||
|
get_git_remote_url
|
||||||
|
cargo_test
|
||||||
|
cd "${CWD}" || error_close "Can't find ${CWD}"
|
||||||
|
check_tag
|
||||||
|
printf "\nnew tag chosen: %s\n\n" "${NEW_TAG_VERSION}"
|
||||||
|
RELEASE_BRANCH=release-$NEW_TAG_VERSION
|
||||||
|
echo -e
|
||||||
|
ask_changelog_update
|
||||||
|
git checkout -b "$RELEASE_BRANCH"
|
||||||
|
update_cargo_toml
|
||||||
|
git add .
|
||||||
|
git commit -m "chore: release $NEW_TAG_VERSION"
|
||||||
|
git checkout main
|
||||||
|
git merge --no-ff "$RELEASE_BRANCH" -m "chore: merge ${RELEASE_BRANCH} into main"
|
||||||
|
git tag -am "${RELEASE_BRANCH}" "$NEW_TAG_VERSION"
|
||||||
|
echo "git tag -am \"${RELEASE_BRANCH}\" \"$NEW_TAG_VERSION\""
|
||||||
|
git push --atomic origin main "$NEW_TAG_VERSION"
|
||||||
|
git checkout dev
|
||||||
|
git merge --no-ff main -m 'chore: merge main into dev'
|
||||||
|
git branch -d "$RELEASE_BRANCH"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cmd=(dialog --backtitle "Choose build option" --radiolist "choose" 14 80 16)
|
||||||
|
options=(
|
||||||
|
1 "fmt" off
|
||||||
|
2 "build" off
|
||||||
|
3 "test" off
|
||||||
|
4 "release" off
|
||||||
|
)
|
||||||
|
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
|
||||||
|
exitStatus=$?
|
||||||
|
clear
|
||||||
|
if [ $exitStatus -ne 0 ]; then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
for choice in $choices
|
||||||
|
do
|
||||||
|
case $choice in
|
||||||
|
0)
|
||||||
|
exit
|
||||||
|
break;;
|
||||||
|
1)
|
||||||
|
cargo fmt
|
||||||
|
main
|
||||||
|
break;;
|
||||||
|
2)
|
||||||
|
cargo_build_all
|
||||||
|
main
|
||||||
|
break;;
|
||||||
|
3)
|
||||||
|
npm_test
|
||||||
|
main
|
||||||
|
break;;
|
||||||
|
4)
|
||||||
|
release_flow
|
||||||
|
break;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
use std::{cmp::Ordering, collections::VecDeque, fmt};
|
||||||
|
|
||||||
|
use tui::{
|
||||||
|
style::Color,
|
||||||
|
widgets::{ListItem, ListState},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StatefulList<T> {
|
||||||
|
pub state: ListState,
|
||||||
|
pub items: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StatefulList<T> {
|
||||||
|
pub fn new(items: Vec<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
state: ListState::default(),
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn end(&mut self) {
|
||||||
|
let len = self.items.len();
|
||||||
|
if len > 0 {
|
||||||
|
self.state.select(Some(self.items.len() - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
self.state.select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
if !self.items.is_empty() {
|
||||||
|
let i = match self.state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i < self.items.len() - 1 {
|
||||||
|
i + 1
|
||||||
|
} else {
|
||||||
|
i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.state.select(Some(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&mut self) {
|
||||||
|
if !self.items.is_empty() {
|
||||||
|
let i = match self.state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.state.select(Some(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_state_title(&self) -> String {
|
||||||
|
if self.items.is_empty() {
|
||||||
|
String::from("")
|
||||||
|
} else {
|
||||||
|
let len = self.items.len();
|
||||||
|
let c = if let Some(value) = self.state.selected() {
|
||||||
|
if len > 0 {
|
||||||
|
value + 1
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
format!("{}/{}", c, self.items.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// States of the container
|
||||||
|
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||||
|
pub enum State {
|
||||||
|
Dead,
|
||||||
|
Exited,
|
||||||
|
Paused,
|
||||||
|
Removing,
|
||||||
|
Restarting,
|
||||||
|
Running,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
pub fn get_color(&self) -> Color {
|
||||||
|
match self {
|
||||||
|
Self::Running => Color::Green,
|
||||||
|
Self::Removing => Color::LightRed,
|
||||||
|
Self::Restarting => Color::LightGreen,
|
||||||
|
Self::Paused => Color::Yellow,
|
||||||
|
_ => Color::Red,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for State {
|
||||||
|
fn from(input: &str) -> Self {
|
||||||
|
match input {
|
||||||
|
"dead" => Self::Dead,
|
||||||
|
"exited" => Self::Exited,
|
||||||
|
"paused" => Self::Paused,
|
||||||
|
"removing" => Self::Removing,
|
||||||
|
"restarting" => Self::Restarting,
|
||||||
|
"running" => Self::Running,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for State {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let disp = match self {
|
||||||
|
Self::Dead => "✖ dead",
|
||||||
|
Self::Exited => "✖ exited",
|
||||||
|
Self::Paused => "॥ paused",
|
||||||
|
Self::Removing => "removing",
|
||||||
|
Self::Restarting => "↻ restarting",
|
||||||
|
Self::Running => "✓ running",
|
||||||
|
Self::Unknown => "? unknown",
|
||||||
|
};
|
||||||
|
write!(f, "{}", disp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Items for the container control list
|
||||||
|
/// Should probably have a vec for each container
|
||||||
|
/// so that can remove Pause if container currently Paused etc
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DockerControls {
|
||||||
|
Pause,
|
||||||
|
Unpause,
|
||||||
|
Restart,
|
||||||
|
Stop,
|
||||||
|
Start,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DockerControls {
|
||||||
|
pub fn get_color(&self) -> Color {
|
||||||
|
match self {
|
||||||
|
Self::Start => Color::Green,
|
||||||
|
Self::Stop => Color::Red,
|
||||||
|
Self::Restart => Color::Magenta,
|
||||||
|
Self::Pause => Color::Yellow,
|
||||||
|
Self::Unpause => Color::Blue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_vec(state: &State) -> Vec<Self> {
|
||||||
|
match state {
|
||||||
|
State::Dead | State::Exited => vec![Self::Start, Self::Restart],
|
||||||
|
State::Paused => vec![Self::Unpause, Self::Stop],
|
||||||
|
State::Running => vec![Self::Pause, Self::Restart, Self::Stop],
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DockerControls {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let disp = match self {
|
||||||
|
Self::Pause => "pause",
|
||||||
|
Self::Unpause => "unpause",
|
||||||
|
Self::Restart => "restart",
|
||||||
|
Self::Stop => "stop",
|
||||||
|
Self::Start => "start",
|
||||||
|
};
|
||||||
|
write!(f, "{}", disp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Stats {
|
||||||
|
fn get_value(&self) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Struct for frequently updated CPU stats
|
||||||
|
/// So can use custom display formatter
|
||||||
|
/// Use trait Stats for use as generic in draw_chart function
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CpuStats {
|
||||||
|
value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CpuStats {
|
||||||
|
pub fn new(value: f64) -> Self {
|
||||||
|
Self { value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for CpuStats {}
|
||||||
|
|
||||||
|
impl PartialEq for CpuStats {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.value == other.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for CpuStats {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
self.value.partial_cmp(&other.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for CpuStats {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
if self.value > other.value {
|
||||||
|
Ordering::Greater
|
||||||
|
} else if self.value == other.value {
|
||||||
|
Ordering::Equal
|
||||||
|
} else {
|
||||||
|
Ordering::Less
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stats for CpuStats {
|
||||||
|
fn get_value(&self) -> f64 {
|
||||||
|
self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CpuStats {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let disp = format!("{:05.2}%", self.value);
|
||||||
|
write!(f, "{:>x$}", disp, x = f.width().unwrap_or(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Struct for frequently updated memory usage stats
|
||||||
|
/// So can use custom display formatter
|
||||||
|
/// Use trait Stats for use as generic in draw_chart function
|
||||||
|
#[derive(Clone, Debug, Eq)]
|
||||||
|
pub struct ByteStats {
|
||||||
|
value: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ByteStats {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.value == other.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for ByteStats {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
self.value.partial_cmp(&other.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for ByteStats {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.value.cmp(&other.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ByteStats {
|
||||||
|
pub fn new(value: u64) -> Self {
|
||||||
|
Self { value }
|
||||||
|
}
|
||||||
|
pub fn update(&mut self, value: u64) {
|
||||||
|
self.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Stats for ByteStats {
|
||||||
|
fn get_value(&self) -> f64 {
|
||||||
|
self.value as f64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert from bytes to kb, mb, gb etc
|
||||||
|
impl fmt::Display for ByteStats {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let one_kb = 1000.0;
|
||||||
|
let one_mb = one_kb * one_kb;
|
||||||
|
let one_gb = one_mb * 1000.0;
|
||||||
|
let as_f64 = self.value as f64;
|
||||||
|
let p = match as_f64 {
|
||||||
|
x if x >= one_gb => format!("{y:.2} GB", y = as_f64 / one_gb),
|
||||||
|
x if x >= one_kb => format!("{y:.2} MB", y = as_f64 / one_mb),
|
||||||
|
x if x >= one_mb => format!("{y:.2} kB", y = as_f64 / one_kb),
|
||||||
|
_ => format!("{} B", self.value),
|
||||||
|
};
|
||||||
|
write!(f, "{:>x$}", p, x = f.width().unwrap_or(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info for each container
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ContainerItem {
|
||||||
|
pub cpu_stats: VecDeque<CpuStats>,
|
||||||
|
pub docker_controls: StatefulList<DockerControls>,
|
||||||
|
pub id: String,
|
||||||
|
pub image: String,
|
||||||
|
pub last_updated: u64,
|
||||||
|
pub logs: StatefulList<ListItem<'static>>,
|
||||||
|
pub mem_limit: ByteStats,
|
||||||
|
pub mem_stats: VecDeque<ByteStats>,
|
||||||
|
pub name: String,
|
||||||
|
pub net_rx: ByteStats,
|
||||||
|
pub net_tx: ByteStats,
|
||||||
|
pub state: State,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MemTuple = (Vec<(f64, f64)>, ByteStats, State);
|
||||||
|
pub type CpuTuple = (Vec<(f64, f64)>, CpuStats, State);
|
||||||
|
|
||||||
|
impl ContainerItem {
|
||||||
|
/// Create a new container item
|
||||||
|
pub fn new(id: String, status: String, image: String, state: State, name: String) -> Self {
|
||||||
|
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(&state));
|
||||||
|
docker_controls.start();
|
||||||
|
Self {
|
||||||
|
cpu_stats: VecDeque::with_capacity(60),
|
||||||
|
docker_controls,
|
||||||
|
id,
|
||||||
|
image,
|
||||||
|
last_updated: 0,
|
||||||
|
logs: StatefulList::new(vec![]),
|
||||||
|
mem_limit: ByteStats::new(0),
|
||||||
|
mem_stats: VecDeque::with_capacity(60),
|
||||||
|
name,
|
||||||
|
net_rx: ByteStats::new(0),
|
||||||
|
net_tx: ByteStats::new(0),
|
||||||
|
state,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the max value in the last 30 items in the cpu stats vec
|
||||||
|
fn max_cpu_stats(&self) -> CpuStats {
|
||||||
|
match self.cpu_stats.iter().max() {
|
||||||
|
Some(value) => value.to_owned(),
|
||||||
|
None => CpuStats::new(0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the max value in the last 30 items in the mem stats vec
|
||||||
|
fn max_mem_stats(&self) -> ByteStats {
|
||||||
|
match self.mem_stats.iter().max() {
|
||||||
|
Some(value) => value.to_owned(),
|
||||||
|
None => ByteStats::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert cpu stats into a vec for the charts function
|
||||||
|
fn get_cpu_dataset(&self) -> Vec<(f64, f64)> {
|
||||||
|
self.cpu_stats
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|i| (i.0 as f64, i.1.value))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert mem stats into a vec for the charts function
|
||||||
|
fn get_mem_dataset(&self) -> Vec<(f64, f64)> {
|
||||||
|
self.mem_stats
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|i| (i.0 as f64, i.1.value as f64))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all cpu chart data
|
||||||
|
fn get_cpu_chart_data(&self) -> CpuTuple {
|
||||||
|
(
|
||||||
|
self.get_cpu_dataset(),
|
||||||
|
self.max_cpu_stats(),
|
||||||
|
self.state.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all mem chart data
|
||||||
|
fn get_mem_chart_data(&self) -> MemTuple {
|
||||||
|
(
|
||||||
|
self.get_mem_dataset(),
|
||||||
|
self.max_mem_stats(),
|
||||||
|
self.state.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get chart info for cpu & memory in one function
|
||||||
|
/// So only need to call .lock() once
|
||||||
|
pub fn get_chart_data(&self) -> (CpuTuple, MemTuple) {
|
||||||
|
(self.get_cpu_chart_data(), self.get_mem_chart_data())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Container information panel headings + widths, for nice pretty formatting
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Columns {
|
||||||
|
pub cpu: (String, usize),
|
||||||
|
pub image: (String, usize),
|
||||||
|
pub name: (String, usize),
|
||||||
|
pub state: (String, usize),
|
||||||
|
pub status: (String, usize),
|
||||||
|
pub mem: (String, usize),
|
||||||
|
pub net_rx: (String, usize),
|
||||||
|
pub net_tx: (String, usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Columns {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
// 7 to allow for 100.00%
|
||||||
|
cpu: (String::from("cpu"), 7),
|
||||||
|
image: (String::from("image"), 5),
|
||||||
|
name: (String::from("name"), 4),
|
||||||
|
state: (String::from("state"), 11),
|
||||||
|
status: (String::from("status"), 16),
|
||||||
|
mem: (String::from("mem/limit"), 9),
|
||||||
|
net_rx: (String::from("↓ rx"), 5),
|
||||||
|
net_tx: (String::from("↑ tx"), 5),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
use bollard::models::ContainerSummary;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tui::widgets::ListItem;
|
||||||
|
|
||||||
|
mod container_state;
|
||||||
|
|
||||||
|
use crate::{app_error::AppError, parse_args::CliArgs, ui::log_sanitizer};
|
||||||
|
pub use container_state::*;
|
||||||
|
|
||||||
|
/// Global app_state, stored in an Arc<Mutex>
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AppData {
|
||||||
|
args: CliArgs,
|
||||||
|
error: Option<AppError>,
|
||||||
|
logs_parsed: bool,
|
||||||
|
pub containers: StatefulList<ContainerItem>,
|
||||||
|
pub init: bool,
|
||||||
|
pub show_error: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppData {
|
||||||
|
/// Generate a default app_state
|
||||||
|
pub fn default(args: CliArgs) -> Self {
|
||||||
|
Self {
|
||||||
|
args,
|
||||||
|
containers: StatefulList::new(vec![]),
|
||||||
|
error: None,
|
||||||
|
init: false,
|
||||||
|
logs_parsed: false,
|
||||||
|
show_error: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Current time as unix timestamp
|
||||||
|
fn get_systemtime(&self) -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("In our known reality, this error should never occur")
|
||||||
|
.as_secs()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Get the current select docker command
|
||||||
|
/// So know which command to execute
|
||||||
|
pub fn get_docker_command(&self) -> Option<DockerControls> {
|
||||||
|
let mut output = None;
|
||||||
|
if let Some(index) = self.containers.state.selected() {
|
||||||
|
if let Some(control_index) = self.containers.items[index]
|
||||||
|
.docker_controls
|
||||||
|
.state
|
||||||
|
.selected()
|
||||||
|
{
|
||||||
|
output =
|
||||||
|
Some(self.containers.items[index].docker_controls.items[control_index].clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change selected choice of docker commands of selected container
|
||||||
|
pub fn docker_command_next(&mut self) {
|
||||||
|
if let Some(index) = self.containers.state.selected() {
|
||||||
|
self.containers.items[index].docker_controls.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change selected choice of docker commands of selected container
|
||||||
|
pub fn docker_command_previous(&mut self) {
|
||||||
|
if let Some(index) = self.containers.state.selected() {
|
||||||
|
self.containers.items[index].docker_controls.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change selected choice of docker commands of selected container
|
||||||
|
pub fn docker_command_start(&mut self) {
|
||||||
|
if let Some(index) = self.containers.state.selected() {
|
||||||
|
self.containers.items[index].docker_controls.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change selected choice of docker commands of selected container
|
||||||
|
pub fn docker_command_end(&mut self) {
|
||||||
|
if let Some(index) = self.containers.state.selected() {
|
||||||
|
self.containers.items[index].docker_controls.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return single app_state error
|
||||||
|
pub fn get_error(&self) -> Option<AppError> {
|
||||||
|
self.error.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove single app_state error
|
||||||
|
pub fn remove_error(&mut self) {
|
||||||
|
self.error = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// insert single app_state error
|
||||||
|
pub fn set_error(&mut self, error: AppError) {
|
||||||
|
self.error = Some(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the if of the currently selected container
|
||||||
|
/// If any containers on system, will always return
|
||||||
|
/// Only returns None when no containers found
|
||||||
|
pub fn get_selected_container_id(&self) -> Option<String> {
|
||||||
|
let mut output = None;
|
||||||
|
if let Some(index) = self.containers.state.selected() {
|
||||||
|
let id = self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.skip(index)
|
||||||
|
.take(1)
|
||||||
|
.map(|i| i.id.to_owned())
|
||||||
|
.collect::<String>();
|
||||||
|
output = Some(id)
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the index of the currently selected single log line
|
||||||
|
pub fn get_selected_log_index(&self) -> Option<usize> {
|
||||||
|
let mut output = None;
|
||||||
|
if let Some(id) = self.get_selected_container_id() {
|
||||||
|
if let Some(index) = self.containers.items.iter().position(|i| i.id == id) {
|
||||||
|
output = Some(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the title for log panel for selected container
|
||||||
|
/// will be "logs x/x"
|
||||||
|
pub fn get_log_title(&self) -> String {
|
||||||
|
if let Some(index) = self.get_selected_log_index() {
|
||||||
|
self.containers.items[index].logs.get_state_title()
|
||||||
|
} else {
|
||||||
|
String::from("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// select next selected log line
|
||||||
|
pub fn log_next(&mut self) {
|
||||||
|
if let Some(index) = self.get_selected_log_index() {
|
||||||
|
self.containers.items[index].logs.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// select previous selected log line
|
||||||
|
pub fn log_previous(&mut self) {
|
||||||
|
if let Some(index) = self.get_selected_log_index() {
|
||||||
|
self.containers.items[index].logs.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// select last selected log line
|
||||||
|
pub fn log_end(&mut self) {
|
||||||
|
if let Some(index) = self.get_selected_log_index() {
|
||||||
|
self.containers.items[index].logs.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// select first selected log line
|
||||||
|
pub fn log_start(&mut self) {
|
||||||
|
if let Some(index) = self.get_selected_log_index() {
|
||||||
|
self.containers.items[index].logs.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialised(&mut self, all_ids: &[(bool, String)]) -> bool {
|
||||||
|
let count_is_running = all_ids.iter().filter(|i| i.0).count();
|
||||||
|
let number_with_cpu_status = self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|i| !i.cpu_stats.is_empty())
|
||||||
|
.count();
|
||||||
|
self.logs_parsed && count_is_running == number_with_cpu_status
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Just get the total number of containers
|
||||||
|
pub fn get_container_len(&self) -> usize {
|
||||||
|
self.containers.items.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the widths for the strings in the containers panel
|
||||||
|
/// So can display nicely and evenly
|
||||||
|
pub fn get_width(&self) -> Columns {
|
||||||
|
let mut output = Columns::new();
|
||||||
|
let count = |x: &String| x.chars().count();
|
||||||
|
|
||||||
|
for container in self.containers.items.iter() {
|
||||||
|
let cpu_count = count(
|
||||||
|
&container
|
||||||
|
.cpu_stats
|
||||||
|
.back()
|
||||||
|
.unwrap_or(&CpuStats::new(0.0))
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
let mem_count = count(&format!(
|
||||||
|
"{} / {}",
|
||||||
|
container.mem_stats.back().unwrap_or(&ByteStats::new(0)),
|
||||||
|
container.mem_limit
|
||||||
|
));
|
||||||
|
|
||||||
|
let net_rx_count = count(&container.net_rx.to_string());
|
||||||
|
let net_tx_count = count(&container.net_tx.to_string());
|
||||||
|
let image_count = count(&container.image);
|
||||||
|
let name_count = count(&container.name);
|
||||||
|
let state_count = count(&container.state.to_string());
|
||||||
|
let status_count = count(&container.status);
|
||||||
|
|
||||||
|
if cpu_count > output.cpu.1 {
|
||||||
|
output.cpu.1 = cpu_count;
|
||||||
|
};
|
||||||
|
if image_count > output.image.1 {
|
||||||
|
output.image.1 = image_count;
|
||||||
|
};
|
||||||
|
if mem_count > output.mem.1 {
|
||||||
|
output.mem.1 = mem_count;
|
||||||
|
};
|
||||||
|
if name_count > output.name.1 {
|
||||||
|
output.name.1 = name_count;
|
||||||
|
};
|
||||||
|
if state_count > output.state.1 {
|
||||||
|
output.state.1 = state_count;
|
||||||
|
};
|
||||||
|
if status_count > output.status.1 {
|
||||||
|
output.status.1 = status_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
if net_rx_count > output.net_rx.1 {
|
||||||
|
output.net_rx.1 = net_rx_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
if net_tx_count > output.net_tx.1 {
|
||||||
|
output.net_tx.1 = net_tx_count;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all containers ids
|
||||||
|
pub fn get_all_ids(&self) -> Vec<String> {
|
||||||
|
self.containers
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.id.to_owned())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// find container given id
|
||||||
|
fn get_container_by_id(&mut self, id: &str) -> Option<&mut ContainerItem> {
|
||||||
|
self.containers.items.iter_mut().find(|i| i.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update container mem + cpu stats, in single function so only need to call .lock() once
|
||||||
|
pub fn update_stats(
|
||||||
|
&mut self,
|
||||||
|
id: String,
|
||||||
|
cpu_stat: Option<f64>,
|
||||||
|
mem_stat: Option<u64>,
|
||||||
|
mem_limit: u64,
|
||||||
|
rx: u64,
|
||||||
|
tx: u64,
|
||||||
|
) {
|
||||||
|
if let Some(container) = self.get_container_by_id(&id) {
|
||||||
|
if container.cpu_stats.len() >= 60 {
|
||||||
|
container.cpu_stats.pop_front();
|
||||||
|
}
|
||||||
|
if container.mem_stats.len() >= 60 {
|
||||||
|
container.mem_stats.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cpu) = cpu_stat {
|
||||||
|
container.cpu_stats.push_back(CpuStats::new(cpu));
|
||||||
|
}
|
||||||
|
if let Some(mem) = mem_stat {
|
||||||
|
container.mem_stats.push_back(ByteStats::new(mem));
|
||||||
|
}
|
||||||
|
|
||||||
|
container.net_rx.update(rx);
|
||||||
|
container.net_tx.update(tx);
|
||||||
|
container.mem_limit.update(mem_limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update, or insert, containers
|
||||||
|
pub fn update_containers(&mut self, containers: &[ContainerSummary]) {
|
||||||
|
let all_ids = self.get_all_ids();
|
||||||
|
|
||||||
|
if !containers.is_empty() && self.containers.state.selected().is_none() {
|
||||||
|
self.containers.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, id) in all_ids.iter().enumerate() {
|
||||||
|
if !containers
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.id.as_ref().unwrap())
|
||||||
|
.any(|x| x == id)
|
||||||
|
{
|
||||||
|
// If removed container is currently selected, then change selected to previous
|
||||||
|
// This will default to 0 in any edge cases
|
||||||
|
if self.containers.state.selected().is_some() {
|
||||||
|
self.containers.previous();
|
||||||
|
}
|
||||||
|
self.containers.items.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in containers.iter() {
|
||||||
|
let id = i.id.as_ref().unwrap().to_owned();
|
||||||
|
let mut name = i
|
||||||
|
.names
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&vec!["".to_owned()])
|
||||||
|
.get(0)
|
||||||
|
.unwrap()
|
||||||
|
.to_owned();
|
||||||
|
if let Some(c) = name.chars().next() {
|
||||||
|
if c == '/' {
|
||||||
|
name.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = State::from(i.state.as_ref().unwrap_or(&"dead".to_owned()).trim());
|
||||||
|
let status = i
|
||||||
|
.status
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"".to_owned())
|
||||||
|
.trim()
|
||||||
|
.to_owned();
|
||||||
|
let image = i.image.as_ref().unwrap_or(&"".to_owned()).trim().to_owned();
|
||||||
|
if let Some(current_container) = self.get_container_by_id(&id) {
|
||||||
|
if current_container.name != name {
|
||||||
|
current_container.name = name
|
||||||
|
};
|
||||||
|
if current_container.status != status {
|
||||||
|
current_container.status = status
|
||||||
|
};
|
||||||
|
if current_container.state != state {
|
||||||
|
current_container.docker_controls.items = DockerControls::gen_vec(&state);
|
||||||
|
|
||||||
|
// Update the list state, needs to be None if the gen_vec returns an empty vec
|
||||||
|
match state {
|
||||||
|
State::Removing | State::Restarting | State::Unknown => {
|
||||||
|
current_container.docker_controls.state.select(None)
|
||||||
|
}
|
||||||
|
_ => current_container.docker_controls.start(),
|
||||||
|
};
|
||||||
|
current_container.state = state;
|
||||||
|
};
|
||||||
|
if current_container.image != image {
|
||||||
|
current_container.image = image
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let mut container = ContainerItem::new(id, status, image, state, name);
|
||||||
|
container.logs.end();
|
||||||
|
self.containers.items.push(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update logs of a given container, based on index not id
|
||||||
|
pub fn update_log_by_index(&mut self, output: Vec<String>, index: usize) {
|
||||||
|
let tz = self.get_systemtime();
|
||||||
|
if let Some(container) = self.containers.items.get_mut(index) {
|
||||||
|
container.last_updated = tz;
|
||||||
|
let current_len = container.logs.items.len();
|
||||||
|
output.iter().for_each(|i| {
|
||||||
|
let lines = if self.args.color {
|
||||||
|
log_sanitizer::colorize_logs(i.to_owned())
|
||||||
|
} else if self.args.raw {
|
||||||
|
log_sanitizer::raw(i.to_owned())
|
||||||
|
} else {
|
||||||
|
log_sanitizer::remove_ansi(i.to_owned())
|
||||||
|
};
|
||||||
|
container.logs.items.push(ListItem::new(lines));
|
||||||
|
});
|
||||||
|
if container.logs.state.selected().is_none()
|
||||||
|
|| container.logs.state.selected().unwrap() + 1 == current_len
|
||||||
|
{
|
||||||
|
container.logs.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.logs_parsed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn update_all_logs(&mut self, all_logs: Vec<Vec<String>>) {
|
||||||
|
for (index, output) in all_logs.into_iter().enumerate() {
|
||||||
|
self.update_log_by_index(output, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use core::fmt;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::app_data::DockerControls;
|
||||||
|
|
||||||
|
/// app errors to set in global state
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum AppError {
|
||||||
|
DockerConnect,
|
||||||
|
DockerInterval,
|
||||||
|
InputPoll,
|
||||||
|
DockerCommand(DockerControls),
|
||||||
|
Terminal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
/// for handling errors from terminal
|
||||||
|
pub fn disp(&self) {
|
||||||
|
match self {
|
||||||
|
Self::DockerConnect => error!("Unable to access docker daemon"),
|
||||||
|
Self::DockerInterval => error!("Docker update interval needs to be greater than 0"),
|
||||||
|
Self::InputPoll => error!("Unable to poll user input"),
|
||||||
|
Self::Terminal => error!("Unable to draw to terminal"),
|
||||||
|
Self::DockerCommand(s) => {
|
||||||
|
let error = format!("Unable to {} container", s);
|
||||||
|
error!(%error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert errors into strings to display
|
||||||
|
impl fmt::Display for AppError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let disp = match self {
|
||||||
|
Self::DockerConnect => "Unable to access docker daemon".to_owned(),
|
||||||
|
Self::DockerInterval => "Docker update interval needs to be greater than 0".to_owned(),
|
||||||
|
Self::InputPoll => "Unable to poll user input".to_owned(),
|
||||||
|
Self::Terminal => "Unable to draw to terminal".to_owned(),
|
||||||
|
Self::DockerCommand(s) => format!("Unable to {} container", s),
|
||||||
|
};
|
||||||
|
write!(f, "{}", disp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
use bollard::{
|
||||||
|
container::{ListContainersOptions, LogsOptions, Stats, StatsOptions},
|
||||||
|
Docker,
|
||||||
|
};
|
||||||
|
use futures_util::{future::join_all, StreamExt};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{app_data::AppData, parse_args::CliArgs, ui::GuiState};
|
||||||
|
|
||||||
|
pub struct DockerData {
|
||||||
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
docker: Arc<Docker>,
|
||||||
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
initialised: bool,
|
||||||
|
sleep_duration: Duration,
|
||||||
|
timestamps: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DockerData {
|
||||||
|
/// Use docker stats for work out current cpu usage
|
||||||
|
fn calculate_usage(stats: &Stats) -> f64 {
|
||||||
|
let mut cpu_percentage = 0.0;
|
||||||
|
let previous_cpu = stats.precpu_stats.cpu_usage.total_usage;
|
||||||
|
let cpu_delta = stats.cpu_stats.cpu_usage.total_usage as f64 - previous_cpu as f64;
|
||||||
|
if stats.cpu_stats.system_cpu_usage.is_some()
|
||||||
|
&& stats.precpu_stats.system_cpu_usage.is_some()
|
||||||
|
{
|
||||||
|
let system_delta = (stats.cpu_stats.system_cpu_usage.unwrap()
|
||||||
|
- stats.precpu_stats.system_cpu_usage.unwrap())
|
||||||
|
as f64;
|
||||||
|
let online_cpus = stats.cpu_stats.online_cpus.unwrap_or_else(|| {
|
||||||
|
stats
|
||||||
|
.cpu_stats
|
||||||
|
.cpu_usage
|
||||||
|
.percpu_usage
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.len() as u64
|
||||||
|
}) as f64;
|
||||||
|
if system_delta > 0.0 && cpu_delta > 0.0 {
|
||||||
|
cpu_percentage = (cpu_delta / system_delta) * online_cpus * 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cpu_percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single docker stat in order to update mem and cpu usage
|
||||||
|
/// don't take &self, so that can tokio::spawn into it's on thread
|
||||||
|
async fn update_container_stat(
|
||||||
|
docker: Arc<Docker>,
|
||||||
|
id: String,
|
||||||
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
is_running: bool,
|
||||||
|
) {
|
||||||
|
let mut stream = docker
|
||||||
|
.stats(
|
||||||
|
&id,
|
||||||
|
Some(StatsOptions {
|
||||||
|
stream: false,
|
||||||
|
one_shot: !is_running,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.take(1);
|
||||||
|
|
||||||
|
while let Some(Ok(stats)) = stream.next().await {
|
||||||
|
let mem_stat = stats.memory_stats.usage.unwrap_or(0);
|
||||||
|
let mem_limit = stats.memory_stats.limit.unwrap_or(0);
|
||||||
|
|
||||||
|
let key = if let Some(networks) = &stats.networks {
|
||||||
|
networks.keys().next().map(|x| x.to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let cpu_stats = Self::calculate_usage(&stats);
|
||||||
|
|
||||||
|
let (rx, tx) = if let Some(k) = key {
|
||||||
|
let ii = stats.networks.unwrap();
|
||||||
|
let v = ii.get(&k).unwrap();
|
||||||
|
(v.rx_bytes.to_owned(), v.tx_bytes.to_owned())
|
||||||
|
} else {
|
||||||
|
(0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_running {
|
||||||
|
app_data.lock().update_stats(
|
||||||
|
id.clone(),
|
||||||
|
Some(cpu_stats),
|
||||||
|
Some(mem_stat),
|
||||||
|
mem_limit,
|
||||||
|
rx,
|
||||||
|
tx,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
app_data
|
||||||
|
.lock()
|
||||||
|
.update_stats(id.clone(), None, None, mem_limit, rx, tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update all stats, spawn each container into own tokio::spawn thread
|
||||||
|
async fn update_all_container_stats(&mut self, all_ids: &[(bool, String)]) {
|
||||||
|
for (is_running, id) in all_ids.iter() {
|
||||||
|
let docker = Arc::clone(&self.docker);
|
||||||
|
let app_data = Arc::clone(&self.app_data);
|
||||||
|
let is_running = *is_running;
|
||||||
|
let id = id.to_owned();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
Self::update_container_stat(docker, id, app_data, is_running).await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here
|
||||||
|
/// Just make sure that items sent are guaranteed to have an id
|
||||||
|
pub async fn update_all_containers(&mut self) -> Vec<(bool, String)> {
|
||||||
|
let containers = self
|
||||||
|
.docker
|
||||||
|
.list_containers(Some(ListContainersOptions::<String> {
|
||||||
|
all: true,
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
// iter over containers, to only send ones which have an id, as use ID for extensivley!
|
||||||
|
// alternative is to create my own container struct, and will out with details
|
||||||
|
containers.iter().filter(|i| i.id.is_some()).for_each(|c| {
|
||||||
|
output.push(c.to_owned());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.app_data.lock().update_containers(&output);
|
||||||
|
output
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
i.state.as_ref().unwrap() == "running",
|
||||||
|
i.id.as_ref().unwrap().to_owned(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update single container logs
|
||||||
|
/// don't take &self, so that can tokio::spawn into it's on thread
|
||||||
|
async fn update_log(
|
||||||
|
docker: Arc<Docker>,
|
||||||
|
id: String,
|
||||||
|
timestamps: bool,
|
||||||
|
since: i64,
|
||||||
|
) -> Vec<String> {
|
||||||
|
let options = Some(LogsOptions::<String> {
|
||||||
|
stdout: true,
|
||||||
|
timestamps,
|
||||||
|
since,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut logs = docker.logs(&id, options);
|
||||||
|
|
||||||
|
let mut output = vec![];
|
||||||
|
|
||||||
|
while let Some(value) = logs.next().await {
|
||||||
|
if let Ok(data) = value {
|
||||||
|
let log_string = data.to_string();
|
||||||
|
if !log_string.trim().is_empty() {
|
||||||
|
output.push(log_string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update all logs, spawn each container into own tokio::spawn thread
|
||||||
|
// rename init all logs, as only gets run once
|
||||||
|
async fn update_all_logs(&mut self, all_ids: &[(bool, String)]) {
|
||||||
|
let mut handles = vec![];
|
||||||
|
|
||||||
|
for (_, id) in all_ids.iter() {
|
||||||
|
let docker = Arc::clone(&self.docker);
|
||||||
|
let timestamps = self.timestamps;
|
||||||
|
let id = id.to_owned();
|
||||||
|
handles.push(Self::update_log(docker, id, timestamps, 0));
|
||||||
|
}
|
||||||
|
let all_logs = join_all(handles).await;
|
||||||
|
self.app_data.lock().update_all_logs(all_logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_everything(&mut self) {
|
||||||
|
let all_ids = self.update_all_containers().await;
|
||||||
|
let op_index = self.app_data.lock().get_selected_log_index();
|
||||||
|
if let Some(index) = op_index {
|
||||||
|
let docker = Arc::clone(&self.docker);
|
||||||
|
let since = self.app_data.lock().containers.items[index].last_updated as i64;
|
||||||
|
let timestamps = self.timestamps;
|
||||||
|
let id = self.app_data.lock().containers.items[index].id.to_owned();
|
||||||
|
let logs = Self::update_log(docker, id, timestamps, since).await;
|
||||||
|
self.app_data.lock().update_log_by_index(logs, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.update_all_container_stats(&all_ids).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialise self, and start the updated loop
|
||||||
|
pub async fn init(
|
||||||
|
args: CliArgs,
|
||||||
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
docker: Arc<Docker>,
|
||||||
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
) {
|
||||||
|
if app_data.lock().get_error().is_none() {
|
||||||
|
let mut inner = Self {
|
||||||
|
app_data,
|
||||||
|
docker,
|
||||||
|
gui_state,
|
||||||
|
initialised: false,
|
||||||
|
sleep_duration: Duration::from_millis(args.docker as u64),
|
||||||
|
timestamps: args.timestamp,
|
||||||
|
};
|
||||||
|
inner.initialise_container_data().await;
|
||||||
|
inner.update_loop().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialise_container_data(&mut self) {
|
||||||
|
let gui_state = Arc::clone(&self.gui_state);
|
||||||
|
// could also just loop while init is false, would need to move an arc mutex into here
|
||||||
|
// so instead just abort at end of function
|
||||||
|
let loading_spin = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
gui_state.lock().next_loading();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let all_ids = self.update_all_containers().await;
|
||||||
|
self.update_all_container_stats(&all_ids).await;
|
||||||
|
|
||||||
|
// Maybe only do a single one at first?
|
||||||
|
self.update_all_logs(&all_ids).await;
|
||||||
|
|
||||||
|
if all_ids.is_empty() {
|
||||||
|
self.initialised = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until all logs have initialised
|
||||||
|
while !self.initialised {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
self.initialised = self.app_data.lock().initialised(&all_ids);
|
||||||
|
}
|
||||||
|
self.app_data.lock().init = true;
|
||||||
|
loading_spin.abort();
|
||||||
|
self.gui_state.lock().reset_loading();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update all items, wait until all complete
|
||||||
|
/// sleep for CliArgs.docker ms before updating next
|
||||||
|
async fn update_loop(&mut self) {
|
||||||
|
loop {
|
||||||
|
let start = Instant::now();
|
||||||
|
self.update_everything().await;
|
||||||
|
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
if elapsed < self.sleep_duration {
|
||||||
|
tokio::time::sleep(self.sleep_duration - elapsed).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
use crossterm::event::{KeyCode, MouseEvent};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum InputMessages {
|
||||||
|
ButtonPress(KeyCode),
|
||||||
|
MouseEvent(MouseEvent),
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bollard::{container::StartContainerOptions, Docker};
|
||||||
|
use crossterm::event::{KeyCode, MouseButton, MouseEvent, MouseEventKind};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use tokio::sync::broadcast::Receiver;
|
||||||
|
use tui::layout::Rect;
|
||||||
|
|
||||||
|
mod message;
|
||||||
|
use crate::{
|
||||||
|
app_data::{AppData, DockerControls},
|
||||||
|
app_error::AppError,
|
||||||
|
ui::{GuiState, SelectablePanel},
|
||||||
|
};
|
||||||
|
pub use message::InputMessages;
|
||||||
|
|
||||||
|
/// Handle all input events
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InputHandler {
|
||||||
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
docker: Arc<Docker>,
|
||||||
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
is_running: Arc<AtomicBool>,
|
||||||
|
rec: Receiver<InputMessages>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputHandler {
|
||||||
|
/// Initialize self, and running the message handling loop
|
||||||
|
pub async fn init(
|
||||||
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
rec: Receiver<InputMessages>,
|
||||||
|
docker: Arc<Docker>,
|
||||||
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
is_running: Arc<AtomicBool>,
|
||||||
|
) {
|
||||||
|
let mut inner = Self {
|
||||||
|
app_data,
|
||||||
|
docker,
|
||||||
|
gui_state,
|
||||||
|
is_running,
|
||||||
|
rec,
|
||||||
|
};
|
||||||
|
inner.start().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// check for incoming messages
|
||||||
|
async fn start(&mut self) {
|
||||||
|
while let Ok(message) = self.rec.recv().await {
|
||||||
|
match message {
|
||||||
|
InputMessages::ButtonPress(key_code) => self.button_press(key_code).await,
|
||||||
|
InputMessages::MouseEvent(mouse_event) => {
|
||||||
|
let show_error = self.app_data.lock().show_error;
|
||||||
|
let show_info = self.gui_state.lock().show_help;
|
||||||
|
if !show_error && !show_info {
|
||||||
|
self.mouse_press(mouse_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.is_running.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle any keyboard button events
|
||||||
|
async fn button_press(&mut self, key_code: KeyCode) {
|
||||||
|
let show_error = self.app_data.lock().show_error;
|
||||||
|
let show_info = self.gui_state.lock().show_help;
|
||||||
|
if show_error {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
self.is_running.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
KeyCode::Char('c') => {
|
||||||
|
self.app_data.lock().show_error = false;
|
||||||
|
self.app_data.lock().remove_error();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
} else if show_info {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
self.is_running.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
KeyCode::Char('h') => {
|
||||||
|
self.gui_state.lock().show_help = false;
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
self.is_running.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
KeyCode::Char('h') => {
|
||||||
|
self.gui_state.lock().show_help = true;
|
||||||
|
}
|
||||||
|
KeyCode::Tab => self.gui_state.lock().next_panel(),
|
||||||
|
KeyCode::BackTab => self.gui_state.lock().previous_panel(),
|
||||||
|
KeyCode::Home => {
|
||||||
|
let mut locked_data = self.app_data.lock();
|
||||||
|
match self.gui_state.lock().selected_panel {
|
||||||
|
SelectablePanel::Containers => locked_data.containers.start(),
|
||||||
|
SelectablePanel::Logs => locked_data.log_start(),
|
||||||
|
SelectablePanel::Commands => locked_data.docker_command_start(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::End => {
|
||||||
|
let mut locked_data = self.app_data.lock();
|
||||||
|
match self.gui_state.lock().selected_panel {
|
||||||
|
SelectablePanel::Containers => locked_data.containers.end(),
|
||||||
|
SelectablePanel::Logs => locked_data.log_end(),
|
||||||
|
SelectablePanel::Commands => locked_data.docker_command_end(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Up => self.previous(),
|
||||||
|
KeyCode::PageUp => {
|
||||||
|
for _ in 0..=6 {
|
||||||
|
self.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => self.next(),
|
||||||
|
KeyCode::PageDown => {
|
||||||
|
for _ in 0..=6 {
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
// Does is matter though?
|
||||||
|
// This isn't great, just means you can't send docker commands before full initialization of the program
|
||||||
|
// could change to to if loading = true, although at the moment don't have a loading bool
|
||||||
|
let panel = self.gui_state.lock().selected_panel;
|
||||||
|
if panel == SelectablePanel::Commands {
|
||||||
|
let command = self.app_data.lock().get_docker_command();
|
||||||
|
|
||||||
|
if command.is_some() {
|
||||||
|
let id = self.app_data.lock().get_selected_container_id();
|
||||||
|
let app_data = Arc::clone(&self.app_data);
|
||||||
|
let docker = Arc::clone(&self.docker);
|
||||||
|
if id.is_some() {
|
||||||
|
let id = id.unwrap();
|
||||||
|
match command.unwrap() {
|
||||||
|
DockerControls::Pause => {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
docker.pause_container(&id).await.unwrap_or_else(
|
||||||
|
|_| {
|
||||||
|
app_data.lock().set_error(
|
||||||
|
AppError::DockerCommand(
|
||||||
|
DockerControls::Pause,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DockerControls::Unpause => {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
docker.unpause_container(&id).await.unwrap_or_else(
|
||||||
|
|_| {
|
||||||
|
app_data.lock().set_error(
|
||||||
|
AppError::DockerCommand(
|
||||||
|
DockerControls::Unpause,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DockerControls::Start => {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
docker
|
||||||
|
.start_container(
|
||||||
|
&id,
|
||||||
|
None::<StartContainerOptions<String>>,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
app_data.lock().set_error(
|
||||||
|
AppError::DockerCommand(
|
||||||
|
DockerControls::Start,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DockerControls::Stop => {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
docker.stop_container(&id, None).await.unwrap_or_else(
|
||||||
|
|_| {
|
||||||
|
app_data.lock().set_error(
|
||||||
|
AppError::DockerCommand(
|
||||||
|
DockerControls::Stop,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
DockerControls::Restart => {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
docker
|
||||||
|
.restart_container(&id, None)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
app_data.lock().set_error(
|
||||||
|
AppError::DockerCommand(
|
||||||
|
DockerControls::Restart,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle mouse button events
|
||||||
|
fn mouse_press(&mut self, mouse_event: MouseEvent) {
|
||||||
|
match mouse_event.kind {
|
||||||
|
MouseEventKind::ScrollUp => self.previous(),
|
||||||
|
MouseEventKind::ScrollDown => self.next(),
|
||||||
|
MouseEventKind::Down(MouseButton::Left) => {
|
||||||
|
self.gui_state.lock().rect_insersects(Rect::new(
|
||||||
|
mouse_event.column,
|
||||||
|
mouse_event.row,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change state of selected container
|
||||||
|
fn next(&mut self) {
|
||||||
|
let mut locked_data = self.app_data.lock();
|
||||||
|
match self.gui_state.lock().selected_panel {
|
||||||
|
SelectablePanel::Containers => locked_data.containers.next(),
|
||||||
|
SelectablePanel::Logs => locked_data.log_next(),
|
||||||
|
SelectablePanel::Commands => locked_data.docker_command_next(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change state of selected container
|
||||||
|
fn previous(&mut self) {
|
||||||
|
let mut locked_data = self.app_data.lock();
|
||||||
|
match self.gui_state.lock().selected_panel {
|
||||||
|
SelectablePanel::Containers => locked_data.containers.previous(),
|
||||||
|
SelectablePanel::Logs => locked_data.log_previous(),
|
||||||
|
SelectablePanel::Commands => locked_data.docker_command_previous(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
use app_data::AppData;
|
||||||
|
use app_error::AppError;
|
||||||
|
use bollard::Docker;
|
||||||
|
use docker_data::DockerData;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use parse_args::CliArgs;
|
||||||
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
|
use tracing::{info, Level};
|
||||||
|
|
||||||
|
mod app_data;
|
||||||
|
mod app_error;
|
||||||
|
mod docker_data;
|
||||||
|
mod input_handler;
|
||||||
|
mod parse_args;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
use ui::{create_ui, GuiState};
|
||||||
|
|
||||||
|
fn setup_tracing() {
|
||||||
|
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
setup_tracing();
|
||||||
|
let args = CliArgs::new();
|
||||||
|
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
||||||
|
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
||||||
|
|
||||||
|
let docker_args = args.clone();
|
||||||
|
let docker_app_data = Arc::clone(&app_data);
|
||||||
|
let docker_gui_state = Arc::clone(&gui_state);
|
||||||
|
|
||||||
|
// Create docker daemon handler, and only spawn up the docker data handler if ping returns non-error
|
||||||
|
let docker = Arc::new(Docker::connect_with_socket_defaults().unwrap());
|
||||||
|
match docker.ping().await {
|
||||||
|
Ok(_) => {
|
||||||
|
let docker = Arc::clone(&docker);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
DockerData::init(docker_args, docker_app_data, docker, docker_gui_state).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(_) => app_data.lock().set_error(AppError::DockerConnect),
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_app_data = Arc::clone(&app_data);
|
||||||
|
|
||||||
|
let (s, r) = tokio::sync::broadcast::channel(16);
|
||||||
|
|
||||||
|
let input_docker = Arc::clone(&docker);
|
||||||
|
let is_running = Arc::new(AtomicBool::new(true));
|
||||||
|
let input_is_running = Arc::clone(&is_running);
|
||||||
|
let input_gui_state = Arc::clone(&gui_state);
|
||||||
|
|
||||||
|
// Spawn input handling into own tokio thread
|
||||||
|
tokio::spawn(async {
|
||||||
|
input_handler::InputHandler::init(
|
||||||
|
input_app_data,
|
||||||
|
r,
|
||||||
|
input_docker,
|
||||||
|
input_gui_state,
|
||||||
|
input_is_running,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Debug mode for testing, mostly pointless, doesn't take terminal nor draw gui
|
||||||
|
if !args.gui {
|
||||||
|
loop {
|
||||||
|
info!("in debug mode");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
create_ui(app_data, s, is_running, gui_state).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use std::process;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
#[clap(about, version, author)]
|
||||||
|
|
||||||
|
pub struct CliArgs {
|
||||||
|
/// Docker update interval in ms, minimum 1, reccomended 500+
|
||||||
|
#[clap(short = 'd', default_value_t = 1000)]
|
||||||
|
pub docker: u32,
|
||||||
|
|
||||||
|
/// Don't draw gui - for debugging - mostly pointless
|
||||||
|
#[clap(short = 'g')]
|
||||||
|
pub gui: bool,
|
||||||
|
|
||||||
|
/// Remove timestamps from Docker logs
|
||||||
|
#[clap(short = 't')]
|
||||||
|
pub timestamp: bool,
|
||||||
|
|
||||||
|
/// Show raw logs, default is to remove ansi formatting
|
||||||
|
#[clap(short = 'r', conflicts_with = "color")]
|
||||||
|
pub raw: bool,
|
||||||
|
|
||||||
|
/// Attempt to colorize the logs
|
||||||
|
#[clap(short = 'c', conflicts_with = "raw")]
|
||||||
|
pub color: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliArgs {
|
||||||
|
/// Parse cli arguments
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let args = CliArgs::parse();
|
||||||
|
|
||||||
|
// Quit the program if the docker update argument is 0
|
||||||
|
// Should maybe change it to check if less than 100
|
||||||
|
if args.docker == 0 {
|
||||||
|
error!("docker args needs to be greater than 0");
|
||||||
|
process::exit(1)
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
color: args.color,
|
||||||
|
docker: args.docker,
|
||||||
|
gui: !args.gui,
|
||||||
|
raw: args.raw,
|
||||||
|
timestamp: !args.timestamp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
pub mod log_sanitizer {
|
||||||
|
|
||||||
|
use cansi::{categorise_text, Color as CansiColor, Intensity};
|
||||||
|
use tui::{
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
text::{Span, Spans},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Attempt to colorize the given string to tui-rs standars
|
||||||
|
pub fn colorize_logs(input: String) -> Vec<Spans<'static>> {
|
||||||
|
vec![Spans::from(
|
||||||
|
categorise_text(&input)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| {
|
||||||
|
let fg_color = color_ansi_to_tui(i.fg_colour);
|
||||||
|
let bg_color = color_ansi_to_tui(i.bg_colour);
|
||||||
|
let style = Style::default().bg(bg_color).fg(fg_color);
|
||||||
|
if i.blink {
|
||||||
|
style.add_modifier(Modifier::SLOW_BLINK);
|
||||||
|
}
|
||||||
|
if i.underline {
|
||||||
|
style.add_modifier(Modifier::UNDERLINED);
|
||||||
|
}
|
||||||
|
if i.reversed {
|
||||||
|
style.add_modifier(Modifier::REVERSED);
|
||||||
|
}
|
||||||
|
if i.intensity == Intensity::Bold {
|
||||||
|
style.add_modifier(Modifier::BOLD);
|
||||||
|
}
|
||||||
|
if i.hidden {
|
||||||
|
style.add_modifier(Modifier::HIDDEN);
|
||||||
|
}
|
||||||
|
if i.strikethrough {
|
||||||
|
style.add_modifier(Modifier::CROSSED_OUT);
|
||||||
|
}
|
||||||
|
Span::styled(i.text.to_owned(), style)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove all ansi formatting from a given string and create tui-rs spans
|
||||||
|
pub fn remove_ansi(input: String) -> Vec<Spans<'static>> {
|
||||||
|
let mut output = String::from("");
|
||||||
|
for i in categorise_text(&input) {
|
||||||
|
output.push_str(i.text)
|
||||||
|
}
|
||||||
|
raw(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create tui-rs spans that exactly match the given strings
|
||||||
|
pub fn raw(input: String) -> Vec<Spans<'static>> {
|
||||||
|
vec![Spans::from(Span::raw(input))]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change from ansi to tui colors
|
||||||
|
fn color_ansi_to_tui(color: CansiColor) -> Color {
|
||||||
|
match color {
|
||||||
|
CansiColor::Black => Color::Black,
|
||||||
|
CansiColor::Red => Color::Red,
|
||||||
|
CansiColor::Green => Color::Green,
|
||||||
|
CansiColor::Yellow => Color::Yellow,
|
||||||
|
CansiColor::Blue => Color::Blue,
|
||||||
|
CansiColor::Magenta => Color::Magenta,
|
||||||
|
CansiColor::Cyan => Color::Cyan,
|
||||||
|
CansiColor::White => Color::White,
|
||||||
|
CansiColor::BrightBlack => Color::Black,
|
||||||
|
CansiColor::BrightRed => Color::LightRed,
|
||||||
|
CansiColor::BrightGreen => Color::LightGreen,
|
||||||
|
CansiColor::BrightYellow => Color::LightYellow,
|
||||||
|
CansiColor::BrightBlue => Color::LightBlue,
|
||||||
|
CansiColor::BrightMagenta => Color::LightMagenta,
|
||||||
|
CansiColor::BrightCyan => Color::LightCyan,
|
||||||
|
CansiColor::BrightWhite => Color::White,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::default::Default;
|
||||||
|
use std::{fmt::Display, sync::Arc};
|
||||||
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
symbols,
|
||||||
|
text::{Span, Spans},
|
||||||
|
widgets::{
|
||||||
|
Axis, Block, BorderType, Borders, Chart, Clear, Dataset, GraphType, List, ListItem,
|
||||||
|
Paragraph,
|
||||||
|
},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
||||||
|
app_error::AppError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{GuiState, SelectablePanel};
|
||||||
|
|
||||||
|
const NAME_TEXT: &str = r#"
|
||||||
|
88
|
||||||
|
88
|
||||||
|
88
|
||||||
|
,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba,
|
||||||
|
a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8
|
||||||
|
8b d8 )888( 8888[ 8PP""""""" 88
|
||||||
|
"8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88
|
||||||
|
`"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 "#;
|
||||||
|
|
||||||
|
const NAME: &str = env!("CARGO_PKG_NAME");
|
||||||
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||||
|
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||||
|
const MARGIN: &str = " ";
|
||||||
|
|
||||||
|
/// Generate block, add a bored if is the selected panel,
|
||||||
|
/// add custom title based on state of each panel
|
||||||
|
fn generate_block<'a>(
|
||||||
|
selectable_panel: Option<SelectablePanel>,
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
selected_panel: &SelectablePanel,
|
||||||
|
) -> Block<'a> {
|
||||||
|
let mut block = Block::default().borders(Borders::ALL);
|
||||||
|
|
||||||
|
if let Some(panel) = selectable_panel {
|
||||||
|
let title = match panel {
|
||||||
|
SelectablePanel::Containers => {
|
||||||
|
format!(
|
||||||
|
" {} {} ",
|
||||||
|
panel.title(),
|
||||||
|
app_data.lock().containers.get_state_title()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SelectablePanel::Logs => {
|
||||||
|
format!(" {} {} ", panel.title(), app_data.lock().get_log_title())
|
||||||
|
}
|
||||||
|
_ => String::from(""),
|
||||||
|
};
|
||||||
|
block = block.title(title);
|
||||||
|
if selected_panel == &panel {
|
||||||
|
let selected_style = Style::default().fg(Color::LightCyan);
|
||||||
|
let selected_border = BorderType::Plain;
|
||||||
|
block = block
|
||||||
|
.border_style(selected_style)
|
||||||
|
.border_type(selected_border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the selectable panels
|
||||||
|
pub fn draw_commands<B: Backend>(
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
area: Rect,
|
||||||
|
f: &mut Frame<'_, B>,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
index: Option<usize>,
|
||||||
|
selected_panel: &SelectablePanel,
|
||||||
|
) {
|
||||||
|
let panel = SelectablePanel::Commands;
|
||||||
|
let block = generate_block(Some(panel), app_data, selected_panel);
|
||||||
|
|
||||||
|
gui_state.lock().insert_into_area_map(panel, area);
|
||||||
|
|
||||||
|
if let Some(i) = index {
|
||||||
|
let items = app_data.lock().containers.items[i]
|
||||||
|
.docker_controls
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
let lines = Spans::from(vec![Span::styled(
|
||||||
|
i.to_string(),
|
||||||
|
Style::default().fg(i.get_color()),
|
||||||
|
)]);
|
||||||
|
ListItem::new(lines)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let items = List::new(items)
|
||||||
|
.block(block)
|
||||||
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||||
|
.highlight_symbol("▶ ");
|
||||||
|
|
||||||
|
f.render_stateful_widget(
|
||||||
|
items,
|
||||||
|
area,
|
||||||
|
&mut app_data.lock().containers.items[i].docker_controls.state,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let debug_text = String::from("");
|
||||||
|
let paragraph = Paragraph::new(debug_text)
|
||||||
|
.block(block)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
f.render_widget(paragraph, area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the selectable panels
|
||||||
|
pub fn draw_containers<B: Backend>(
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
area: Rect,
|
||||||
|
f: &mut Frame<'_, B>,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
selected_panel: &SelectablePanel,
|
||||||
|
widths: &Columns,
|
||||||
|
) {
|
||||||
|
let panel = SelectablePanel::Containers;
|
||||||
|
let block = generate_block(Some(panel), app_data, selected_panel);
|
||||||
|
|
||||||
|
gui_state.lock().insert_into_area_map(panel, area);
|
||||||
|
|
||||||
|
let items = app_data
|
||||||
|
.lock()
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
let state_style = Style::default().fg(i.state.get_color());
|
||||||
|
let blue = Style::default().fg(Color::Blue);
|
||||||
|
|
||||||
|
let mems = format!(
|
||||||
|
"{:>1} / {:>1}",
|
||||||
|
i.mem_stats.back().unwrap_or(&ByteStats::new(0)),
|
||||||
|
i.mem_limit
|
||||||
|
);
|
||||||
|
|
||||||
|
let lines = Spans::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("{:<width$}", i.state.to_string(), width = widths.state.1),
|
||||||
|
state_style,
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}{:>width$}", MARGIN, i.status, width = widths.status.1),
|
||||||
|
state_style,
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!(
|
||||||
|
"{}{:>width$}",
|
||||||
|
MARGIN,
|
||||||
|
i.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)),
|
||||||
|
width = widths.cpu.1
|
||||||
|
),
|
||||||
|
state_style,
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}{:>width$}", MARGIN, mems, width = widths.mem.1),
|
||||||
|
state_style,
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}{:>width$}", MARGIN, i.name, width = widths.name.1),
|
||||||
|
blue,
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}{:>width$}", MARGIN, i.image, width = widths.image.1),
|
||||||
|
blue,
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}{:>width$}", MARGIN, i.net_rx, width = widths.net_rx.1),
|
||||||
|
Style::default().fg(Color::Rgb(255, 233, 193)),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}{:>width$}", MARGIN, i.net_tx, width = widths.net_tx.1),
|
||||||
|
Style::default().fg(Color::Rgb(205, 140, 140)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
ListItem::new(lines)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if items.is_empty() {
|
||||||
|
let debug_text = String::from("no containers running");
|
||||||
|
let paragraph = Paragraph::new(debug_text)
|
||||||
|
.block(block)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
f.render_widget(paragraph, area)
|
||||||
|
} else {
|
||||||
|
let items = List::new(items)
|
||||||
|
.block(block)
|
||||||
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||||
|
.highlight_symbol("⚪ ");
|
||||||
|
|
||||||
|
f.render_stateful_widget(items, area, &mut app_data.lock().containers.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the selectable panels
|
||||||
|
pub fn draw_logs<B: Backend>(
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
area: Rect,
|
||||||
|
f: &mut Frame<'_, B>,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
index: Option<usize>,
|
||||||
|
selected_panel: &SelectablePanel,
|
||||||
|
) {
|
||||||
|
let panel = SelectablePanel::Logs;
|
||||||
|
|
||||||
|
gui_state.lock().insert_into_area_map(panel, area);
|
||||||
|
|
||||||
|
let block = generate_block(Some(panel), app_data, selected_panel);
|
||||||
|
|
||||||
|
let init = app_data.lock().init;
|
||||||
|
if !init {
|
||||||
|
let icon = gui_state.lock().get_loading();
|
||||||
|
let parsing_logs = format!("parsing logs {}", icon);
|
||||||
|
let paragraph = Paragraph::new(parsing_logs)
|
||||||
|
.style(Style::default())
|
||||||
|
.block(block)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
f.render_widget(paragraph, area)
|
||||||
|
|
||||||
|
} else if let Some(index) = index {
|
||||||
|
let items = app_data.lock().containers.items[index]
|
||||||
|
.logs
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|i| i.1.to_owned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let items = List::new(items)
|
||||||
|
.block(block)
|
||||||
|
.highlight_symbol("▶ ")
|
||||||
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||||
|
f.render_stateful_widget(
|
||||||
|
items,
|
||||||
|
area,
|
||||||
|
&mut app_data.lock().containers.items[index].logs.state,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let debug_text = String::from("no logs found");
|
||||||
|
let paragraph = Paragraph::new(debug_text)
|
||||||
|
.block(block)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
f.render_widget(paragraph, area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the cpu + mem charts
|
||||||
|
pub fn draw_chart<B: Backend>(
|
||||||
|
f: &mut Frame<'_, B>,
|
||||||
|
area: Rect,
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
index: Option<usize>,
|
||||||
|
) {
|
||||||
|
if let Some(index) = index {
|
||||||
|
let area = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
|
.split(area);
|
||||||
|
let (cpu, mem) = app_data.lock().containers.items[index].get_chart_data();
|
||||||
|
|
||||||
|
let cpu_dataset = vec![Dataset::default()
|
||||||
|
.marker(symbols::Marker::Dot)
|
||||||
|
.style(Style::default().fg(Color::Magenta))
|
||||||
|
.graph_type(GraphType::Line)
|
||||||
|
.data(&cpu.0)];
|
||||||
|
|
||||||
|
let mem_dataset = vec![Dataset::default()
|
||||||
|
.marker(symbols::Marker::Dot)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.graph_type(GraphType::Line)
|
||||||
|
.data(&mem.0)];
|
||||||
|
let cpu_chart = make_chart(
|
||||||
|
cpu.2,
|
||||||
|
String::from("cpu"),
|
||||||
|
cpu_dataset,
|
||||||
|
CpuStats::new(cpu.0.last().unwrap_or(&(0.00, 0.00)).1),
|
||||||
|
cpu.1,
|
||||||
|
);
|
||||||
|
let mem_chart = make_chart(
|
||||||
|
mem.2,
|
||||||
|
String::from("memory"),
|
||||||
|
mem_dataset,
|
||||||
|
ByteStats::new(mem.0.last().unwrap_or(&(0.0, 0.0)).1 as u64),
|
||||||
|
mem.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
f.render_widget(cpu_chart, area[0]);
|
||||||
|
f.render_widget(mem_chart, area[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create charts
|
||||||
|
fn make_chart<T: Stats + Display>(
|
||||||
|
state: State,
|
||||||
|
name: String,
|
||||||
|
dataset: Vec<Dataset>,
|
||||||
|
current: T,
|
||||||
|
max: T,
|
||||||
|
) -> Chart {
|
||||||
|
let title_color = match state {
|
||||||
|
State::Running => Color::Green,
|
||||||
|
_ => state.get_color(),
|
||||||
|
};
|
||||||
|
let label_color = match state {
|
||||||
|
State::Running => ORANGE,
|
||||||
|
_ => state.get_color(),
|
||||||
|
};
|
||||||
|
Chart::new(dataset)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.title(Span::styled(
|
||||||
|
format!(" {} {} ", name, current),
|
||||||
|
Style::default()
|
||||||
|
.fg(title_color)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Plain),
|
||||||
|
)
|
||||||
|
.x_axis(
|
||||||
|
Axis::default()
|
||||||
|
.style(Style::default().fg(title_color))
|
||||||
|
.bounds([0.00, 60.0]),
|
||||||
|
)
|
||||||
|
.y_axis(
|
||||||
|
Axis::default()
|
||||||
|
.labels(vec![
|
||||||
|
Span::styled("", Style::default().fg(label_color)),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}", max),
|
||||||
|
Style::default()
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.fg(label_color),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
// add 0.01, for cases when the value is 0
|
||||||
|
.bounds([0.0, max.get_value() +0.01]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show error popup over whole screen
|
||||||
|
pub fn draw_info_bar<B: Backend>(
|
||||||
|
area: Rect,
|
||||||
|
columns: &Columns,
|
||||||
|
f: &mut Frame<'_, B>,
|
||||||
|
has_containers: bool,
|
||||||
|
info_visible: bool,
|
||||||
|
) {
|
||||||
|
let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black));
|
||||||
|
|
||||||
|
f.render_widget(block(), area);
|
||||||
|
|
||||||
|
let mut column_headings = format!(" {:>width$}", columns.state.0, width = columns.state.1);
|
||||||
|
column_headings.push_str(
|
||||||
|
format!(
|
||||||
|
"{} {:>width$}",
|
||||||
|
MARGIN,
|
||||||
|
columns.status.0,
|
||||||
|
width = columns.status.1
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
column_headings
|
||||||
|
.push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str());
|
||||||
|
column_headings
|
||||||
|
.push_str(format!("{}{:>width$}", MARGIN, columns.mem.0, width = columns.mem.1).as_str());
|
||||||
|
column_headings.push_str(
|
||||||
|
format!(
|
||||||
|
"{}{:>width$}",
|
||||||
|
MARGIN,
|
||||||
|
columns.name.0,
|
||||||
|
width = columns.name.1
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
column_headings.push_str(
|
||||||
|
format!(
|
||||||
|
"{}{:>width$}",
|
||||||
|
MARGIN,
|
||||||
|
columns.image.0,
|
||||||
|
width = columns.image.1
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
column_headings.push_str(
|
||||||
|
format!(
|
||||||
|
"{}{:>width$}",
|
||||||
|
MARGIN,
|
||||||
|
columns.net_rx.0,
|
||||||
|
width = columns.net_rx.1
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
column_headings.push_str(
|
||||||
|
format!(
|
||||||
|
"{}{:>width$}",
|
||||||
|
MARGIN,
|
||||||
|
columns.net_tx.0,
|
||||||
|
width = columns.net_tx.1
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let suffix = if info_visible { "exit" } else { "show" };
|
||||||
|
let info_text = format!("( h ) to {} help {}", suffix, MARGIN);
|
||||||
|
let info_width = info_text.chars().count();
|
||||||
|
|
||||||
|
let column_width = column_headings.chars().count();
|
||||||
|
|
||||||
|
let splits = if has_containers {
|
||||||
|
vec![
|
||||||
|
Constraint::Min(column_width as u16),
|
||||||
|
Constraint::Min(info_width as u16),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![Constraint::Percentage(100)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let split_bar = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(splits.as_ref())
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
if has_containers {
|
||||||
|
let paragraph = Paragraph::new(column_headings)
|
||||||
|
.block(block())
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
f.render_widget(paragraph, split_bar[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(info_text)
|
||||||
|
.block(block())
|
||||||
|
.alignment(Alignment::Right);
|
||||||
|
|
||||||
|
let index = if has_containers { 1 } else { 0 };
|
||||||
|
f.render_widget(paragraph, split_bar[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show error popup over whole screen
|
||||||
|
pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
||||||
|
let title = format!(" {} ", VERSION);
|
||||||
|
|
||||||
|
let mut description_text =
|
||||||
|
String::from("\n A basic docker container information viewer and controller.");
|
||||||
|
description_text.push_str("\n Tab or Alt+Tab to change panels, arrows to change lines, enter to send docker container commands.");
|
||||||
|
description_text.push_str("\n Mouse input also available.");
|
||||||
|
description_text.push_str("\n ( q ) to quit at any time.");
|
||||||
|
description_text
|
||||||
|
.push_str("\n\n currenty an early work in progress, all and any input appreciated");
|
||||||
|
description_text.push_str(format!("\n {}", REPO.trim()).as_str());
|
||||||
|
|
||||||
|
let mut max_line_width = 0;
|
||||||
|
|
||||||
|
let all_text = format!("{}{}", NAME_TEXT, description_text);
|
||||||
|
|
||||||
|
all_text.lines().into_iter().for_each(|line| {
|
||||||
|
let width = line.chars().count();
|
||||||
|
if width > max_line_width {
|
||||||
|
max_line_width = width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut lines = all_text.lines().count();
|
||||||
|
|
||||||
|
// Add some vertical and horizontal padding to the info box
|
||||||
|
lines += 3;
|
||||||
|
max_line_width += 4;
|
||||||
|
|
||||||
|
let name_paragraph = Paragraph::new(NAME_TEXT)
|
||||||
|
.style(Style::default().bg(Color::Magenta).fg(Color::White))
|
||||||
|
.block(Block::default())
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
|
||||||
|
let description_paragraph = Paragraph::new(description_text.as_str())
|
||||||
|
.style(Style::default().bg(Color::Magenta).fg(Color::Black))
|
||||||
|
.block(Block::default())
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title(title)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.border_style(Style::default().fg(Color::Black));
|
||||||
|
|
||||||
|
let area = centered_info(lines as u16, max_line_width as u16, f.size());
|
||||||
|
|
||||||
|
let split_popup = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Max(NAME_TEXT.lines().count() as u16),
|
||||||
|
Constraint::Max(description_text.lines().count() as u16),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
// Order is important here
|
||||||
|
f.render_widget(Clear, area);
|
||||||
|
f.render_widget(name_paragraph, split_popup[0]);
|
||||||
|
f.render_widget(description_paragraph, split_popup[1]);
|
||||||
|
f.render_widget(block, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show error popup over whole screen
|
||||||
|
pub fn draw_error<B: Backend>(f: &mut Frame<'_, B>, error: AppError, seconds: Option<u8>) {
|
||||||
|
let block = Block::default()
|
||||||
|
.title(" Error ")
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
|
||||||
|
let to_push = match error {
|
||||||
|
AppError::DockerConnect => {
|
||||||
|
format!(
|
||||||
|
"\n\n {}::v{} closing in {:02} seconds",
|
||||||
|
NAME,
|
||||||
|
VERSION,
|
||||||
|
seconds.unwrap_or(5)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => String::from("\n\n ( c ) to clear error\n ( q ) to quit oxker"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut text = format!("\n{}", error);
|
||||||
|
|
||||||
|
text.push_str(to_push.as_str());
|
||||||
|
|
||||||
|
let mut max_line_width = 0;
|
||||||
|
text.lines().into_iter().for_each(|line| {
|
||||||
|
let width = line.chars().count();
|
||||||
|
if width > max_line_width {
|
||||||
|
max_line_width = width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut lines = text.lines().count();
|
||||||
|
|
||||||
|
// Add some horizontal & vertical margins
|
||||||
|
max_line_width += 8;
|
||||||
|
lines += 3;
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.style(Style::default().bg(Color::Red).fg(Color::White))
|
||||||
|
.block(block)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
|
||||||
|
let area = centered_info(lines as u16, max_line_width as u16, f.size());
|
||||||
|
f.render_widget(Clear, area);
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// draw a box in the center of the screen, based on max line width + number of lines
|
||||||
|
fn centered_info(number_lines: u16, max_line_width: u16, r: Rect) -> Rect {
|
||||||
|
// This can panic if number_lines or max_line_width is larger than r.height or r.width
|
||||||
|
let blank_vertical = (r.height - number_lines) / 2;
|
||||||
|
let blank_horizontal = (r.width - max_line_width) / 2;
|
||||||
|
|
||||||
|
let popup_layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(number_lines),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(r);
|
||||||
|
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Max(blank_horizontal),
|
||||||
|
Constraint::Max(max_line_width),
|
||||||
|
Constraint::Max(blank_horizontal),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(popup_layout[1])[1]
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
use std::{collections::HashMap, fmt};
|
||||||
|
use tui::layout::Rect;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, std::hash::Hash, std::cmp::Eq, Clone, Copy)]
|
||||||
|
pub enum SelectablePanel {
|
||||||
|
Containers,
|
||||||
|
Commands,
|
||||||
|
Logs,
|
||||||
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Loading {
|
||||||
|
One,
|
||||||
|
Two,
|
||||||
|
Three,
|
||||||
|
Four,
|
||||||
|
Five,
|
||||||
|
Six,
|
||||||
|
Seven,
|
||||||
|
Eight,
|
||||||
|
Nine,
|
||||||
|
Ten,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Loading {
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::One => Self::Two,
|
||||||
|
Self::Two => Self::Three,
|
||||||
|
Self::Three => Self::Four,
|
||||||
|
Self::Four => Self::Five,
|
||||||
|
Self::Five => Self::Six,
|
||||||
|
Self::Six => Self::Seven,
|
||||||
|
Self::Seven => Self::Eight,
|
||||||
|
Self::Eight => Self::Nine,
|
||||||
|
Self::Nine => Self::Ten,
|
||||||
|
Self::Ten => Self::One,
|
||||||
|
// Self::Five => Self::One
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// "⠋",
|
||||||
|
// "⠙",
|
||||||
|
// "⠹",
|
||||||
|
// "⠸",
|
||||||
|
// "⠼",
|
||||||
|
// "⠴",
|
||||||
|
// "⠦",
|
||||||
|
// "⠧",
|
||||||
|
// "⠇",
|
||||||
|
// "⠏"
|
||||||
|
|
||||||
|
impl fmt::Display for Loading {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let disp = match self {
|
||||||
|
Self::One => "⠋",
|
||||||
|
Self::Two => "⠙",
|
||||||
|
Self::Three => "⠹",
|
||||||
|
Self::Four => "⠸",
|
||||||
|
Self::Five => "⠼",
|
||||||
|
Self::Six => "⠴",
|
||||||
|
Self::Seven => "⠦",
|
||||||
|
Self::Eight => "⠧",
|
||||||
|
Self::Nine => "⠇",
|
||||||
|
Self::Ten => "⠏",
|
||||||
|
};
|
||||||
|
write!(f, "{}", disp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectablePanel {
|
||||||
|
pub fn title(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Containers => "Containers",
|
||||||
|
Self::Logs => "Logs",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Containers => Self::Commands,
|
||||||
|
Self::Commands => Self::Logs,
|
||||||
|
Self::Logs => Self::Containers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn prev(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Containers => Self::Logs,
|
||||||
|
Self::Commands => Self::Containers,
|
||||||
|
Self::Logs => Self::Commands,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global gui_state, stored in an Arc<Mutex>
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GuiState {
|
||||||
|
// Think this should be a BMapTree, so can define order when iterating over potential intersects
|
||||||
|
// Is an issue if two panels are in the same space, sush as a smaller panel embedded, yet infront of, a larger panel
|
||||||
|
// If a BMapTree think it would mean have to implement ordering for SelectablePanel
|
||||||
|
area_map: HashMap<SelectablePanel, Rect>,
|
||||||
|
loading: Loading,
|
||||||
|
pub selected_panel: SelectablePanel,
|
||||||
|
pub show_help: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuiState {
|
||||||
|
/// Generate a default gui_state
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
area_map: HashMap::new(),
|
||||||
|
loading: Loading::One,
|
||||||
|
selected_panel: SelectablePanel::Containers,
|
||||||
|
show_help: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||||
|
pub fn clear_area_map(&mut self) {
|
||||||
|
self.area_map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||||
|
pub fn rect_insersects(&mut self, rect: Rect) {
|
||||||
|
if let Some(data) = self
|
||||||
|
.area_map
|
||||||
|
.iter()
|
||||||
|
.filter(|i| i.1.intersects(rect))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.get(0)
|
||||||
|
{
|
||||||
|
self.selected_panel = *data.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert selectable gui panel into area map
|
||||||
|
pub fn insert_into_area_map(&mut self, panel: SelectablePanel, area: Rect) {
|
||||||
|
self.area_map.entry(panel).or_insert(area);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change to next selectable panel
|
||||||
|
pub fn next_panel(&mut self) {
|
||||||
|
self.selected_panel = self.selected_panel.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change to previous selectable panel
|
||||||
|
pub fn previous_panel(&mut self) {
|
||||||
|
self.selected_panel = self.selected_panel.prev();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_loading(&mut self) {
|
||||||
|
self.loading = self.loading.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_loading(&mut self) -> String {
|
||||||
|
self.loading.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_loading(&mut self) {
|
||||||
|
self.loading = Loading::One;
|
||||||
|
}
|
||||||
|
}
|
||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use crossterm::{
|
||||||
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
sync::{atomic::Ordering, Arc},
|
||||||
|
};
|
||||||
|
use tokio::sync::broadcast::Sender;
|
||||||
|
use tui::{
|
||||||
|
backend::{Backend, CrosstermBackend},
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
Frame, Terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod color_match;
|
||||||
|
mod draw_blocks;
|
||||||
|
mod gui_state;
|
||||||
|
|
||||||
|
pub use self::color_match::*;
|
||||||
|
pub use self::gui_state::{GuiState, SelectablePanel};
|
||||||
|
use crate::{app_data::AppData, app_error::AppError, input_handler::InputMessages};
|
||||||
|
use draw_blocks::*;
|
||||||
|
|
||||||
|
/// Take control of the terminal in order to draw gui
|
||||||
|
pub async fn create_ui(
|
||||||
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
sender: Sender<InputMessages>,
|
||||||
|
is_running: Arc<AtomicBool>,
|
||||||
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
let res = run_app(&mut terminal, app_data, sender, is_running, gui_state).await;
|
||||||
|
|
||||||
|
disable_raw_mode().unwrap();
|
||||||
|
execute!(
|
||||||
|
terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
terminal.show_cursor().unwrap();
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
err.disp()
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a loop to draw the gui
|
||||||
|
async fn run_app<B: Backend>(
|
||||||
|
terminal: &mut Terminal<B>,
|
||||||
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
sender: Sender<InputMessages>,
|
||||||
|
is_running: Arc<AtomicBool>,
|
||||||
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let input_poll_rate = std::time::Duration::from_millis(75);
|
||||||
|
|
||||||
|
// Check for docker connect errors before attempting to draw the gui
|
||||||
|
let e = app_data.lock().get_error();
|
||||||
|
if let Some(error) = e {
|
||||||
|
if let AppError::DockerConnect = error {
|
||||||
|
let mut seconds = 5;
|
||||||
|
loop {
|
||||||
|
if seconds < 1 {
|
||||||
|
is_running.store(false, Ordering::SeqCst);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
terminal
|
||||||
|
.draw(|f| draw_error(f, AppError::DockerConnect, Some(seconds)))
|
||||||
|
.unwrap();
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
seconds -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| ui(f, &app_data, &gui_state)).unwrap();
|
||||||
|
if crossterm::event::poll(input_poll_rate).unwrap() {
|
||||||
|
let event = event::read().unwrap();
|
||||||
|
if let Event::Key(key) = event {
|
||||||
|
sender
|
||||||
|
.send(InputMessages::ButtonPress(key.code))
|
||||||
|
.unwrap_or(0);
|
||||||
|
} else if let Event::Mouse(m) = event {
|
||||||
|
sender.send(InputMessages::MouseEvent(m)).unwrap_or(0);
|
||||||
|
} else if let Event::Resize(_, _) = event {
|
||||||
|
gui_state.lock().clear_area_map();
|
||||||
|
terminal.autoresize().unwrap_or(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_running.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui<B: Backend>(
|
||||||
|
f: &mut Frame<'_, B>,
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
) {
|
||||||
|
// set max height for container section, needs +4 to deal with docker commands list and borders
|
||||||
|
let mut height = app_data.lock().get_container_len();
|
||||||
|
if height < 12 {
|
||||||
|
height += 4;
|
||||||
|
} else {
|
||||||
|
height = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
let column_widths = app_data.lock().get_width();
|
||||||
|
let has_containers = !app_data.lock().containers.items.is_empty();
|
||||||
|
let has_error = app_data.lock().get_error();
|
||||||
|
let log_index = app_data.lock().get_selected_log_index();
|
||||||
|
let selected_panel = gui_state.lock().selected_panel;
|
||||||
|
let show_help = gui_state.lock().show_help;
|
||||||
|
|
||||||
|
let whole_layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
|
||||||
|
.split(f.size());
|
||||||
|
|
||||||
|
// Split into 3, containers+controls, logs, then graphs
|
||||||
|
let upper_main = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Max(height as u16), Constraint::Percentage(50)].as_ref())
|
||||||
|
.split(whole_layout[1]);
|
||||||
|
|
||||||
|
let top_split = if has_containers {
|
||||||
|
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
|
||||||
|
} else {
|
||||||
|
vec![Constraint::Percentage(100)]
|
||||||
|
};
|
||||||
|
// Containers + docker commands
|
||||||
|
let top_panel = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(top_split.as_ref())
|
||||||
|
.split(upper_main[0]);
|
||||||
|
|
||||||
|
let lower_split = if has_containers {
|
||||||
|
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
|
||||||
|
} else {
|
||||||
|
vec![Constraint::Percentage(100)]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split into 3, containers+controls, logs, then graphs
|
||||||
|
let lower_main = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(lower_split.as_ref())
|
||||||
|
.split(upper_main[1]);
|
||||||
|
|
||||||
|
draw_containers(
|
||||||
|
app_data,
|
||||||
|
top_panel[0],
|
||||||
|
f,
|
||||||
|
gui_state,
|
||||||
|
&selected_panel,
|
||||||
|
&column_widths,
|
||||||
|
);
|
||||||
|
|
||||||
|
if has_containers {
|
||||||
|
draw_commands(
|
||||||
|
app_data,
|
||||||
|
top_panel[1],
|
||||||
|
f,
|
||||||
|
gui_state,
|
||||||
|
log_index,
|
||||||
|
&selected_panel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_logs(
|
||||||
|
app_data,
|
||||||
|
lower_main[0],
|
||||||
|
f,
|
||||||
|
gui_state,
|
||||||
|
log_index,
|
||||||
|
&selected_panel,
|
||||||
|
);
|
||||||
|
|
||||||
|
draw_info_bar(
|
||||||
|
whole_layout[0],
|
||||||
|
&column_widths,
|
||||||
|
f,
|
||||||
|
has_containers,
|
||||||
|
show_help,
|
||||||
|
);
|
||||||
|
|
||||||
|
// only draw charts if there are containers
|
||||||
|
if has_containers {
|
||||||
|
draw_chart(f, lower_main[1], app_data, log_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if error, and show popup if so
|
||||||
|
if show_help {
|
||||||
|
draw_help_box(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(error) = has_error {
|
||||||
|
app_data.lock().show_error = true;
|
||||||
|
draw_error(f, error, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user