feat: complete Rust-to-Go rewrite with Bubbletea v2
Rewrites oxker from Rust/ratatui to Go/Bubbletea, migrated to the Bubbletea v2 API (charm.land/bubbletea/v2). Removes all original Rust source files and legacy Go modules (internal/ui, internal/input, bubbles). Key changes: - View() returns tea.View with declarative AltScreen and MouseMode - KeyMsg → KeyPressMsg, MouseMsg → MouseClickMsg/WheelMsg/MotionMsg/ReleaseMsg - execWriteKey rewritten for v2 key fields (Code/Mod/Text) - Mouse toggle via View field instead of imperative commands - Filter/search text input uses msg.Text for v2 space key compat Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
ARG VARIANT="bullseye"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base: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
|
||||
|
||||
ENV PATH="/home/vscode/.cargo/bin:${PATH}"
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends build-essential pkg-config libssl-dev
|
||||
|
||||
USER vscode
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
@@ -1,53 +0,0 @@
|
||||
// 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"
|
||||
],
|
||||
"postCreateCommand": "cargo install cross typos-cli cargo-expand cargo-insta",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"bmuskalla.vscode-tldr",
|
||||
"christian-kohler.path-intellisense",
|
||||
"citreae535.sparse-crates",
|
||||
"foxundermoon.shell-format",
|
||||
"gruntfuggly.todo-tree",
|
||||
"mutantdino.resourcemonitor",
|
||||
"redhat.vscode-yaml",
|
||||
"rust-lang.rust-analyzer",
|
||||
"tamasfe.even-better-toml",
|
||||
"timonwong.shellcheck",
|
||||
"vadimcn.vscode-lldb"
|
||||
],
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2":{},
|
||||
"ghcr.io/devcontainers/features/git:1": {
|
||||
"version":"os-provided"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
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.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.1 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,25 +0,0 @@
|
||||
### 2026-02-23
|
||||
|
||||
*BREAKING CHANGES*
|
||||
+ `log_scroll_forward`, `log_scroll_back` renamed to `scroll_forward`, `scroll_back`
|
||||
+ Additional KeyMap entry, `inspect` defaults to `i`, enables Inspect mode
|
||||
+ Docker Host priorities reordered, *should* now be, from high to low order, `--host` cli argument, `config.toml` `host` value, `DOCKER_HOST` env, [Docker library](https://github.com/fussybeaver/bollard) default setting.
|
||||
+ `config.toml` `host` value is now commented out by default - this should help with invalid Docker connection errors and enable easy Podman support
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [4658a8de264698b0c8092e1227f0683527219a0b], [8b5899ca238bcbff32519b376b920cd7b7509809], [bebb687c59f3b408e69b23d2e68fa69f006a3231]
|
||||
+ GitHub workflow updated, [a0aa7918241ee8f702d6472c620287aa4be7d56c]
|
||||
|
||||
### Features
|
||||
+ Network chart, closes #79, [99fcb8fedf01599ec346b65d435d4c301a7a8851]
|
||||
+ Inspect mode & help panel redesign, [ae7f3f4a9472b451c37c0ab97b1756b41a3529f5]
|
||||
+ set rust-version in Cargo.toml, closes #77, [0763a1024f44d98b8d9d65f57995da538e40963c]
|
||||
|
||||
### Fixes
|
||||
+ Enable quit on Docker connect error screen, [5f942eb2e963660bd7fe9d80fa7ba8a83754803a]
|
||||
|
||||
### Refactors
|
||||
+ dead code removed, [3e31a2a6bc02d6ef75bd6cbc18568e82e60e1ee3]
|
||||
+ docker data spawns, [cd943f67e465fff9726b40570a089301a4a8f534]
|
||||
|
||||
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
@@ -1,186 +0,0 @@
|
||||
name: Release CI
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
jobs:
|
||||
|
||||
#################################################
|
||||
## Cross platform binary build for release page #
|
||||
#################################################
|
||||
|
||||
cross_platform_build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-musl
|
||||
output_name: linux_x86_64.tar.gz
|
||||
|
||||
- target: aarch64-unknown-linux-musl
|
||||
output_name: linux_aarch64.tar.gz
|
||||
|
||||
- target: arm-unknown-linux-musleabihf
|
||||
output_name: linux_armv6.tar.gz
|
||||
|
||||
- target: aarch64-apple-darwin
|
||||
output_name: apple_darwin_aarch64.tar.gz
|
||||
|
||||
- target: x86_64-pc-windows-gnu
|
||||
output_name: windows_x86_64.zip
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Install stable rust, and associated tools
|
||||
- name: install rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# Install cross-rs
|
||||
- name: install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: build
|
||||
if: matrix.target == 'aarch64-apple-darwin'
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v "$(pwd):/io" \
|
||||
-w /io \
|
||||
ghcr.io/rust-cross/cargo-zigbuild \
|
||||
bash -ec '
|
||||
rustup update stable
|
||||
rustup default stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
cargo zigbuild --release --target aarch64-apple-darwin
|
||||
'
|
||||
|
||||
# Build all other targets using Cross
|
||||
- name: build
|
||||
if: matrix.target != 'aarch64-apple-darwin'
|
||||
run: cross build --target ${{ matrix.target }} --release
|
||||
|
||||
# Compress the output
|
||||
- name: compress windows
|
||||
if: matrix.target == 'x86_64-pc-windows-gnu'
|
||||
run: |
|
||||
zip -j "./oxker_${{ matrix.output_name }}" target/${{ matrix.target }}/release/oxker.exe
|
||||
|
||||
# Compress the output
|
||||
- name: compress linux
|
||||
if: matrix.target != 'x86_64-pc-windows-gnu'
|
||||
run: |
|
||||
tar -C "target/${{ matrix.target }}/release" -czf "./oxker_${{ matrix.output_name }}" oxker
|
||||
|
||||
# Upload output for release page
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
if-no-files-found: error
|
||||
name: ${{ matrix.target }}
|
||||
path: oxker_${{ matrix.output_name }}
|
||||
retention-days: 1
|
||||
|
||||
###################
|
||||
## Create release #
|
||||
###################
|
||||
|
||||
create_release:
|
||||
needs: [cross_platform_build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup | Artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
|
||||
- name: Update Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
makeLatest: true
|
||||
name: ${{ github.ref_name }}
|
||||
tag: ${{ github.ref }}
|
||||
bodyFile: ".github/release-body.md"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: |
|
||||
**/oxker_*.zip
|
||||
**/oxker_*.tar.gz
|
||||
|
||||
#########################################
|
||||
## Build images for Dockerhub & ghcr.io #
|
||||
#########################################
|
||||
|
||||
container_image_build:
|
||||
needs: [create_release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Write release version to env
|
||||
run: |
|
||||
CURRENT_SEMVER=${GITHUB_REF_NAME#v}
|
||||
echo "CURRENT_SEMVER=$CURRENT_SEMVER" >> $GITHUB_ENV
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
install: true
|
||||
- name: Build for Dockerhub & ghcr.io
|
||||
run: |
|
||||
docker build --platform linux/arm/v6,linux/arm64,linux/amd64 \
|
||||
-t ${{ secrets.DOCKERHUB_USERNAME }}/oxker:latest \
|
||||
-t ${{ secrets.DOCKERHUB_USERNAME }}/oxker:${{env.CURRENT_SEMVER}} \
|
||||
-t ghcr.io/${{ github.repository }}:latest \
|
||||
-t ghcr.io/${{ github.repository }}:${{env.CURRENT_SEMVER}} \
|
||||
--provenance=false --sbom=false \
|
||||
--push \
|
||||
-f containerised/Dockerfile .
|
||||
|
||||
########################
|
||||
# Publish to crates.io #
|
||||
########################
|
||||
|
||||
# This could be moved to before a github release is made
|
||||
dry_run_cargo_publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [container_image_build]
|
||||
steps:
|
||||
- name: update rust stable
|
||||
run: rustup update stable
|
||||
- uses: actions/checkout@v5
|
||||
- name: Publish Dry Run
|
||||
run: cargo publish --dry-run
|
||||
|
||||
cargo_publish:
|
||||
environment: crates.io
|
||||
runs-on: ubuntu-latest
|
||||
needs: [dry_run_cargo_publish]
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
|
||||
steps:
|
||||
- name: update rust stable
|
||||
run: rustup update stable
|
||||
- uses: actions/checkout@v5
|
||||
- name: Publish
|
||||
run: cargo publish
|
||||
Vendored
-45
@@ -1,45 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'oxker'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=oxker",
|
||||
"--package=oxker"
|
||||
],
|
||||
"filter": {
|
||||
"name": "oxker",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'oxker'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=oxker",
|
||||
"--package=oxker"
|
||||
],
|
||||
"filter": {
|
||||
"name": "oxker",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
-805
@@ -1,805 +0,0 @@
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.13.0'>v0.13.0</a>
|
||||
### 2026-02-23
|
||||
|
||||
*BREAKING CHANGES*
|
||||
+ `log_scroll_forward`, `log_scroll_back` renamed to `scroll_forward`, `scroll_back`
|
||||
+ Additional KeyMap entry, `inspect` defaults to `i`, enables Inspect mode
|
||||
+ Docker Host priorities reordered, *should* now be, from high to low order, `--host` cli argument, `config.toml` `host` value, `DOCKER_HOST` env, [Docker library](https://github.com/fussybeaver/bollard) default setting.
|
||||
+ `config.toml` `host` value is now commented out by default - this should help with invalid Docker connection errors and enable easy Podman support
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [4658a8de](https://github.com/mrjackwills/oxker/commit/4658a8de264698b0c8092e1227f0683527219a0b), [8b5899ca](https://github.com/mrjackwills/oxker/commit/8b5899ca238bcbff32519b376b920cd7b7509809), [bebb687c](https://github.com/mrjackwills/oxker/commit/bebb687c59f3b408e69b23d2e68fa69f006a3231)
|
||||
+ GitHub workflow updated, [a0aa7918](https://github.com/mrjackwills/oxker/commit/a0aa7918241ee8f702d6472c620287aa4be7d56c)
|
||||
|
||||
### Features
|
||||
+ Network chart, closes [#79](https://github.com/mrjackwills/oxker/issues/79), [99fcb8fe](https://github.com/mrjackwills/oxker/commit/99fcb8fedf01599ec346b65d435d4c301a7a8851)
|
||||
+ Inspect mode & help panel redesign, [ae7f3f4a](https://github.com/mrjackwills/oxker/commit/ae7f3f4a9472b451c37c0ab97b1756b41a3529f5)
|
||||
+ set rust-version in Cargo.toml, closes [#77](https://github.com/mrjackwills/oxker/issues/77), [0763a102](https://github.com/mrjackwills/oxker/commit/0763a1024f44d98b8d9d65f57995da538e40963c)
|
||||
|
||||
### Fixes
|
||||
+ Enable quit on Docker connect error screen, [5f942eb2](https://github.com/mrjackwills/oxker/commit/5f942eb2e963660bd7fe9d80fa7ba8a83754803a)
|
||||
|
||||
### Refactors
|
||||
+ dead code removed, [3e31a2a6](https://github.com/mrjackwills/oxker/commit/3e31a2a6bc02d6ef75bd6cbc18568e82e60e1ee3)
|
||||
+ docker data spawns, [cd943f67](https://github.com/mrjackwills/oxker/commit/cd943f67e465fff9726b40570a089301a4a8f534)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.12.0'>v0.12.0</a>
|
||||
### 2025-09-28
|
||||
|
||||
### Chores
|
||||
+ create_release.sh updated, [d4af754a](https://github.com/mrjackwills/oxker/commit/d4af754ad245540db60177f7b202b3c64519c961)
|
||||
+ dependencies updated, [03599b46](https://github.com/mrjackwills/oxker/commit/03599b46657d38d0c9f25c2ccfd9510f2b98dd84), [aef0c950](https://github.com/mrjackwills/oxker/commit/aef0c9503e7045a256856aa887d8c8d7722b9936), [f0771eab](https://github.com/mrjackwills/oxker/commit/f0771eab5d07d141fe7a8997db650f0f65ffe0a7), [1596de86](https://github.com/mrjackwills/oxker/commit/1596de8681ad6c0a7832eb922dd2dc36ab30eb41)
|
||||
+ GitHub workflow updated, [66dae5e6](https://github.com/mrjackwills/oxker/commit/66dae5e61ea294ac8ce134a6c32b27c04166b6eb)
|
||||
|
||||
### Docs
|
||||
+ fix numerous typos, [618a43b5](https://github.com/mrjackwills/oxker/commit/618a43b501914fdf2659e171172ad180364cf87a)
|
||||
|
||||
### Features
|
||||
+ *BREAKING CHANGE* - `scroll_down_many` & `scroll_up_many` removed, `scroll_down_one` `scroll_up_one` renamed `scroll_down`, `scroll_up`, see [example_config](https://github.com/mrjackwills/oxker/tree/main/example_config), [52a04ec1](https://github.com/mrjackwills/oxker/commit/52a04ec1d0b9e4877e304f60a857ebc00f88b4fd)
|
||||
+ log search feature, closes [#72](https://github.com/mrjackwills/oxker/issues/72). Use `#` button, remappable via `log_search_mode`, to enter log search mode. Case-sensitive by default, editable in `config.toml` with `log_search_case_sensitive` entry. Customise colours via `[colors.log_search]` entries, again see see [example_config](https://github.com/mrjackwills/oxker/tree/main/example_config), [96d94696](https://github.com/mrjackwills/oxker/commit/96d9469623a7c90b79aa8d82abf587290343ad37), [a2316a9c](https://github.com/mrjackwills/oxker/commit/a2316a9cac270790920a1ebd1be6532d51aba77c)
|
||||
+ `term` renamed `filter term`, tests updated, [487c3faf](https://github.com/mrjackwills/oxker/commit/487c3faf96f4c197c8b82644c02466ea40626a5e)
|
||||
|
||||
My 32-bit armhf armv6 hardware no longer seems to be able to run Docker. Future `oxker` releases won't be tested on real hardware but will continue to be built and published for armv6.
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.11.1'>v0.11.1</a>
|
||||
### 2025-08-21
|
||||
|
||||
### Reverts
|
||||
+ GitHub workflow reverted, [d1b69858](https://github.com/mrjackwills/oxker/commit/d1b69858c622636afbc46db7d37cf91e58d61212)
|
||||
|
||||
see [v0.11.0 release notes](https://github.com/mrjackwills/oxker/releases/tag/v0.11.0) for more information about v0.11 release
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.11.0'>v0.11.0</a>
|
||||
### 2025-08-21
|
||||
|
||||
### Chores
|
||||
+ Dependencies updated, [ced885e0](https://github.com/mrjackwills/oxker/commit/ced885e0128b6d5d3a3c7cb97d7e53bc2da64893), [f9b40ea0](https://github.com/mrjackwills/oxker/commit/f9b40ea03d0e70e235c28646ff3f9ebb468a904d)
|
||||
+ Rust 1.89 linting, [79d19cee](https://github.com/mrjackwills/oxker/commit/79d19ceeb81ae60bc5562683e405d6e74e6f2578)
|
||||
+ GitHub workflow updated, [08384200](https://github.com/mrjackwills/oxker/commit/08384200558fa1b9d378ea62ea832708caebaa91), [6573af1e](https://github.com/mrjackwills/oxker/commit/6573af1ed7d382a81c1305397e904066bb8395a8)
|
||||
|
||||
### Features
|
||||
+ Horizontally scroll across logs. By default use `←` & `→` keys to traverse horizontally across the lines when logs panel selected. Updated `config.toml` with `log_scroll_forward` and `log_scroll_back` [c190f020](https://github.com/mrjackwills/oxker/commit/c190f0206cc55b8e45b8373f9be954e828c18b3b), [8939ac03](https://github.com/mrjackwills/oxker/commit/8939ac0345326633e794cc10a981a1f3c5c07549)
|
||||
+ Force clear screen & redraw of UI. By default uses `f` key, `config.toml` updated with `force_redraw` [50edbc0c](https://github.com/mrjackwills/oxker/commit/50edbc0cc09db864835fe81a03cba8eadafe548b)
|
||||
+ Increase scroll speed using the `ctrl` key in conjunction with a scroll key, `config.toml` updated with `scroll_modifier`. The next release will remove `scroll_down_many` & `scroll_down_up` keys, [c5bbffdb](https://github.com/mrjackwills/oxker/commit/c5bbffdb5f9e800951e4060aa6aee8e00db589aa)
|
||||
|
||||
### Refactors
|
||||
+ remove macos cfg none-const functions, Zigbuild now uses Rust 1.87.0, [eb686e2c](https://github.com/mrjackwills/oxker/commit/eb686e2c952e04da74b3e12c0bfa015ec4615e1d)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.10.5'>v0.10.5</a>
|
||||
### 2025-06-19
|
||||
|
||||
### Reverts
|
||||
+ Bollard update rolled back, closes [#66](https://github.com/mrjackwills/oxker/issues/66), [aac9c6b5](https://github.com/mrjackwills/oxker/commit/aac9c6b598ce6c23b14f5a8b0116e662b18074d2)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.10.4'>v0.10.4</a>
|
||||
### 2025-06-18
|
||||
|
||||
### Chores
|
||||
+ .devcontainer updated, [324f8268](https://github.com/mrjackwills/oxker/commit/324f8268278081504d5357f2ed89b78ca2c25d04)
|
||||
+ dependencies updated, [0ace9dd6](https://github.com/mrjackwills/oxker/commit/0ace9dd662144a589341779a64d7fcd8de7d9978), [a6360075](https://github.com/mrjackwills/oxker/commit/a636007547280b3b3db69374601dbece4bc21eef)
|
||||
+ Rust 1.87.0 linting, [395b1aa7](https://github.com/mrjackwills/oxker/commit/395b1aa7e997a528e4f21e66f5f859001c1c3ec1), [67e5888e](https://github.com/mrjackwills/oxker/commit/67e5888e008cfd504c10e47f678f9351c838be99)
|
||||
back
|
||||
### Docs
|
||||
+ example config files updated, [63ab7de7](https://github.com/mrjackwills/oxker/commit/63ab7de72897de460f31181c5a42befbee2f91d3), [8fb5ac4a](https://github.com/mrjackwills/oxker/commit/8fb5ac4a945b75f3fcd118c53be1202ccbc43c59)
|
||||
+ README.md updated, link to directories crate, closes [#65](https://github.com/mrjackwills/oxker/issues/65), [c2bfe329](https://github.com/mrjackwills/oxker/commit/c2bfe3296563daf4b7f077469f3eeff6895720b0)
|
||||
|
||||
### Features
|
||||
+ log panel size configurable, closes [#50](https://github.com/mrjackwills/oxker/issues/50), use the `-` or `=` keys to change the height of the logs panel, or `\` to toggle visibility. Automatically hide the logs panel using a new config item `show_logs`, see `example_config/*` files for more details, [6edf99e0](https://github.com/mrjackwills/oxker/commit/6edf99e0846bb4134d8ee5b646065b8cda8074d7)
|
||||
+ build release binaries for aarch64-apple-darwin, closes [#62](https://github.com/mrjackwills/oxker/issues/62), personally untested on MacOS - but others suggest it works as expected, [e7114d2f](https://github.com/mrjackwills/oxker/commit/e7114d2f5e0ed8935943be64726fc2d90464a777), [2e850090](https://github.com/mrjackwills/oxker/commit/2e8500902a515a246f9d9a503b4350849d634978)
|
||||
|
||||
### Fixes
|
||||
+ merge args color/raw fix, [d1983987](https://github.com/mrjackwills/oxker/commit/d198398795698a21d81d3fd20231c482cc346ab5)
|
||||
|
||||
### Refactors
|
||||
+ reduce cloning of the logs text items, can expect 40-50% reduction in CPU and memory usage in certain common situations, [ecefa302](https://github.com/mrjackwills/oxker/commit/ecefa302b9ef5320ad4cce0b606aca70a7b459e2)
|
||||
+ dead code removed, [b40b6b19](https://github.com/mrjackwills/oxker/commit/b40b6b197e4e5fbdab083bc918d1a5d2750597f3)
|
||||
|
||||
### Tests
|
||||
+ add more whole layout tests, [4b81c6ca](https://github.com/mrjackwills/oxker/commit/4b81c6caaf12028d7527c3f23cd2de6d1503e223)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.10.3'>v0.10.3</a>
|
||||
### 2025-04-22
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [bbfd2462](https://github.com/mrjackwills/oxker/commit/bbfd2462a1f45008587b488e8c6049ee76da72f2)
|
||||
|
||||
### Tests
|
||||
+ fix tests for MacOS, closes [#61](https://github.com/mrjackwills/oxker/issues/61), [cfc2decd](https://github.com/mrjackwills/oxker/commit/cfc2decd8d237f1ac3f0bdb2b3d5581684064448)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.10.2'>v0.10.2</a>
|
||||
### 2025-04-19
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [1345ecb1](https://github.com/mrjackwills/oxker/commit/1345ecb1a2b17ad3d288f2de2058c0777b84f93b)
|
||||
|
||||
### Tests
|
||||
+ use a fixed version String, `0.00.000`, in tests, [230174b3](https://github.com/mrjackwills/oxker/commit/230174b3c327c3f217cdcf8fce07d5d9ddea1033)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.10.1'>v0.10.1</a>
|
||||
### 2025-04-18
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [8f959c54](https://github.com/mrjackwills/oxker/commit/8f959c5408995527485e817514c3f4a10bca31bd), [69d1801e](https://github.com/mrjackwills/oxker/commit/69d1801ea1e71e7d84c00fb2142ba27577e3bb73), [188490e1](https://github.com/mrjackwills/oxker/commit/188490e13fd1255eecb305ac0a99a7a1913e0294)
|
||||
+ Rust 1.86.0 linting, [9acf6033](https://github.com/mrjackwills/oxker/commit/9acf60334c5224faa9ee4ecf7030b2e9b13b7d67)
|
||||
|
||||
### Docs
|
||||
+ comment typo, [723b220c](https://github.com/mrjackwills/oxker/commit/723b220c6aa6393b8eebd84a6ddba69f35dc18b8)
|
||||
|
||||
### Fixes
|
||||
+ github workflow update, [997eebca](https://github.com/mrjackwills/oxker/commit/997eebca20a2883dcfcb35009dd4d7b0d438dec6)
|
||||
+ config merging, [a468827f](https://github.com/mrjackwills/oxker/commit/a468827f02c5243b9bd4e0183fed16854ae6c851)
|
||||
|
||||
### Refactors
|
||||
+ rename ChartType to ChartVariant, [bca67116](https://github.com/mrjackwills/oxker/commit/bca67116f3b71451156a39a7b0957568b26fa183), [d0caa927](https://github.com/mrjackwills/oxker/commit/d0caa9271b6f92f52ccfe3dec69708efe54e5170)
|
||||
+ rename FileType to FileFormat, [848f64d0](https://github.com/mrjackwills/oxker/commit/848f64d0da429e547a2f6c8de62e4da5f5c9a187)
|
||||
|
||||
### Tests
|
||||
+ Use insta, closes [#57](https://github.com/mrjackwills/oxker/issues/57), [9362d7b4](https://github.com/mrjackwills/oxker/commit/9362d7b481ea22eab6f902dc7f3c10150c7ddf22)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.10.0'>v0.10.0</a>
|
||||
### 2025-02-23
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [e5f355a1](https://github.com/mrjackwills/oxker/commit/e5f355a1928f78abdb64e4c5617d6fac06340016), [4539d8ad](https://github.com/mrjackwills/oxker/commit/4539d8ad0705b46d7c89c51c7be482b696d26e5f), [6aee6181](https://github.com/mrjackwills/oxker/commit/6aee6181136235a1a4f79af9b9748c1801be8bf8), [64d1bdf2](https://github.com/mrjackwills/oxker/commit/64d1bdf2bf88407e02f0eded1e03fcfc5ee2d8e3)
|
||||
+ .devcontainer dependencies updated, [5c8e76e7](https://github.com/mrjackwills/oxker/commit/5c8e76e7bb4d7aab8543c9be09fdbc4ffa446b10)
|
||||
+ example docker-compose.yml updated, [2354b0b9](https://github.com/mrjackwills/oxker/commit/2354b0b9be1ab3795a421512594b2650b9cbdd74)
|
||||
+ Rust 1.84 linting, [3065265e](https://github.com/mrjackwills/oxker/commit/3065265e26c30d78ba738cfe731d3901ec1948d0)
|
||||
|
||||
### Features
|
||||
+ Config file introduced, including customizing color scheme of application, closes [#47](https://github.com/mrjackwills/oxker/issues/47), [f4d54e1b](https://github.com/mrjackwills/oxker/commit/f4d54e1ba8ea1516394aef19511a63e6271f27bf)
|
||||
+ Enable log timestamps to be set to any given timezone, plus custom timestamp format via the config file, closes [#56](https://github.com/mrjackwills/oxker/issues/56), [7a5e7a25873d2c270e5808730721ebb5427a051]
|
||||
+ update Rust edition to 2024, [7e4a960b](https://github.com/mrjackwills/oxker/commit/7e4a960b888f1dab524d6045504162cea1171d20)
|
||||
|
||||
### Fixes
|
||||
+ Only draw screen if data or layout has changed, drastically reduces CPU usage, [bfc295c5](https://github.com/mrjackwills/oxker/commit/bfc295c50e982886ccaa5e60b57f10d3690b3f09)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.9.0'>v0.9.0</a>
|
||||
### 2024-12-05
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [b7871357](https://github.com/mrjackwills/oxker/commit/b78713579c4706d605e5b35fcd832610a0152294), [c6200e8f](https://github.com/mrjackwills/oxker/commit/c6200e8f77f8bb1f0152cb9374029d15cc45df9d)
|
||||
+ Rust 1.83 linting, [751d997a](https://github.com/mrjackwills/oxker/commit/751d997a3dac823e144ae62e6c1455676e50ddb8)
|
||||
|
||||
### Features
|
||||
+ `--no-stderr` cli arg, removes Standard error output from logs, closes [#52](https://github.com/mrjackwills/oxker/issues/52), [c739637b](https://github.com/mrjackwills/oxker/commit/c739637b91c8fa742a69f4d888678d7b3964678c)
|
||||
+ ContainerPorts use ipaddr, [1b26997d](https://github.com/mrjackwills/oxker/commit/1b26997d25f748e0d452f41fe41791533046ecdf)
|
||||
|
||||
### Fixes
|
||||
+ update containerised Dockerfile, [0c6f5322](https://github.com/mrjackwills/oxker/commit/0c6f53228f01196e352c2069383ba1e7a10950a8)
|
||||
+ calculate_usage overflow, [5106a01f](https://github.com/mrjackwills/oxker/commit/5106a01f3dcb87ce5a8f1fb7bf49dc6b3c25d03e)
|
||||
+ DockerData spawns insertion error, [d4906d33](https://github.com/mrjackwills/oxker/commit/d4906d33c26b75d92e7d80040c488faa90a257c6)
|
||||
|
||||
### Refactors
|
||||
+ speed up docker logs init process, [8b9fe424](https://github.com/mrjackwills/oxker/commit/8b9fe4246865441704ae12dff0938868a4fe6f81)
|
||||
+ remove docker sleep, [f1562d10](https://github.com/mrjackwills/oxker/commit/f1562d1084336fe5be39894c93cb49107f0a4a6d)
|
||||
+ dead code removed, [5ee48d57](https://github.com/mrjackwills/oxker/commit/5ee48d5708fa6de0206c021db0bb611196e66fba), [ba6a9524](https://github.com/mrjackwills/oxker/commit/ba6a95241389f99d504ee4bf3e87e19006f12e49), [f0b11456](https://github.com/mrjackwills/oxker/commit/f0b1145651625ad4e577d79baaf902d4d3bc0579)
|
||||
+ input_handler, [7f423834](https://github.com/mrjackwills/oxker/commit/7f4238349525c01ae9fb8b1f6c0946e5364dd55e)
|
||||
+ statefulList get_state_title, [2d540b0e](https://github.com/mrjackwills/oxker/commit/2d540b0e2210cc04d73035ec59211ffc739174f6)
|
||||
+ statefulList next/previous, [7bb2bef2](https://github.com/mrjackwills/oxker/commit/7bb2bef28d90ebc58da86a0365a1904a0c32dffe)
|
||||
+ help_box closure fn, [2860426d](https://github.com/mrjackwills/oxker/commit/2860426d57a4458fcee49a2fd20e8e7bb9e71fb5)
|
||||
+ use check_sub for sleep calculations, [fe3696e5](https://github.com/mrjackwills/oxker/commit/fe3696e5576739d8b033d9e748b5ea696c4b4e4f)
|
||||
+ rename scheduler to heartbeat, [68a6551e](https://github.com/mrjackwills/oxker/commit/68a6551ed038a36330b2f098112829465a1c3c7a)
|
||||
+ remove unnecessary is_running load, [76ccf7c0](https://github.com/mrjackwills/oxker/commit/76ccf7c00691f815c3ab0bede838c99252ba84f0)
|
||||
+ execute_command(), [2a834d6c](https://github.com/mrjackwills/oxker/commit/2a834d6c2fa4a15124d24ddbd12f667829e148ad)
|
||||
+ Remove numerous clones(), [e5927f78](https://github.com/mrjackwills/oxker/commit/e5927f781a7e9517b9fa00a2d1a835d2774a9d26)
|
||||
+ remove app_data param from generate_lock(), [1a8dab65](https://github.com/mrjackwills/oxker/commit/1a8dab654a1fdbf351a72dc54fe3d1943355bba6)
|
||||
+ combine get_filter methods, [356ea554](https://github.com/mrjackwills/oxker/commit/356ea5549bb4877e9893fe0e1053e73c5a62e806)
|
||||
+ FrameData refactors, [57781701](https://github.com/mrjackwills/oxker/commit/57781701ff14c553dfbafb965ee8a33ab44dd36f), [6e2f82db](https://github.com/mrjackwills/oxker/commit/6e2f82db81caaa98ce4781fa15928eb9e246ace6)
|
||||
+ update_container_stat combine is_alive(), [55cc7467](https://github.com/mrjackwills/oxker/commit/55cc746736f6863aedc5ad838744a983796244d8)
|
||||
+ remove `input_poll_rate` from `Ui`, instead use const `POLL_RATE`, [69f6c96b](https://github.com/mrjackwills/oxker/commit/69f6c96b700b9fde5578ae204992a67986d456ab)
|
||||
+ pass `&FrameDate` into `draw_frame()`, [35aec506](https://github.com/mrjackwills/oxker/commit/35aec5060fdbe606267be26656b4aeee43d50c02)
|
||||
+ dead code removed, [caf23be4](https://github.com/mrjackwills/oxker/commit/caf23be4a7faff99aaca80b081a02e4e0a372009)
|
||||
+ input_handler, [9c4f8910](https://github.com/mrjackwills/oxker/commit/9c4f8910381b90b563da12eaba4b79cb60c40129)
|
||||
+ draw_block, [de76bc22](https://github.com/mrjackwills/oxker/commit/de76bc22936b124dcb9646f302f6cc14691dbb63)
|
||||
|
||||
### Tests
|
||||
+ fix logs tests, [9b22f5da](https://github.com/mrjackwills/oxker/commit/9b22f5da18e4bf92766a68a7f4cd61ad72724cfd)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.8.0'>v0.8.0</a>
|
||||
### 2024-10-22
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [ea877d23](https://github.com/mrjackwills/oxker/commit/ea877d23711b98ffd1108a74206d93d43482d44d), [af609c0d](https://github.com/mrjackwills/oxker/commit/af609c0dbf0caab4a073f822166de34999afb41b)
|
||||
+ .devcontainer updated, [a9844436](https://github.com/mrjackwills/oxker/commit/a9844436d003b84a3e9d8b600ea029b232566f3a)
|
||||
+ create_release.sh updated, [c4943370](https://github.com/mrjackwills/oxker/commit/c4943370f4a67f6c01c75a8a7f825912427666a2), [1389d8ad](https://github.com/mrjackwills/oxker/commit/1389d8adbba75fef480eb1de09337eb7beb10ba3)
|
||||
|
||||
### Features
|
||||
+ Add Stderr output to logs, thanks [vincentmasse](https://github.com/vincentmasse), closes [#48](https://github.com/mrjackwills/oxker/issues/48), merges [#49](https://github.com/mrjackwills/oxker/pull/49), [b95c9311](https://github.com/mrjackwills/oxker/commit/b95c9311416cd0dbcfa5de90c23f3065bc2d6b17), [9936ad45](https://github.com/mrjackwills/oxker/commit/9936ad45e186ee431aade920674a2dc283937355), [289ede3f](https://github.com/mrjackwills/oxker/commit/289ede3f2531feeec56094a76bf34f4c69431bbe)
|
||||
|
||||
### Refactors
|
||||
+ Rust 1.82 linting, [c058c5a3](https://github.com/mrjackwills/oxker/commit/c058c5a301cfd4e8d7a0079c4c3f8fdeae2803e5)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.7.2'>v0.7.2</a>
|
||||
### 2024-09-07
|
||||
|
||||
### Reverts
|
||||
+ Expect lint was causing issues with crates/docker builds, revert until fix is found, [578ed9f0](https://github.com/mrjackwills/oxker/commit/578ed9f085df0d97c451c06dab4ecbfccd894c52)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.7.1'>v0.7.1</a>
|
||||
### 2024-09-07
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [d6238587](https://github.com/mrjackwills/oxker/commit/d6238587ffc536f1ea93a47dd4d2ee69c36f35e3), [a564ef80](https://github.com/mrjackwills/oxker/commit/a564ef80318adbde9f188dd2cf38626a98793c75), [1d82ff13](https://github.com/mrjackwills/oxker/commit/1d82ff1368f7b43e6df8478d5e7b8d682320010a), [99f05f2e](https://github.com/mrjackwills/oxker/commit/99f05f2e5b511d039804159c92ade6c77ff360b7)
|
||||
+ Rust 1.81.0 linting, [372f759c](https://github.com/mrjackwills/oxker/commit/372f759ca467e47c373a086c6a247c150b87d4bc)
|
||||
+ .devcontainer updated, [5d77f1e0](https://github.com/mrjackwills/oxker/commit/5d77f1e02a428b4e5ee1bf5466055ad17f6e05af)
|
||||
|
||||
### Docs
|
||||
+ CHANGELOG.md duplicate removed, [16ecc5a5](https://github.com/mrjackwills/oxker/commit/16ecc5a51f7defcc9dd4f4c6e34fa5bbfc7fea78)
|
||||
+ Readme raspberry pi fix, [baf68783](https://github.com/mrjackwills/oxker/commit/baf68783929e5d6ac111a39dc62388cd24133da6)
|
||||
+ Add installation guide to README for macOS installation via `brew install oxker`, thanks [miketheman](https://github.com/miketheman), [59817311](https://github.com/mrjackwills/oxker/commit/59817311baea628d2691765ff9387e055dce3307), [895ec620](https://github.com/mrjackwills/oxker/commit/895ec6204cc8220be64be7622df6edb6ef6d795b)
|
||||
|
||||
### Refactors
|
||||
+ switch lints from `allow(x)` to `expect(x)`, [2a0ab6d8](https://github.com/mrjackwills/oxker/commit/2a0ab6d81ce4062de053a92b85f8b25ea23412b6)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.7.0'>v0.7.0</a>
|
||||
### 2024-08-01
|
||||
|
||||
### Chores
|
||||
+ .devcontainer extensions updated, [0288cbc8](https://github.com/mrjackwills/oxker/commit/0288cbc8146cde1dd40ceaec9550198b635bb8f5)
|
||||
+ dependencies updated, [1df4f78d](https://github.com/mrjackwills/oxker/commit/1df4f78dc41013c33d901925933b1ccb29ad4bc8), [5ae253b8](https://github.com/mrjackwills/oxker/commit/5ae253b8734ba0495e4e8149b17d5228b3d86f8d), [7a517db9](https://github.com/mrjackwills/oxker/commit/7a517db9f7c14c35e56ff70cf76ffb608fd30e17), [9c291cd9](https://github.com/mrjackwills/oxker/commit/9c291cd9c81b6d9a02085878588ed3b845fd0046), [0e90f4eb](https://github.com/mrjackwills/oxker/commit/0e90f4eb55ac5fb5d45e7d212c3686027dd3913e), [fe71cbfb](https://github.com/mrjackwills/oxker/commit/fe71cbfb00f166b7c02a6e28e64650ed1b47d15d)
|
||||
+ docker-compose alpine version bump, [51ceab3e](https://github.com/mrjackwills/oxker/commit/51ceab3ebdb09356cd401d2f268840239255126f)
|
||||
+ Rust 1.80 linting, [93e1279b](https://github.com/mrjackwills/oxker/commit/93e1279b1fc77019442a385e2e36be2fe438e828)
|
||||
+ create_release v0.5.6, [f408acfe](https://github.com/mrjackwills/oxker/commit/f408acfe9a9f5a976735b8a8a51500fd7b865daf)
|
||||
|
||||
### Docs
|
||||
+ screenshot updated, [6975ebe7](https://github.com/mrjackwills/oxker/commit/6975ebe70f7058229c232e4a56b090f55247d2a2)
|
||||
|
||||
### Features
|
||||
+ left align all text, [e0d421c4](https://github.com/mrjackwills/oxker/commit/e0d421c4918a17c9e0e21fd214edb99d71281c9d)
|
||||
+ place image name in logs panel title, [12f24357](https://github.com/mrjackwills/oxker/commit/12f24357a68abe871f44d871d95b6e2ef062181e)
|
||||
+ distinguish between unhealthy & healthy running containers, closes [#43](https://github.com/mrjackwills/oxker/issues/43), [de876818](https://github.com/mrjackwills/oxker/commit/de8768181631c6d961ce0e4dacb50c2ed02abc36)
|
||||
+ filter containers, use `F1` or `/` to enter filter mode, closes [#37](https://github.com/mrjackwills/oxker/issues/37), thanks to [MohammadShabaniSBU](https://github.com/MohammadShabaniSBU) for the original PR, [d5d8a0db](https://github.com/mrjackwills/oxker/commit/d5d8a0dbc5437ff3b17f34b9dbb9589bb56b4a3e), [7ee1f06f](https://github.com/mrjackwills/oxker/commit/7ee1f06f804683e3395953a02138d4e9da115ea9)
|
||||
|
||||
### Fixes
|
||||
+ log_sanitizer `raw()` & `remove_ansi()` now functioning as intended, [0dc98dfc](https://github.com/mrjackwills/oxker/commit/0dc98dfc8113869b81be9d697ca77418c919e4bf)
|
||||
+ Dockerfile command use uppercase, [068e4025](https://github.com/mrjackwills/oxker/commit/068e4025a5d6049a9a6951a0480a6bdef7379f88)
|
||||
+ heading section help margin, [0e927aae](https://github.com/mrjackwills/oxker/commit/0e927aae178c1d8f60561b93607a26d45a1d9331)
|
||||
+ install.sh use curl, [197a031b](https://github.com/mrjackwills/oxker/commit/197a031b8cf356f49f08e04472d0d1c489699415)
|
||||
|
||||
### Tests
|
||||
+ fix layout tests with new left alignment, [dfced564](https://github.com/mrjackwills/oxker/commit/dfced564278eafdbb8a5b95badbae3a7c4bf87b3)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.4'>v0.6.4</a>
|
||||
### 2024-05-25
|
||||
|
||||
### Chores
|
||||
+ Dependencies updated, [51fdd26b](https://github.com/mrjackwills/oxker/commit/51fdd26be5b3166bcff5c26ece6d6ec0d893381e), [c1be658b](https://github.com/mrjackwills/oxker/commit/c1be658b8cc4786a9a7f2e0a88568019b3995c14)
|
||||
|
||||
### Docs
|
||||
+ exec mode "not available on Windows", in both README.md and help panel, [df449a85](https://github.com/mrjackwills/oxker/commit/df449a85376bbeec87215952d6a9196721f7132e)
|
||||
|
||||
### Fixes
|
||||
+ closes [#36](https://github.com/mrjackwills/oxker/issues/36) Double key strokes on Windows, [9b7d575a](https://github.com/mrjackwills/oxker/commit/9b7d575a76398cbe19e17f6494baf802dbb512b9)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.3'>v0.6.3</a>
|
||||
### 2024-05-07
|
||||
|
||||
### Chores
|
||||
+ Dependencies updated, [07e293ac](https://github.com/mrjackwills/oxker/commit/07e293ac2ce2e7deb5735154fcdb24ef83a19b67), [27d72c54](https://github.com/mrjackwills/oxker/commit/27d72c547e738f6816cd4b353ac881e454a0be70)
|
||||
|
||||
### Features
|
||||
+ Allow closing dialogs with `Escape`, thanks [JCQuintas](https://github.com/JCQuintas), [0e4c3cea](https://github.com/mrjackwills/oxker/commit/0e4c3ceab933458d40b54d5fcff7e6cf7a3ab315)
|
||||
|
||||
### Fixes
|
||||
+ correct header display when terminal width changes, [4628803b](https://github.com/mrjackwills/oxker/commit/4628803b2b9fe63522d033b192763ed6ff5b57dd)
|
||||
|
||||
### Refactors
|
||||
+ use tokio CancellationToken, [0631a73e](https://github.com/mrjackwills/oxker/commit/0631a73ec27530f8fcc88988a0a02ca75e32c5ba)
|
||||
+ impl AsyncTTY, [bf33776e](https://github.com/mrjackwills/oxker/commit/bf33776e9a61684032a80d22d995ba7e0446620e)
|
||||
|
||||
### Tests
|
||||
+ reduced header section test, [aa094740](https://github.com/mrjackwills/oxker/commit/aa0947405393db2c306e86986183514cbc0f5a75)
|
||||
+ test_draw_blocks_help() with add esc text, [ff839af4](https://github.com/mrjackwills/oxker/commit/ff839af4ef68193149d6456e70fee189228c4a44)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.2'>v0.6.2</a>
|
||||
### 2024-03-31
|
||||
|
||||
### Chores
|
||||
+ .devcontainer updated, [82a7f84e](https://github.com/mrjackwills/oxker/commit/82a7f84ed94a9bad6cc5fe078bfadbda65c8ea8f)
|
||||
+ Rust 1.77 linting, [dfd4948d](https://github.com/mrjackwills/oxker/commit/dfd4948d9c43cfbffb091c303d3cb2a05522047a)
|
||||
+ platform.sh formatted, [7953e68f](https://github.com/mrjackwills/oxker/commit/7953e68f3067ac3c9d4fe67e9f934c25036c0833)
|
||||
+ dependencies updated, [8e6c3ca6](https://github.com/mrjackwills/oxker/commit/8e6c3ca6d83768f043923ccf6836397beaaae639)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.1'>v0.6.1</a>
|
||||
### 2024-02-14
|
||||
|
||||
### Chores
|
||||
+ create_release v0.5.5, [616338b7](https://github.com/mrjackwills/oxker/commit/616338b7107036e968f51c3ff80739f9ffb40fbd)
|
||||
+ update dependencies, [10180d2e](https://github.com/mrjackwills/oxker/commit/10180d2e0817c00a198e27f7d71080c502639a6b)
|
||||
+ update to ratatui v0.26.0, [d33dce3e](https://github.com/mrjackwills/oxker/commit/d33dce3eec4c19cc3c3668dab77f7d25d6970c3c)
|
||||
+ GitHub workflow dependency bump, [0314eac9](https://github.com/mrjackwills/oxker/commit/0314eac9df6cf9fea1943dcd06bd6a0b27131c16)
|
||||
|
||||
### Docs
|
||||
+ screenshot updated, [fe5ec4f5](https://github.com/mrjackwills/oxker/commit/fe5ec4f5dd25f11817be37f3f1867a6a2b0afc42)
|
||||
|
||||
### Fixes
|
||||
+ ports all listed in white, [d3b23585](https://github.com/mrjackwills/oxker/commit/d3b23585b38045eb3bc827367eca90eb7f7a7dd5)
|
||||
+ use long container name in delete popup, [6202b7bb](https://github.com/mrjackwills/oxker/commit/6202b7bbfdfb04a94959b5143dac3f1aa59cd336)
|
||||
+ memory display, closes [#33](https://github.com/mrjackwills/oxker/issues/33), [a182d40a](https://github.com/mrjackwills/oxker/commit/a182d40a7463164ef5dcac379d1a1768d77209d2)
|
||||
|
||||
### Refactors
|
||||
+ use &[T] instead of &Vec<T>, [76cd08ab](https://github.com/mrjackwills/oxker/commit/76cd08ab2f98687a866a6bbb4fa93bbdedaa7699), [1f62bb50](https://github.com/mrjackwills/oxker/commit/1f62bb50210f2d66bb7215e42e8b21a3c1a6ec06)
|
||||
+ draw_block constraints into consts, [0436ff1b](https://github.com/mrjackwills/oxker/commit/0436ff1b7356c80532048c7d497c66d331092b01)
|
||||
|
||||
### Tests
|
||||
+ update port test with new colour, [f74ae3f5](https://github.com/mrjackwills/oxker/commit/f74ae3f5c34d74b78822078291fed401427c4cba)
|
||||
+ color match tests updated, [5b287416](https://github.com/mrjackwills/oxker/commit/5b287416315942b19c62f8c66348ce28462d894c)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.0'>v0.6.0</a>
|
||||
### 2024-01-18
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [53b4bafb](https://github.com/mrjackwills/oxker/commit/53b4bafbe53312fe41608ddf33e865d474222aaa), [58ef1516](https://github.com/mrjackwills/oxker/commit/58ef151600e362048a607c8ae61a5edfe80ab1dd), [b6fd3502](https://github.com/mrjackwills/oxker/commit/b6fd35022a99ec0e982ddb154b0450d49c4840e9), [0438c108](https://github.com/mrjackwills/oxker/commit/0438c108bdd9815d7eae1b89c47c4e6438f358d6)
|
||||
+ files formatted, [1806165c](https://github.com/mrjackwills/oxker/commit/1806165c3e266876b2d1806f7b662d09705f3aad)
|
||||
+ create_release.sh check for unused lint, [d0b27211](https://github.com/mrjackwills/oxker/commit/d0b27211928f93f8455e1ee5a6a6485c6a21d382)
|
||||
|
||||
### Docs
|
||||
= Readme updated, screenshot added, [7561a934](https://github.com/mrjackwills/oxker/commit/7561a93415c1e1f596b15edba95e7b32a939cd90), [4069e557](https://github.com/mrjackwills/oxker/commit/4069e5572f81cb689dbb9f735db919e4636cdccc)
|
||||
|
||||
### Features
|
||||
+ Ports section added, closes [#21](https://github.com/mrjackwills/oxker/issues/21), [65a1afcb](https://github.com/mrjackwills/oxker/commit/65a1afcb0605604ede350a5630c775f94ebb74ee), [7a096a65](https://github.com/mrjackwills/oxker/commit/7a096a65c40924021fe643fe0aa1067095832df9)
|
||||
|
||||
### Fixes
|
||||
+ sort arrow now on left of header, [40ddcb72](https://github.com/mrjackwills/oxker/commit/40ddcb727d2c1758d6dd26a58507b85b219f51e2)
|
||||
|
||||
### Refactors
|
||||
+ rename string_wrapper > unit_struct, [27cf53e4](https://github.com/mrjackwills/oxker/commit/27cf53e41f8b379f606c1c27620ee08e79bac57e)
|
||||
|
||||
### Tests
|
||||
+ Finally have tests, currently for layout and associated methods, at the moment running the tests will not interfere with any running Docker containers, [4bcf77db](https://github.com/mrjackwills/oxker/commit/4bcf77db776a36e0a8151ecfbda722a66c4ba46c)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.5.0'>v0.5.0</a>
|
||||
### 2024-01-05
|
||||
|
||||
### Chores
|
||||
+ .devcontainer updated, [2313618e](https://github.com/mrjackwills/oxker/commit/2313618eb1493ce41d70847b888c32b65fdc40ea), [5af6b8bc](https://github.com/mrjackwills/oxker/commit/5af6b8bcd31c3c38ff5a5799c76dc1cbe1167763), [9b0b6b10](https://github.com/mrjackwills/oxker/commit/9b0b6b10c3a0c1d5095490cfd3cda18d252f38f5)
|
||||
+ alpine version bump, [061de032](https://github.com/mrjackwills/oxker/commit/061de032dad935c56c6caab419ecb5c9bbac4c7e)
|
||||
+ dependencies updated, [0890991f](https://github.com/mrjackwills/oxker/commit/0890991ff1a239fe2d556a0c4eac6ae05beb9b50), [0a7b266b](https://github.com/mrjackwills/oxker/commit/0a7b266b2a358a4788ae877ca8a97f08eac4eef2), [333621f1](https://github.com/mrjackwills/oxker/commit/333621f1a7321c1fdf73fd35dd7f3ab165a9dc64), [3e51889c](https://github.com/mrjackwills/oxker/commit/3e51889cd8a552b1da463ae6a40d5de6eec188f5), [a179bb6f](https://github.com/mrjackwills/oxker/commit/a179bb6f6a7e076269fa830f56c0d4a31cf8488a)
|
||||
+ file formatting, [eb5e74ae](https://github.com/mrjackwills/oxker/commit/eb5e74ae67d815bf49f241d2baf319e41cf9adf8)
|
||||
+ Rust 1.75.0 linting, [81be75f2](https://github.com/mrjackwills/oxker/commit/81be75f27fd32a59ebff57e44c5022ff862df84b)
|
||||
|
||||
### Docs
|
||||
+ screenshot updated, [0231d1bd](https://github.com/mrjackwills/oxker/commit/0231d1bdcda304300d289243a95044ab3bdce85c)
|
||||
+ comment typo, [0ad1ec9d](https://github.com/mrjackwills/oxker/commit/0ad1ec9d85d6f0cac743b4421d0ad03432c9d717)
|
||||
|
||||
### Features
|
||||
+ re-arrange columns, container name is now the first column, added a ContainerName & ContainerImage struct via `string_wrapper` macro, closes [#32](https://github.com/mrjackwills/oxker/issues/32), [e936bb4b](https://github.com/mrjackwills/oxker/commit/e936bb4b78980d0e34a1ef5e9f6f82a9ed0ddc7f)
|
||||
|
||||
### Fixes
|
||||
+ Docker Commands hidden, [4301e470](https://github.com/mrjackwills/oxker/commit/4301e4709f99fc23ee438bf345b0dc698a05dc4e)
|
||||
+ .gitattributes, [1234ea53](https://github.com/mrjackwills/oxker/commit/1234ea53897b2ed6ada0eb18cd81b8783a5dc5f5)
|
||||
|
||||
### Refactors
|
||||
+ GitHub workflow action improved, [04b66af2](https://github.com/mrjackwills/oxker/commit/04b66af2b60c96cfbece0b13109e30b08ef35cc4)
|
||||
+ sort_containers, [ccf8b55a](https://github.com/mrjackwills/oxker/commit/ccf8b55a7495982f72b4fb3af6e11a9bd7465216)
|
||||
+ string_wrapper .get() return `&str`, [a722731c](https://github.com/mrjackwills/oxker/commit/a722731c6a77e00d1fb13967b51400aa34e72213)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.4.0'>v0.4.0</a>
|
||||
### 2023-11-21
|
||||
|
||||
### Chores
|
||||
+ workflow dependencies updated, [6a4cf649](https://github.com/mrjackwills/oxker/commit/6a4cf6490d08b976734e2bc8186d94c095700558)
|
||||
+ dependencies updated, [e301b518](https://github.com/mrjackwills/oxker/commit/e301b51891e03ea40b2f904583119da3bc4daf53), [81d5b326](https://github.com/mrjackwills/oxker/commit/81d5b326db8881263f2c9072e1426948e41b4a0f), [294cc268](https://github.com/mrjackwills/oxker/commit/294cc2684f42daab9d51601e235a384f55617678)
|
||||
+ lints moved from main.rs to Cargo.toml, [2de76e2f](https://github.com/mrjackwills/oxker/commit/2de76e2f358be9c1500ca3dc4f9df0979ed8ed28)
|
||||
+ .devcontainer updated, [37d2ee91](https://github.com/mrjackwills/oxker/commit/37d2ee915625806dd11c2cc816a892aae12a777c)
|
||||
|
||||
### Features
|
||||
+ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key, closes [#28](https://github.com/mrjackwills/oxker/issues/28), [c8077bca](https://github.com/mrjackwills/oxker/commit/c8077bca0b673478cfbb417e677a885136ba9eff), [0e5ee143](https://github.com/mrjackwills/oxker/commit/0e5ee143b008c9d0ee0b681231a1568be227150b), [0e5ee143](https://github.com/mrjackwills/oxker/commit/0e5ee143b008c9d0ee0b681231a1568be227150b)
|
||||
+ Export logs feature, press `s` to save logs, use `--save-dir` cli-arg to customise output location, closes [#1](https://github.com/mrjackwills/oxker/issues/1), [a15da5ed](https://github.com/mrjackwills/oxker/commit/a15da5ed43d07852504a4dd1884a189e3f5b9d84)
|
||||
|
||||
### Fixes
|
||||
+ GitHub workflow, cargo publish before create release, [ae4ce3b5](https://github.com/mrjackwills/oxker/commit/ae4ce3b549c40cc8bd713f375f030b185179a6e2)
|
||||
+ sorted created_at clash, closes [#22](https://github.com/mrjackwills/oxker/issues/22), [3a648939](https://github.com/mrjackwills/oxker/commit/3a6489396e87702ce94b349a7f47028ece7922f6)
|
||||
+ `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [77fbaa8b](https://github.com/mrjackwills/oxker/commit/77fbaa8b1669286369b6ec1edd80220c808b628f)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.3.3'>v0.3.3</a>
|
||||
### 2023-10-21
|
||||
|
||||
### Chores
|
||||
+ docker-compose Alpine bump, [d46c425f](https://github.com/mrjackwills/oxker/commit/d46c425fa29f3c1d27bd57764748bae7e0b82f69)
|
||||
+ dependencies updated, [e6eecbbd](https://github.com/mrjackwills/oxker/commit/e6eecbbdce9c0ccff42aa8806dddb6e3364f990c), [ec93115e](https://github.com/mrjackwills/oxker/commit/ec93115ece83002fa127f3358f573319e29357e1), [b36daa5a](https://github.com/mrjackwills/oxker/commit/b36daa5aeaa354b6c4f45b7ae67ac1a6345ea1c0), [9c0de1f0](https://github.com/mrjackwills/oxker/commit/9c0de1f0feff3165d0f5b6cb5dda843c124bcfa4), [6dd953df](https://github.com/mrjackwills/oxker/commit/6dd953df458096aee5914411ce40e46c3f600ede)
|
||||
+ Rust 1.73 linting, [21234c66](https://github.com/mrjackwills/oxker/commit/21234c66c3935330ccd58543dd3a915a293ac776)
|
||||
|
||||
### Docs
|
||||
+ README.md updated, [3fd3915b](https://github.com/mrjackwills/oxker/commit/3fd3915b3e929742d8007109fd4c7b4a345eb0fa)
|
||||
|
||||
### Refactors
|
||||
+ LogsTZ from `&str`, [44f581f5](https://github.com/mrjackwills/oxker/commit/44f581f5b3652cc4e623fe145141878754dca292)
|
||||
+ from string impl, [ca79893d](https://github.com/mrjackwills/oxker/commit/ca79893df5f05ebf445ce194d578cb8213c9755e)
|
||||
+ env handling, [18c3ed43](https://github.com/mrjackwills/oxker/commit/18c3ed43376a8b5e2d285d1b34a9f96843357d53)
|
||||
+ `parse_args/mod.rs` > `parse_args.rs`, [a6ff4124](https://github.com/mrjackwills/oxker/commit/a6ff4124319ed17d3f1c46c916418f850ef1d3b0)
|
||||
+ set_info_box take `&str`, [faeaca0c](https://github.com/mrjackwills/oxker/commit/faeaca0cd1bb243c7f4a7112b928be776b877ca1)
|
||||
+ GitHub action use concurrency matrix, re-roder workflow, [85f1982f](https://github.com/mrjackwills/oxker/commit/85f1982f4066bfdbc764ab7b88588eded6a17f96)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.3.2'>v0.3.2</a>
|
||||
### 2023-08-28
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [8ce5a187](https://github.com/mrjackwills/oxker/commit/8ce5a1877a8c56d9bbab560c97e2596ea87cc4c0), [94a20584](https://github.com/mrjackwills/oxker/commit/94a20584e6ef0701c9f36838b0dfbcd911698dbe), [29e02e0d](https://github.com/mrjackwills/oxker/commit/29e02e0d1faae4a836c7e5cfd0d791338ff586e3), [8e4c2e68](https://github.com/mrjackwills/oxker/commit/8e4c2e686761df56920df2267b765ab1297c9972)
|
||||
+ `_typos.toml` added, [84ba1020](https://github.com/mrjackwills/oxker/commit/84ba1020939606abf4a287cbd1de1f3a10d3f0c0)
|
||||
|
||||
### Features
|
||||
+ Custom hostname. `oxker` will use `$DOCKER_HOST` env if set, or one can use the cli argument `--host`, which takes priority over the `$DOCKER_HOST`, closes [#30](https://github.com/mrjackwills/oxker/issues/30), [10950787](https://github.com/mrjackwills/oxker/commit/10950787649d2b66fc1e8cd8b85526df51479857)
|
||||
|
||||
### Refactors
|
||||
+ `set_error()` takes `gui_state` and error enum, to make sure app_data & gui_state is in sync [62c78dfa](https://github.com/mrjackwills/oxker/commit/62c78dfaa50a8d8c084f7fbf7e203b50aaa731ae)
|
||||
+ `fn loading_spin` doesn't need to be async, [2e27462d](https://github.com/mrjackwills/oxker/commit/2e27462d1b3f0bdb27d7646511e36d0c9af07f3e)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.3.1'>v0.3.1</a>
|
||||
### 2023-06-04
|
||||
|
||||
### Chores
|
||||
+ github workflow ubuntu latest, build for x86 musl, [4fa841e6](https://github.com/mrjackwills/oxker/commit/4fa841e6e74e3e10e3d3e82eac1a1ca1338814cf)
|
||||
+ dependencies updated, [0caa92f6](https://github.com/mrjackwills/oxker/commit/0caa92f6a4728d50d8b2d8f15d96a21112732ec5), [1fd1dfc7](https://github.com/mrjackwills/oxker/commit/1fd1dfc75d6fa4e84451ebc845b9e1c730381f41)
|
||||
+ `Spans` -> `Line`, ratatui 0.21 update, [4679ddc8](https://github.com/mrjackwills/oxker/commit/4679ddc885a9b35c901f3600b63fd9e86118264c), [0d37ac55](https://github.com/mrjackwills/oxker/commit/0d37ac55018038363e5f92dc4215996f8cff7b2e)
|
||||
+ `create_release.sh` updated, [7dec5f14](https://github.com/mrjackwills/oxker/commit/7dec5f14a381d237c5e72fbf9551bcf398f93f3e)
|
||||
|
||||
### Fixes
|
||||
+ workflow additional image fix, closes [#29](https://github.com/mrjackwills/oxker/issues/29), [47cda44b](https://github.com/mrjackwills/oxker/commit/47cda44b8213cfb8c3807df6c43e3f5dc2452b57)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.3.0'>v0.3.0</a>
|
||||
### 2023-03-30
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [7a9bdc96](https://github.com/mrjackwills/oxker/commit/7a9bdc9699594532e17a33e044ca0678693c8d3f), [58e03a75](https://github.com/mrjackwills/oxker/commit/58e03a750fe89b914b9069cb0c6c02a3d0929439), [b246e8c2](https://github.com/mrjackwills/oxker/commit/b246e8c25af0c5136953afca7c694cda66550d9b)
|
||||
|
||||
### Docs
|
||||
+ README.md and screenshot updated, [73ab7580](https://github.com/mrjackwills/oxker/commit/73ab7580c61dd59c59f10872629111360afb9033)
|
||||
|
||||
### Features
|
||||
+ Ability to delete a container, be warned, as this will force delete, closes [#27](https://github.com/mrjackwills/oxker/issues/27), [937202fe](https://github.com/mrjackwills/oxker/commit/937202fe34d1692693c62dd1a7ad19db37651233), [b25f8b18](https://github.com/mrjackwills/oxker/commit/b25f8b18f4f2acd5c9af4a1d40655761d1bd720e)
|
||||
+ Publish images to `ghcr.io` as well as Docker Hub, and correctly tag images with `latest` and the current sermver, [cb1271cf](https://github.com/mrjackwills/oxker/commit/cb1271cf7f21c898020481ad85914a3dcc83ec93)
|
||||
+ Replace `tui-rs` with [ratatui](https://github.com/tui-rs-revival/ratatui), [d431f850](https://github.com/mrjackwills/oxker/commit/d431f850219b28af2bc45f3b6917377604596a40)
|
||||
|
||||
### Fixes
|
||||
+ out of bound bug in `heading_bar()`, [b9c125da](https://github.com/mrjackwills/oxker/commit/b9c125da46fe0eb4aae15c354d87ac824e9cb83a)
|
||||
+ `-d` arg error text updated, [e0b49be8](https://github.com/mrjackwills/oxker/commit/e0b49be84062abdfcb636418f57043fad37d06ec)
|
||||
|
||||
### Refactors
|
||||
+ `popup()` use `saturating_x()` rather than `checked_x()`, [d628e802](https://github.com/mrjackwills/oxker/commit/d628e8029942916053b3b7e72d363b1290fc5711)
|
||||
+ button_item() include brackets, [7c92ffef](https://github.com/mrjackwills/oxker/commit/7c92ffef7da20143a31706a310b5e6f2c3e0554f)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.5'>v0.2.5</a>
|
||||
### 2023-03-13
|
||||
|
||||
### Chores
|
||||
+ Rust 1.68.0 clippy linting, [5582c454](https://github.com/mrjackwills/oxker/commit/5582c45403413d3355bbcd629cfad559296f5e5b)
|
||||
+ devcontainer use sparse protocol index, [20b79e9c](https://github.com/mrjackwills/oxker/commit/20b79e9cd5bf75bb253158c0b590284139e0291d)
|
||||
+ dependencies updated, [0c07d4b4](https://github.com/mrjackwills/oxker/commit/0c07d4b40607a0eba003b6dcd0345ec0543c6264), [601a73d2](https://github.com/mrjackwills/oxker/commit/601a73d2c830043a25d64922c4d4aa38f8801912), [5aaa3c1a](https://github.com/mrjackwills/oxker/commit/5aaa3c1ab08b0c85df9bfce18a3e60206556fa58), [7a156303](https://github.com/mrjackwills/oxker/commit/7a1563030e48499da7f41033673c70deefe3de8a), [45715775](https://github.com/mrjackwills/oxker/commit/457157755baa1f9e9cfef9315a7940c357b0953d)
|
||||
|
||||
### Features
|
||||
+ increase mpsc channel size from 16 to 32 messages, [924f14e9](https://github.com/mrjackwills/oxker/commit/924f14e998f79f731447a2eded038eab51f2e932)
|
||||
+ KeyEvents send modifier, so can quit on `ctrl + c`, [598f67c6](https://github.com/mrjackwills/oxker/commit/598f67c6f6a8713102bcc415f0409911763bb914)
|
||||
+ only send relevant mouse events to input handler, [507660d8](https://github.com/mrjackwills/oxker/commit/507660d835d0beaa8cd021110401ecc58c0613c6)
|
||||
|
||||
### Fixes
|
||||
+ GitHub workflow on SEMEVR tag only, [14077386](https://github.com/mrjackwills/oxker/commit/140773865165bf006e74f9d436fc744220f5eae7)
|
||||
|
||||
### Refactors
|
||||
+ replace `unwrap_or(())` with `.ok()`, [8ba37a16](https://github.com/mrjackwills/oxker/commit/8ba37a165bb89277ab957194da6464bdb35be2e6)
|
||||
+ use `unwrap_or_default()`, [79de92c3](https://github.com/mrjackwills/oxker/commit/79de92c3921702417bb2df1f44939a7b09cb7fa0)
|
||||
+ Result return, [d9f0bd55](https://github.com/mrjackwills/oxker/commit/d9f0bd5566e27218b8c8eaba6ece237907771c1d)
|
||||
|
||||
### Reverts
|
||||
+ temporary devcontainer buildkit fix removed, [d1497a44](https://github.com/mrjackwills/oxker/commit/d1497a4451f4de54d3cc26c5a3957cd636c29118)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.4'>v0.2.4</a>
|
||||
### 2023-03-02
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [aac3ef2b](https://github.com/mrjackwills/oxker/commit/aac3ef2b1def3345d749d813d9b76020d6b5e5ca), [4723be7f](https://github.com/mrjackwills/oxker/commit/4723be7fb2eb101024bb9d5a514e2c6cc51eb6f6), [c69ab4f7](https://github.com/mrjackwills/oxker/commit/c69ab4f7c3b873f25ea46958add37be78d23e9cf), [ba643786](https://github.com/mrjackwills/oxker/commit/ba6437862dae0f422660a602aeabd6217d023fac), [2bb4c338](https://github.com/mrjackwills/oxker/commit/2bb4c338903e09856053894d9646307e31d32f1c)
|
||||
+ dev container install x86 musl toolchain, [e650034d](https://github.com/mrjackwills/oxker/commit/e650034d50f01a7598876d4f2887df691700e06a)
|
||||
|
||||
### Docs
|
||||
+ typos removed, [23ad9a5f](https://github.com/mrjackwills/oxker/commit/23ad9a5fb3cacf3fb8cb70c65ca9133ed9949e45), [cebb975c](https://github.com/mrjackwills/oxker/commit/cebb975cb82f653407ec801fd8c726ca6ed68289), [fdc67c92](https://github.com/mrjackwills/oxker/commit/fdc67c9249a239bac97a78b20c9378472865209c)
|
||||
+ comments improved, [ec962295](https://github.com/mrjackwills/oxker/commit/ec962295a8789ff8010604e974969bf618ea7108)
|
||||
|
||||
### Features
|
||||
+ mouse capture is now more specific, should have substantial performance impact, 10x reduction in cpu usage when mouse is moved observed, as well as fixing intermittent mouse events output bug, [0a1b5311](https://github.com/mrjackwills/oxker/commit/0a1b53111627206cc7436589e5b7212e1b72edb8), [93f7c07f](https://github.com/mrjackwills/oxker/commit/93f7c07f708885f8870da5dfb6d57c62f93c9c78), [c74f6c11](https://github.com/mrjackwills/oxker/commit/c74f6c1179b5f62989eb74f395a56b43a8781b03)
|
||||
+ improve the styling of the help information popup, [28de74b8](https://github.com/mrjackwills/oxker/commit/28de74b866f07c8543e46be3cab929eff28953fd)
|
||||
+ use checked_sub & checked_div for bounds checks, [72279e26](https://github.com/mrjackwills/oxker/commit/72279e26ae996353c95a75527f704bac1e4bcf4d)
|
||||
|
||||
### Fixes
|
||||
+ correctly set gui error, [340893a8](https://github.com/mrjackwills/oxker/commit/340893a860e99ec4029d12613f2a6de3cb7b47e2)
|
||||
|
||||
### Refactors
|
||||
+ dead code removed, [b8f5792d](https://github.com/mrjackwills/oxker/commit/b8f5792d1865d3a398cd7f23aa9473a55dc6ea44)
|
||||
+ improve the get_width function, [04c26fe8](https://github.com/mrjackwills/oxker/commit/04c26fe8fc7c79506921b9cff42825b1ee132737)
|
||||
+ place ui methods into a Ui struct, [3437df59](https://github.com/mrjackwills/oxker/commit/3437df59884f084624031fceb34ea3012a8e2251)
|
||||
+ get_horizontal/vertical constraints into single method, [e8f5cf9c](https://github.com/mrjackwills/oxker/commit/e8f5cf9c6f8cd5f807a05fb61e31d7cd1426486f)
|
||||
+ docker update_everything variables, [074cb957](https://github.com/mrjackwills/oxker/commit/074cb957f274675a468f08fecb1c43ff7453217d)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.3'>v0.2.3</a>
|
||||
### 2023-02-04
|
||||
|
||||
### Fixes
|
||||
+ Container runner `FROM scratch` (missing from v0.2.2 D'oh), this now should actually reduce Docker image size by ~60%, [0bd317b7](https://github.com/mrjackwills/oxker/commit/0bd317b7ce6f9f42a614c488099b5fc7a14d91c7)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.2'>v0.2.2</a>
|
||||
### 2023-02-04
|
||||
|
||||
### Chores
|
||||
+ devcontainer.json updated, typos-cli installed, temporary(?) buildkit fix, [3c6a8db6](https://github.com/mrjackwills/oxker/commit/3c6a8db6ef74d499b49fabe8912785cac16d9c4b)
|
||||
+ create_release.sh check for typos, [310a63f4](https://github.com/mrjackwills/oxker/commit/310a63f4cabaa374797a7e4ed0d7fd1f5e79c8fe)
|
||||
|
||||
### Docs
|
||||
+ AUR install instructions, thanks [orhun](https://github.com/orhun), [c5aa346b](https://github.com/mrjackwills/oxker/commit/c5aa346bca139cc5ece1f4127293977924d16fca)
|
||||
+ typos fixes, thanks [kianmeng](https://github.com/kianmeng), [5052d7ab](https://github.com/mrjackwills/oxker/commit/5052d7ab0a156c43cadbd922c0019b284f24943a)
|
||||
+ Readme.md styling tweak, [310a63f4](https://github.com/mrjackwills/oxker/commit/310a63f4cabaa374797a7e4ed0d7fd1f5e79c8fe)
|
||||
+ Contributing guide, [5aaa00d6](https://github.com/mrjackwills/oxker/commit/5aaa00d6a3c58d98cb250b7b14584238df02961c), [a44b15f7](https://github.com/mrjackwills/oxker/commit/a44b15f76088561a0e272d4e7456197c2aaabdb4)
|
||||
|
||||
### Features
|
||||
+ Use a scratch container for the docker image, should reduce image size by around 60%. This checks for the ENV `OXKER_RUNTIME=container`, which is automatically set by the docker image, [17b71b6b](https://github.com/mrjackwills/oxker/commit/17b71b6b41f6a98a0f92277f40a88f4b1b8a1328)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.1'>v0.2.1</a>
|
||||
### 2023-01-29
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [c129f474](https://github.com/mrjackwills/oxker/commit/c129f474fe2976454b1868d00e8d7d99b87ec23b), [9788b8af](https://github.com/mrjackwills/oxker/commit/9788b8afd98e59b1d4412a8adc54b34d2c5671fd), [2ab88eb2](https://github.com/mrjackwills/oxker/commit/2ab88eb26e9bbbc4dad4651256d8d9b044ea3272)
|
||||
|
||||
### Docs
|
||||
+ comment typo, [10255791](https://github.com/mrjackwills/oxker/commit/1025579138f11e4987263c7bfe936c4c8542f8b3)
|
||||
|
||||
### Fixes
|
||||
+ deadlock on draw logs when no containers found, [68e444bf](https://github.com/mrjackwills/oxker/commit/68e444bfc393eb46bac2b99eb57697bb9b0451af)
|
||||
+ github workflow release on main only (with semver tag), [e4ca41df](https://github.com/mrjackwills/oxker/commit/e4ca41dfd8ec3acae202a2d2464b8e18f5c5bdd5), [749ec712](https://github.com/mrjackwills/oxker/commit/749ec712f07cff2c941aed6726c56bdbd5cb8d2c)
|
||||
|
||||
### Refactors
|
||||
+ major refactor of internal data handling, [b4488e4b](https://github.com/mrjackwills/oxker/commit/b4488e4bdb0252f5c5680cee6a46427f22a282ab)
|
||||
+ needless (double) referencing removed, [a174dafe](https://github.com/mrjackwills/oxker/commit/a174dafe1b05908735680a874dc551a86da24777)
|
||||
+ app_data methods re-ordered & renamed, [c0bb5355](https://github.com/mrjackwills/oxker/commit/c0bb5355d6a5d352260655110ce3d5ab695acda9)
|
||||
|
||||
### Reverts
|
||||
+ is_running AtomicBool back to SeqCst, [c4d80061](https://github.com/mrjackwills/oxker/commit/c4d80061dab94afd08d4d793dc147f878c965ad6)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.0'>v0.2.0</a>
|
||||
### 2023-01-21
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [8cd199db](https://github.com/mrjackwills/oxker/commit/8cd199db49186fad6ce432bb277e3a10f0a08d34), [d880b829](https://github.com/mrjackwills/oxker/commit/d880b829c123dbe57deccadef97810e45c083737), [66d57c99](https://github.com/mrjackwills/oxker/commit/66d57c99558ca14d9593d6dbfd5b0e8e5d59055d), [33f93749](https://github.com/mrjackwills/oxker/commit/33f9374908942f4a3b90be227fad94ca353cf351), [007d5d83](https://github.com/mrjackwills/oxker/commit/007d5d83d7f1b93e1e78777a4417b2740db706bd)
|
||||
+ create_release.sh typos, [9a27d46a](https://github.com/mrjackwills/oxker/commit/9a27d46a044452080144ee1367dc95886b10abf8)
|
||||
+ dev container post create install cross, [2d253f03](https://github.com/mrjackwills/oxker/commit/2d253f034182741d434e4bac12317f24221d0d4a)
|
||||
|
||||
### Features
|
||||
**all potentially considered breaking changes**
|
||||
+ store Logs in own struct, use a hashset to track timestamps, hopefully closes [#11](https://github.com/mrjackwills/oxker/issues/11), [657ea2d7](https://github.com/mrjackwills/oxker/commit/657ea2d751a71f05b17547b47c492d5676817336)
|
||||
+ Spawn docker commands into own thread, can now execute multiple docker commands at the same time, [9ec43e12](https://github.com/mrjackwills/oxker/commit/9ec43e124a62a80f4e78acba85fc3af5980ce260)
|
||||
+ align memory columns correctly, minimum byte display value now `0.00 kB`, rather than `0 B`, closes [#20](https://github.com/mrjackwills/oxker/issues/20), [bd7dfcd2](https://github.com/mrjackwills/oxker/commit/bd7dfcd2c512a527d66a1388f90006988a487186), [51c58001](https://github.com/mrjackwills/oxker/commit/51c580010a24de2427373795803936d498dc8cee)
|
||||
|
||||
### Refactors
|
||||
+ main.rs tidy up, [97b89349](https://github.com/mrjackwills/oxker/commit/97b89349dc2de275ca514a1e6420255a63d775e8)
|
||||
+ derive Default for GuiState, [9dcd0509](https://github.com/mrjackwills/oxker/commit/9dcd0509efeb464f58fb53d813bd78de2447949d)
|
||||
+ param reduction, AtomicBool to Relaxed, [0350293d](https://github.com/mrjackwills/oxker/commit/0350293de3c00c6e5e5d787b7596bb3413d1cda1)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.11'>v0.1.11</a>
|
||||
### 2023-01-03
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [9b09146a](https://github.com/mrjackwills/oxker/commit/9b09146aadae5727a5fee4de5fe0c1d70c581c22)
|
||||
|
||||
### Features
|
||||
+ `install.sh` script added, for automated platform selection, download, and installation, [7a42eba6](https://github.com/mrjackwills/oxker/commit/7a42eba6b0968314af40ff87bcc42d288f6860bc), [e0703b76](https://github.com/mrjackwills/oxker/commit/e0703b76a1a28cfe266f130a7f7dec92f1b5ad58)
|
||||
|
||||
### Fixes
|
||||
+ If a sort order is set, sort containers on every `update_stats()` execution, [cfdea775](https://github.com/mrjackwills/oxker/commit/cfdea77594e48c8c20a4d6e6c7ea31c9181361a1)
|
||||
|
||||
### Refactors
|
||||
+ input sort executed in app_data struct `sort_by_header()`, [3cdc5fae](https://github.com/mrjackwills/oxker/commit/3cdc5fae02097628799209f371ae9292e513e76c)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.10'>v0.1.10</a>
|
||||
### 2022-12-25
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [1525b315](https://github.com/mrjackwills/oxker/commit/1525b3150293015c0fb2f2161da463b21ac2694c), [8d539ab1](https://github.com/mrjackwills/oxker/commit/8d539ab14809136d743c49d60779687fc8eeef6d), [1774217a](https://github.com/mrjackwills/oxker/commit/1774217a8a657d261397d213e5ecee667cf3b6b1)
|
||||
+ Rust 1.66 linting, [bf9dcac7](https://github.com/mrjackwills/oxker/commit/bf9dcac7045c0d2314df147ec2744a3ad886564b)
|
||||
|
||||
### Features
|
||||
+ Caching on github action, [a91c9aa4](https://github.com/mrjackwills/oxker/commit/a91c9aa45ffd5c998cd1b83d8e90d0912893c31f)
|
||||
|
||||
### Fixes
|
||||
+ comment typo, [7899b773](https://github.com/mrjackwills/oxker/commit/7899b773569fed86343a035d3023bf34297fdabb)
|
||||
|
||||
### Refactors
|
||||
+ remove_ansi() to single liner, [57c3a6c1](https://github.com/mrjackwills/oxker/commit/57c3a6c186b916faba24bf7b5cdbbda31d636a7e)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.9'>v0.1.9</a>
|
||||
### 2022-12-05
|
||||
|
||||
### Fixes
|
||||
+ disallow commands to be sent to a dockerised oxker container, closes [#19](https://github.com/mrjackwills/oxker/issues/19), [160b8021](https://github.com/mrjackwills/oxker/commit/160b8021b1de898064756b53c127d49b8096ce4d)
|
||||
+ if no container created time, use 0, instead of system_time(), [1adb61ce](https://github.com/mrjackwills/oxker/commit/1adb61ce3b029d4fcf51961958d483b2fae8825a)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.8'>v0.1.8</a>
|
||||
### 2022-12-05
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [e3aa4420](https://github.com/mrjackwills/oxker/commit/e3aa4420cb510df0381e311d37e768937070387a)
|
||||
+ docker-compose.yml alpine bump, [911c6596](https://github.com/mrjackwills/oxker/commit/911c6596684db4ccbe7a55aadd6f595a95f89bb0)
|
||||
+ github workflow use dtolnay/rust-toolchain, [57c18878](https://github.com/mrjackwills/oxker/commit/57c18878690477a05d7330112a65d1d58a07901e)
|
||||
|
||||
### Features
|
||||
+ Clicking a header now toggles between Ascending -> Descending -> Default. Use the containers created_time as the default order - maybe add created column in future version, closes [#18](https://github.com/mrjackwills/oxker/issues/18), [cf14ba49](https://github.com/mrjackwills/oxker/commit/cf14ba498987db587c0f5bef8a67cf4113ffcb1e), [d1de2914](https://github.com/mrjackwills/oxker/commit/d1de291473d8a1028f1936429832d3820d75df54)
|
||||
+ `-s` flag for showing the oxker container when executing the docker image, [c93870e5](https://github.com/mrjackwills/oxker/commit/c93870e5fbbc7df35c69d32e4460d2104e521e33)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.7'>v0.1.7</a>
|
||||
### 2022-11-13
|
||||
|
||||
### Chores
|
||||
+ update dependencies, closes [#17](https://github.com/mrjackwills/oxker/issues/17), [8a5d0ef8](https://github.com/mrjackwills/oxker/commit/8a5d0ef8376e3739dda5b0ed4c3e75e565deed45), [eadfc3d6](https://github.com/mrjackwills/oxker/commit/eadfc3d6c6896ecc8cff88c6a9e9c8b3e477c0cd)
|
||||
+ aggressive linting with Rust 1.65.0, [8f3a1513](https://github.com/mrjackwills/oxker/commit/8f3a15137155dc374e6b2822c9155c07d05d5e28)
|
||||
|
||||
### Docs
|
||||
+ README.md improved Download & Install section, and now available on [NixPkg](https://search.nixos.org/packages?channel=unstable&show=oxker&from=0&size=50&sort=relevance&type=packages&query=oxker), thanks [siph](https://github.com/siph), [67a9e183](https://github.com/mrjackwills/oxker/commit/67a9e183ca04199da758255075ff7e73061eb850)
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.6'>v0.1.6</a>
|
||||
### 2022-10-16
|
||||
|
||||
### Chores
|
||||
+ Cargo update, [c3e72ae7](https://github.com/mrjackwills/oxker/commit/c3e72ae7369a25d903f39e55a4349cb005671dd4),
|
||||
+ create_release.sh v0.1.0, [3c8d59c6](https://github.com/mrjackwills/oxker/commit/3c8d59c666bd4cda9ca54989b2f1b48bba17bc57),
|
||||
+ uuid updated to version 1.2, [438ad770](https://github.com/mrjackwills/oxker/commit/438ad770f4a5ecb5f4bbc308066ad9e808f66514),
|
||||
|
||||
### Fixes
|
||||
+ loading icon shifting error fix, also make icon white, closes [#15](https://github.com/mrjackwills/oxker/issues/15), [59797685](https://github.com/mrjackwills/oxker/commit/59797685dffa29752a48c98e6cf465884d6d9df6),
|
||||
|
||||
### Features
|
||||
+ Show container name in log panel title, closes [#16](https://github.com/mrjackwills/oxker/issues/16), [9cb0c414](https://github.com/mrjackwills/oxker/commit/9cb0c414afc284947fc2b8494504387e4e7edd87),
|
||||
+ use gui_state HashSet to keep track of application gui state, [9e9d5155](https://github.com/mrjackwills/oxker/commit/9e9d51559a13944622abf4fcbd3bd63766d11467),
|
||||
+ terminal.clear() after run_app finished, [67c49575](https://github.com/mrjackwills/oxker/commit/67c49575682cb271fac0998ff377a6504cd0bc86),
|
||||
|
||||
### Refactors
|
||||
+ CpuStats & MemStats use tuple struct, [a060d032](https://github.com/mrjackwills/oxker/commit/a060d032586a0707ac91cb13d922aae0850449c5),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.5'>v0.1.5</a>
|
||||
### 2022-10-07
|
||||
|
||||
### Chores
|
||||
+ Update clap to v4, [15597dbe](https://github.com/mrjackwills/oxker/commit/15597dbe6942ec053541398ce0e9dedc10a4d3ea),
|
||||
|
||||
### Docs
|
||||
+ readme.md updated, [a05bf561](https://github.com/mrjackwills/oxker/commit/a05bf561cc6d96237f683ab0b3c782d6841974d9),
|
||||
|
||||
### Features
|
||||
+ use newtype construct for container id, [41cbb84f](https://github.com/mrjackwills/oxker/commit/41cbb84f2896f8be2c37eba87e390d998aff7382),
|
||||
|
||||
### Refactors
|
||||
+ Impl Copy where able to, [e76878f4](https://github.com/mrjackwills/oxker/commit/e76878f424d72b943713ef84e95e25fada77d79e),
|
||||
+ replace async fn with just fn, [17dc604b](https://github.com/mrjackwills/oxker/commit/17dc604befac75cb9dc0311a0e43f9927fe0ca30),
|
||||
+ remove pointless clone()'s & variable declarations, [6731002e](https://github.com/mrjackwills/oxker/commit/6731002ee42c9460042c2c38aff5101b1bcebbe6),
|
||||
+ replace String::from("") with String::new(), [62fb2247](https://github.com/mrjackwills/oxker/commit/62fb22478697cc9a7ab9fb562a724965b437233a),
|
||||
+ replace map_or_else with map_or, [3e26f292](https://github.com/mrjackwills/oxker/commit/3e26f292c7dc5e13af4580952767ebe821aa5183), [5660b34d](https://github.com/mrjackwills/oxker/commit/5660b34d5149dce27706ff6daa90b854e6f84e14),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.4'>v0.1.4</a>
|
||||
### 2022-09-07
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [a3168daa](https://github.com/mrjackwills/oxker/commit/a3168daa3f769a6747dfbe61103073a7e80a1485),[78e59160](https://github.com/mrjackwills/oxker/commit/78e59160bb6a978ee80e3a99eb72f051fb64e737),
|
||||
|
||||
### Features
|
||||
+ containerize self, github action to build and push to [Docker Hub](https://hub.docker.com/r/mrjackwills/oxker), [07f97202](https://github.com/mrjackwills/oxker/commit/07f972022a69f22bac57925e6ad84234381f7890),
|
||||
+ gui_state is_loading use a HashSet to enable multiple things be loading at the same time, [66583e1b](https://github.com/mrjackwills/oxker/commit/66583e1b037b7e2f3e47948d70d8a4c6f6a2f2d5),
|
||||
+ github action publish to crates.io, [90b2e3f6](https://github.com/mrjackwills/oxker/commit/90b2e3f6db0d5f63840cd80888a30da6ecc22f20),
|
||||
+ derive Eq where appropriate, [d7c2601f](https://github.com/mrjackwills/oxker/commit/d7c2601f959bc12a64cd25cef59c837e1e8c2b2a),
|
||||
+ ignore containers 'oxker' containers, [1be9f52a](https://github.com/mrjackwills/oxker/commit/1be9f52ad4a68f93142784e9df630c59cdec0a79),
|
||||
+ update container info if container is either running OR restarting, [5f12362d](https://github.com/mrjackwills/oxker/commit/5f12362db7cb61ca68f75b99ecfc9725380d87d2),
|
||||
|
||||
### Fixes
|
||||
+ devcontainer updated, [3bde4f56](https://github.com/mrjackwills/oxker/commit/3bde4f5629539cab3dbb57556663ab81685f9d7a),
|
||||
+ Use Binate enum to enable two cycles of cpu/mem update to be executed (for each container) at the same time, refactor hashmap spawn insertions, [7ec58e79](https://github.com/mrjackwills/oxker/commit/7ec58e79a1316ad1f7e50a2781dea0fe8422c588),
|
||||
|
||||
### Refactors
|
||||
+ improved way to remove leading '/' of container name, [832e9782](https://github.com/mrjackwills/oxker/commit/832e9782d7765872cbb84df6b3703fc08cb353c9),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.3'>v0.1.3</a>
|
||||
### 2022-08-04
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [d9801cdf](https://github.com/mrjackwills/oxker/commit/d9801cdf372521fe5624a8d68fac83ed39ef81f4),
|
||||
+ linting: nursery, pedantic, unused_unwraps, [1bd61d4c](https://github.com/mrjackwills/oxker/commit/1bd61d4ce8b369d6d078201add3eea0f59fe0dea), [1263662b](https://github.com/mrjackwills/oxker/commit/1263662bd9412afacddbc10721bf216ae3a843f1), [ca3315a6](https://github.com/mrjackwills/oxker/commit/ca3315a69f593ad705eb637f227f195edd7781b2),
|
||||
|
||||
### Features
|
||||
+ build all production targets on release, [44f8140e](https://github.com/mrjackwills/oxker/commit/44f8140eaec330abe5a94f3ddae9e8b223688aa8),
|
||||
|
||||
### Fixes
|
||||
+ toml keywords, [dd2d82d1](https://github.com/mrjackwills/oxker/commit/dd2d82d114537e09dbeb12f360157f0e68e7846e),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.2'>v0.1.2</a>
|
||||
### 2022-07-23
|
||||
|
||||
### Fixes
|
||||
+ remove reqwest dependency, [10ff8bab](https://github.com/mrjackwills/oxker/commit/10ff8bab5f01f097fd6cdec60b2be947f238197b),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.1'>v0.1.1</a>
|
||||
### 2022-07-23
|
||||
|
||||
### Chores
|
||||
+ update Cargo.toml, in preparation for crates.io publishing, [fdc6898e](https://github.com/mrjackwills/oxker/commit/fdc6898e20c41415f03e310d7b84af4b6c39ab62),
|
||||
|
||||
### Docs
|
||||
+ added cargo install instructions, [c774b10d](https://github.com/mrjackwills/oxker/commit/c774b10d557b10885b9d3a0b3612330a8ecb1cd5),
|
||||
|
||||
### Fixes
|
||||
+ use SpawnId for docker hashmap JoinHandle mapping, [1ae95d58](https://github.com/mrjackwills/oxker/commit/1ae95d58c3302a95d5a0a2f0b61b126c72b6e166),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.0'>v0.1.0</a>
|
||||
### 2022-07-23
|
||||
|
||||
### Chores
|
||||
+ dependencies updated, [cf7e02dd](https://github.com/mrjackwills/oxker/commit/cf7e02dde94f69832a2e485b99785afc66a5bc15),
|
||||
|
||||
### Features
|
||||
+ Enable sorting of containers by each, and every, heading. Either via keyboard or mouse, closes [#3](https://github.com/mrjackwills/oxker/issues/3), [a6c296f2](https://github.com/mrjackwills/oxker/commit/a6c296f2cde56cf241bcd696cab8bd477270e5f4),
|
||||
+ Spawn & track docker information update requests, multiple identical requests cannot be executed, [740c059b](https://github.com/mrjackwills/oxker/commit/740c059b276f35acd1cb03f1030134646bf8a07d),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.6'>v0.0.6</a>
|
||||
### 2022-07-06
|
||||
|
||||
### Docs
|
||||
+ readme update, [f29e29ad](https://github.com/mrjackwills/oxker/commit/f29e29ad151ddf424ba630e6d33edf19acfd7636),
|
||||
+ comments improved, [1674db8a](https://github.com/mrjackwills/oxker/commit/1674db8a20aafa447732deb2e44ac8b97cf0471b),
|
||||
+ readme logo size, [a733efa6](https://github.com/mrjackwills/oxker/commit/a733efa65865e04d9ec86c7ca8785dfbae635695),
|
||||
|
||||
### Fixes
|
||||
+ Remove unwraps(), [61db81ec](https://github.com/mrjackwills/oxker/commit/61db81ecfe5684ddb8a360715f43357a042162c0),
|
||||
+ Help menu alt+tab > shift+tab typo, thanks [siph](https://github.com/siph), [04466803](https://github.com/mrjackwills/oxker/commit/04466803481b75feb7d7f275248279fdb8729862),
|
||||
|
||||
### Refactors
|
||||
+ tokio spawns, [1fd230f2](https://github.com/mrjackwills/oxker/commit/1fd230f2f3cf4e376058359515e76f4fa6e425c2),
|
||||
+ max_line_width(), [a5d7dabb](https://github.com/mrjackwills/oxker/commit/a5d7dabbd68dc15a081df33352ce3b55d9a9891c),
|
||||
+ create_release dead code removed, [297979c1](https://github.com/mrjackwills/oxker/commit/297979c197c2defd409053d8da724f922b0bba1b),
|
||||
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.5'>v0.0.5</a>
|
||||
### 2022-05-30
|
||||
|
||||
### Docs
|
||||
+ Readme one-liner to download & install latest version, [11d5ba36](https://github.com/mrjackwills/oxker/commit/11d5ba361ee4c11d080f1c3c14d8bb677cbfd1fc),
|
||||
+ Example docker-compose.yml bump alpine version to 3.16, [98c83f2f](https://github.com/mrjackwills/oxker/commit/98c83f2f68f59e78f0c78270c59886630d98913c),
|
||||
|
||||
### Fixes
|
||||
+ use Some() checks to make sure that container item indexes are still valid, else can create out-of-bounds errors, closes [#8](https://github.com/mrjackwills/oxker/issues/8), [4cf02e3f](https://github.com/mrjackwills/oxker/commit/4cf02e3f04426ef44ec5a7421687f2104ac5102f),
|
||||
+ Remove + replace as many unwrap()'s as possible, [d8e22d74](https://github.com/mrjackwills/oxker/commit/d8e22d7444965f1874d7367259310440a889432b),
|
||||
+ Help panel typo, [e497f3f2](https://github.com/mrjackwills/oxker/commit/e497f3f2d9e1dca99469860c2e728c99e29353ad),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.4'>v0.0.4</a>
|
||||
### 2022-05-08
|
||||
|
||||
### Fixes
|
||||
+ Help menu logo corrected, [2f545202](https://github.com/mrjackwills/oxker/commit/2f5452027e86f714729b804d4bf65306e755df7f),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.3'>v0.0.3</a>
|
||||
### 2022-05-08
|
||||
|
||||
### Docs
|
||||
+ slight readme tweaks, [eb9184a1](https://github.com/mrjackwills/oxker/commit/eb9184a1aee64be1c20fabd482bfcbe676bed049),
|
||||
|
||||
### Features
|
||||
+ vim movement keys, 'j' & 'k', to move through menus, thanks [siph](https://github.com/siph), [77eb33c0](https://github.com/mrjackwills/oxker/commit/77eb33c008e36965d84d1eafbbc3733af19fd262),
|
||||
|
||||
### Fixes
|
||||
+ create_release.sh correctly link to closed issues, [5820d0a9](https://github.com/mrjackwills/oxker/commit/5820d0a9b68ead71d031377c5d22138675d7dfa8),
|
||||
|
||||
### Refactors
|
||||
+ generate_block reduce params, insert into area hashmap from inside generate_block function, [32705a60](https://github.com/mrjackwills/oxker/commit/32705a60c4f865eb829cc460b2ac82db79107c1a),
|
||||
+ dead code removed, [d20e1bcd](https://github.com/mrjackwills/oxker/commit/d20e1bcd47965859a04f8e080509a5afb2de36d9),
|
||||
+ create_release.sh improved flow & comments, [4283a285](https://github.com/mrjackwills/oxker/commit/4283a285e2e60907e432294e3b97a759ec06a23d),
|
||||
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.2'>v0.0.2</a>
|
||||
### 2022-04-29
|
||||
|
||||
### Features
|
||||
+ allow toggling of mouse capture, to select & copy text with mouse, closes [#2](https://github.com/mrjackwills/oxker/issues/2), [aec184ea](https://github.com/mrjackwills/oxker/commit/aec184ea22b289e91942a4c3e6a415685884bc47),
|
||||
+ show id column, [b10f9274](https://github.com/mrjackwills/oxker/commit/b10f927481c9e38a48c1d4b94e744ec48e8b6ba6),
|
||||
+ draw_popup, using enum to draw in one of 9 areas, closes [#6](https://github.com/mrjackwills/oxker/issues/6), [1017850a](https://github.com/mrjackwills/oxker/commit/1017850a6cc91328abc1127bdb117495f5e909d8),
|
||||
+ use a message rx/sx for all docker commands, remove update loop, wait for update message from gui instead, [9b70fdfa](https://github.com/mrjackwills/oxker/commit/9b70fdfad7b38361ebee301bdc2545d3f0dfcf9e),
|
||||
|
||||
### Fixes
|
||||
+ readme.md typo, [589501f9](https://github.com/mrjackwills/oxker/commit/589501f9a4a0bfabdb0654e68cc0c752c529d97a),
|
||||
+ column heading mem > memory, [5e8e6b59](https://github.com/mrjackwills/oxker/commit/5e8e6b590b06f01a542fdd10bae8f14d303ab08a),
|
||||
+ cargo fmt added to create_release.sh, [bb29c0eb](https://github.com/mrjackwills/oxker/commit/bb29c0ebfafd6a9a036eb317a240954d1405966e),
|
||||
|
||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.1'>v0.0.1</a>
|
||||
### 2022-04-25
|
||||
|
||||
+ init commit
|
||||
@@ -0,0 +1,105 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Oxker is a Go rewrite of the original Rust-based TUI for viewing and controlling Docker containers. It uses the Bubbletea framework for the terminal UI and the Docker SDK for container management.
|
||||
|
||||
**Note:** The git history still contains deleted Rust source files (src/, Cargo.toml, etc.). Original Rust source is preserved in `source/` for reference. The active codebase is entirely Go.
|
||||
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build -o oxker ./cmd/oxker
|
||||
|
||||
# Run
|
||||
./oxker
|
||||
|
||||
# Run with debug mode (no TUI)
|
||||
./oxker -g
|
||||
|
||||
# Vet
|
||||
go vet ./...
|
||||
|
||||
# Test
|
||||
go test ./...
|
||||
|
||||
# Run a single test
|
||||
go test ./cmd/oxker -run TestAppStart
|
||||
|
||||
# Tidy modules
|
||||
go mod tidy
|
||||
|
||||
# All-in-one build script (build + vet + tidy)
|
||||
./build.sh
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The app follows the Elm architecture (Model-View-Update) via Bubbletea:
|
||||
|
||||
- **`cmd/oxker/`** - Entry point and CLI argument parsing
|
||||
- `main.go` - Creates `tea.Program` with AltScreen + mouse support, passes config to `app.New(cfg)`
|
||||
- `cli.go` - Flag definitions, config loading, CLI-to-config mapping
|
||||
|
||||
- **`internal/app/app.go`** - Central application model (~1850 lines, self-contained). Contains:
|
||||
- `App` struct implementing `tea.Model` (Init/Update/View)
|
||||
- `Container` data type with 60-sample rolling stats buffers (CPU, mem, RX, TX)
|
||||
- Container state machine: RunningHealthy, RunningUnhealthy, Paused, Exited, Dead, Restarting, Removing, Created, Unknown
|
||||
- Catppuccin Mocha color palette
|
||||
- Sorting (9 columns, asc/desc toggle), filtering (4 modes: Name/Image/Status/All), log search with match navigation
|
||||
- Sparkline charts for CPU, Memory, and Network bandwidth
|
||||
- Mouse support (wheel scroll, click to select containers, click headers to sort)
|
||||
- Docker exec via `tea.ExecProcess` (suspends TUI, runs `docker exec -it <id> sh`)
|
||||
- Inspect panel (JSON), delete confirmation popup, error/info overlays
|
||||
- Log panel: resize, toggle, horizontal scroll, save to file
|
||||
- SI units (1000-based) for byte formatting
|
||||
- CPU calculation: `(cpu_delta / system_delta) * online_cpus * 100.0`
|
||||
|
||||
- **`internal/docker/client.go`** - Thin wrapper around `github.com/docker/docker/client`. Key methods:
|
||||
- `ContainerStatsOneShot` - Single stats snapshot with proper JSON decoding
|
||||
- `Logs` - Container log streaming
|
||||
- `ContainerExec` - Docker exec API (used by client, app uses CLI `docker exec` via `tea.ExecProcess`)
|
||||
- Standard CRUD: Start, Stop, Restart, Delete, Pause, Unpause, Inspect
|
||||
|
||||
- **`internal/config/config.go`** - Configuration with TOML and JSONC support. Includes:
|
||||
- `AppColors` - Full color theming for every UI element
|
||||
- `Keymap` - Customizable key bindings with primary/secondary support
|
||||
- Config file resolution: absolute path, relative to CWD, then `~/.config/oxker/`
|
||||
|
||||
- **`internal/input/`**, **`internal/ui/`**, **`internal/utils/`** - Legacy modules from earlier architecture, currently unused by app.go
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- `github.com/charmbracelet/bubbletea` v1.3.10 - TUI framework (Elm architecture)
|
||||
- `github.com/charmbracelet/lipgloss` - Terminal styling
|
||||
- `github.com/docker/docker` - Docker SDK
|
||||
- `github.com/BurntSushi/toml` - TOML config parsing
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. `main.go` parses CLI args -> loads config -> creates `App` model with config -> starts `tea.Program`
|
||||
2. `App.Init()` fires `loadContainers()` and `tickCmd()` concurrently
|
||||
3. `tickCmd()` fires every `DockerIntervalMs` (default 1000ms), triggers container list reload + stats updates
|
||||
4. Stats collected via `ContainerStatsOneShot` (one-shot, not streaming) for each running container
|
||||
5. Key/mouse events route through `App.Update()` -> mode-specific handler (normal, filter, search, inspect, delete confirm)
|
||||
6. Container actions dispatch via `doAction()` which calls Docker SDK through `CmdMgr`
|
||||
|
||||
## CLI Flags
|
||||
|
||||
Key flags: `-d` (update interval ms), `-r` (raw logs), `-c` (color logs), `-t` (timestamps), `-s` (show self), `-g` (debug/no-TUI), `-config-file`, `-host`, `-timezone`, `-use-cli`, `-no-stderr`, `-save-dir`
|
||||
|
||||
## Mouse API (bubbletea v1.3.10)
|
||||
|
||||
`tea.MouseMsg` is a struct (not interface). Use `msg.Action` (MouseActionPress/Release/Motion) and `msg.Button` (MouseButtonLeft/WheelUp/WheelDown etc.) directly. No subtypes like MouseWheelMsg or MouseClickMsg.
|
||||
|
||||
## JCodeMunch (jmunch) Usage
|
||||
|
||||
- Start with `get_repo_outline` to quickly understand the repository structure.
|
||||
- Use `get_file_outline` before reading source to understand the API surface first.
|
||||
- Narrow searches using `kind`, `language`, and `file_pattern`.
|
||||
- Batch-retrieve related symbols with `get_symbols` instead of repeated `get_symbol` calls.
|
||||
- Use `search_text` when symbol search does not locate the needed content.
|
||||
- Use `verify: true` on `get_symbol` to detect source drift since indexing.
|
||||
@@ -1,14 +0,0 @@
|
||||
# Contributing to oxker
|
||||
|
||||
oxker encourages any, and all, suggestions, bug reports, pull requests, and/or feedback.
|
||||
|
||||
## Submitting Issues
|
||||
|
||||
Please use the oxker [issue templates](https://github.com/mrjackwills/oxker/issues/new/choose) for any Bug Report, Feature Suggestions,
|
||||
Refactor Idea, or Security Vulnerabilities.
|
||||
|
||||
Don't hesitate to submit any issues or pull requests, regardless of size.
|
||||
|
||||
## Conduct
|
||||
|
||||
oxker follows the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct)
|
||||
Generated
-2989
File diff suppressed because it is too large
Load Diff
-59
@@ -1,59 +0,0 @@
|
||||
[package]
|
||||
name = "oxker"
|
||||
version = "0.13.0"
|
||||
edition = "2024"
|
||||
authors = ["Jack Wills <email@mrjackwills.com>"]
|
||||
description = "A simple tui to view & control docker containers"
|
||||
repository = "https://github.com/mrjackwills/oxker"
|
||||
homepage = "https://github.com/mrjackwills/oxker"
|
||||
license = "MIT"
|
||||
rust-version = "1.90.0"
|
||||
readme = "README.md"
|
||||
keywords = ["docker", "tui", "tokio", "terminal", "podman"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
expect_used = "warn"
|
||||
todo = "warn"
|
||||
unused_async = "warn"
|
||||
unwrap_used = "warn"
|
||||
module_name_repetitions = "allow"
|
||||
doc_markdown = "allow"
|
||||
similar_names = "allow"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bollard = "0.20"
|
||||
cansi = "2.2"
|
||||
clap = { version = "4.5", features = ["color", "derive", "unicode"] }
|
||||
crossterm = "0.29"
|
||||
directories = "6.0"
|
||||
futures-util = "0.3"
|
||||
jiff = { version = "0.2", features = ["tzdb-bundle-always"] }
|
||||
parking_lot = { version = "0.12" }
|
||||
ratatui = "0.30"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0"}
|
||||
serde_jsonc = "1.0"
|
||||
tokio = { version = "1.49", features = ["full"] }
|
||||
tokio-util = "0.7"
|
||||
toml = { version = "1.0", default-features = false, features = ["parse", "serde"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
uuid = { version = "1.21", features = ["fast-rng", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.46"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"]}
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = 'abort'
|
||||
strip = true
|
||||
debug = false
|
||||
|
||||
|
||||
@@ -1,238 +1,47 @@
|
||||
<p align='center'>
|
||||
<img src='./.github/logo.svg' width='100px' />
|
||||
<br>
|
||||
<h1 align='center'>oxker</h1>
|
||||
<div align='center'>A simple tui to view & control docker containers</div>
|
||||
</p>
|
||||
# Oxker - Go Version
|
||||
|
||||
<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/tui-rs-revival/ratatui' target='_blank' rel='noopener noreferrer'>ratatui</a> & <a href='https://github.com/fussybeaver/bollard' target='_blank' rel='noopener noreferrer'>Bollard</a>
|
||||
</p>
|
||||
A simple TUI to view & control Docker containers, built with Bubbletea.
|
||||
|
||||
<p align='center'>
|
||||
<!-- ffmpeg -i input.mp4 -vf "scale='min(1000,iw)':-1:flags=lanczos,fps=2" -vcodec libwebp -lossless 0 -compression_level 4 -q:v 85 -loop 0 demo_01.webp -->
|
||||
<a href='https://raw.githubusercontent.com/mrjackwills/oxker/main/.github/demo_01.webp' target='_blank' rel='noopener noreferrer'>
|
||||
<img src='./.github/demo_01.webp' width='100%' alt='An animated demo of oxker in operation'/>
|
||||
</a>
|
||||
<sub>
|
||||
<!-- TODO update this -->
|
||||
<a href="https://raw.githubusercontent.com/mrjackwills/oxker/main/.github/screenshot_01.png" target='_blank' rel='noopener noreferrer'>
|
||||
link to alternative screenshot
|
||||
</a>
|
||||
</sub>
|
||||
</p>
|
||||
## Features
|
||||
|
||||
- [Download & install](#download--install)
|
||||
- [Run](#run)
|
||||
- [Build step](#build-step)
|
||||
- [Tests](#tests)
|
||||
- View Docker containers in a table format
|
||||
- Filter containers
|
||||
- Start/Stop containers
|
||||
- Container details
|
||||
|
||||
## Download & install
|
||||
## Requirements
|
||||
|
||||
### Cargo
|
||||
Published on <a href='https://www.crates.io/crates/oxker' target='_blank' rel='noopener noreferrer'>crates.io</a>, so if you have cargo installed, simply run
|
||||
- Go 1.23+
|
||||
- Docker installed and running
|
||||
|
||||
## Installation
|
||||
|
||||
```shell
|
||||
cargo install oxker
|
||||
```bash
|
||||
go install github.com/oxker/oxker@latest
|
||||
```
|
||||
|
||||
### Docker
|
||||
## Usage
|
||||
|
||||
Published on <a href='https://ghcr.io/mrjackwills/oxker' target='_blank' rel='noopener noreferrer'>ghcr.io</a> and <a href='https://hub.docker.com/r/mrjackwills/oxker' target='_blank' rel='noopener noreferrer'>Docker Hub</a>,
|
||||
with images built for `linux/amd64`, `linux/arm64`, and `linux/arm/v6`
|
||||
|
||||
|
||||
**via ghcr.io**
|
||||
|
||||
```shell
|
||||
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always ghcr.io/mrjackwills/oxker
|
||||
```
|
||||
|
||||
**via Docker Hub**
|
||||
```shell
|
||||
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always mrjackwills/oxker
|
||||
```
|
||||
|
||||
### Nix
|
||||
Using nix flakes, oxker can be ran directly with
|
||||
|
||||
```shell
|
||||
nix run nixpkgs#oxker
|
||||
```
|
||||
|
||||
Without flakes, you can build a shell that contains oxker using
|
||||
|
||||
```shell
|
||||
nix-shell -p oxker
|
||||
```
|
||||
|
||||
### AUR
|
||||
|
||||
oxker can be installed from the [AUR](https://aur.archlinux.org/packages/oxker) with using an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers):
|
||||
|
||||
```shell
|
||||
paru -S oxker
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
oxker can be installed on macOS using [Homebrew](https://formulae.brew.sh/formula/oxker):
|
||||
|
||||
```shell
|
||||
brew install oxker
|
||||
```
|
||||
|
||||
### Pre-Built
|
||||
See the <a href='https://github.com/mrjackwills/oxker/releases/latest' target='_blank' rel='noopener noreferrer'>pre-built binaries</a>
|
||||
|
||||
or, download & install (x86_64 one liner)
|
||||
|
||||
```shell
|
||||
wget https://www.github.com/mrjackwills/oxker/releases/latest/download/oxker_linux_x86_64.tar.gz &&
|
||||
tar xzvf oxker_linux_x86_64.tar.gz oxker &&
|
||||
install -Dm 755 oxker -t "${HOME}/.local/bin" &&
|
||||
rm oxker_linux_x86_64.tar.gz oxker
|
||||
```
|
||||
|
||||
or, for automatic platform selection, download, and installation (to `$HOME/.local/bin`)
|
||||
|
||||
*One should always verify <a href='https://github.com/mrjackwills/oxker/blob/main/install.sh' target='_blank' rel='noopener noreferrer'>script content</a> before running in a shell*
|
||||
|
||||
```shell
|
||||
curl https://raw.githubusercontent.com/mrjackwills/oxker/main/install.sh | bash
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```shell
|
||||
```bash
|
||||
oxker
|
||||
```
|
||||
In application controls, these, amongst many other settings, can be customized with the [config file](#Config-File)
|
||||
| button| result|
|
||||
|--|--|
|
||||
| ```( tab )``` or ```( shift+tab )``` | Change panel, clicking on a panel also changes the selected panel.|
|
||||
| ```( ↑ ↓ )``` or ```( j k )``` or ```( Home End )```| Scroll line in selected panel - mouse wheel will also scroll.|
|
||||
| ```( ← → )``` | Scroll horizontally across text.|
|
||||
| ```( ctrl )``` | Increase scroll speed, used in conjunction with scroll keys.|
|
||||
| ```( enter )```| Run selected docker command.|
|
||||
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
|
||||
| ```( 0 )``` | Stop sorting.|
|
||||
| ```( F1 )``` or ```( / )``` | Enter filter mode. |
|
||||
| ```( # )``` | Enter log search mode. |
|
||||
| ```( - ) ``` or ```(=)``` | Reduce or increase the height of the logs panel.|
|
||||
| ```( \ )``` | Toggle the visibility of the logs panel.|
|
||||
| ```( e )``` | Exec into the selected container - not available on Windows.|
|
||||
| ```( i )``` | Enter container inspect mode. |
|
||||
| ```( f )``` | Force clear the screen & redraw the gui.|
|
||||
| ```( h )``` | Toggle help menu.|
|
||||
| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
|
||||
| ```( q )``` | Quit.|
|
||||
| ```( s )``` | Save logs to `$HOME/[container_name]_[timestamp].log`, or the directory set by `--save-dir`.|
|
||||
| ```( esc )``` | Close dialog.|
|
||||
|
||||
Available command line arguments
|
||||
## Controls
|
||||
|
||||
| argument|result|
|
||||
|--|--|
|
||||
|```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. Defaults to 1000 (1 second).|
|
||||
|```-r```| Show raw logs. By default, removes ANSI formatting (conflicts with `-c`).|
|
||||
|```-c```| Attempt to color the logs (conflicts with `-r`).|
|
||||
|```-t```| Remove timestamps from each log entry.|
|
||||
|```-s```| If running via Docker, will display the oxker container.|
|
||||
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|
||||
|```--config-file [string]```| Location of a `config.toml`/`config.json`/`config.jsonc`. By default will check the users local config directory.|
|
||||
|```--host [string]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.|
|
||||
|```--no-stderr```| Do not include stderr output in logs.|
|
||||
|```--save-dir [string]```| Save exported logs into a custom directory. Defaults to `$HOME`.|
|
||||
|```--timezone [string]```| Display the Docker logs timestamps in a given [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Defaults to `Etc/UTC`.|
|
||||
|```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.|
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| q | Quit |
|
||||
| r | Refresh containers |
|
||||
| j/k | Navigate up/down |
|
||||
| enter | View container details |
|
||||
| ctrl+c | Quit |
|
||||
|
||||
### Config File
|
||||
## Building
|
||||
|
||||
|
||||
A config file enables the user to persist settings, create a custom keymap, set the color scheme used by the application, and more.
|
||||
<br>
|
||||
<br>
|
||||
Examples of the config file, alsong with explanations of each value, can be found in the [example_config](https://github.com/mrjackwills/oxker/tree/main/example_config) directory. `oxker` supports `.toml`,`.json`, and `.jsonc` file formats.
|
||||
<br>
|
||||
<br>
|
||||
If no config file is found, a `config.toml` file will be created in an `oxker` directory in the user's local config directory, as found by the [directories crate](https://docs.rs/directories/6.0.0/directories/struct.BaseDirs.html#method.config_local_dir).
|
||||
<br>
|
||||
<br>
|
||||
Command line arguments will take priority over values from the config file.
|
||||
<br>
|
||||
<br>
|
||||
If running an `oxker` container, the default config location will be `/` rather than the automatically detected platform-specific local config directory, and can be mounted as follows;
|
||||
|
||||
```shell
|
||||
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro -v /some_location/config.toml:/config.toml:ro ghcr.io/mrjackwills/oxker
|
||||
```bash
|
||||
go build -o oxker
|
||||
```
|
||||
|
||||
## Build step
|
||||
## License
|
||||
|
||||
### x86_64
|
||||
|
||||
```shell
|
||||
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)
|
||||
|
||||
```shell
|
||||
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, suggest running with a -d argument of 5000
|
||||
|
||||
```shell
|
||||
cross build --target arm-unknown-linux-musleabihf --release
|
||||
```
|
||||
|
||||
If no memory information available, try appending either ```/boot/cmdline.txt``` or ```/boot/firmware/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>
|
||||
|
||||
|
||||
### Untested on other platforms
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
~~As of yet untested, needs work~~
|
||||
|
||||
The work has been done, so far the tests don't effect any running containers, but this may change in the future.
|
||||
|
||||
```shell
|
||||
cargo test
|
||||
```
|
||||
|
||||
Run some example docker images
|
||||
|
||||
using docker/docker-compose.yml;
|
||||
|
||||
```shell
|
||||
docker compose -f ./docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
or individually
|
||||
|
||||
```shell
|
||||
docker run --name redis -d redis:alpine3.21
|
||||
```
|
||||
|
||||
```shell
|
||||
docker run --name postgres -e POSTGRES_PASSWORD=never_use_this_password_in_production -d postgres:alpine3.21
|
||||
```
|
||||
|
||||
```shell
|
||||
docker run -d --hostname my-rabbit --name rabbitmq rabbitmq:3
|
||||
```
|
||||
MIT
|
||||
@@ -0,0 +1,454 @@
|
||||
# Oxker Go Rewrite — Review Findings
|
||||
|
||||
Systematischer Vergleich aller Original-Rust-Dateien mit der Go-Reimplementierung.
|
||||
6 parallele Review-Agenten, ~130 Findings gesamt.
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL (App funktioniert falsch)
|
||||
|
||||
### C1. CPU immer 0 — ContainerStatsOneShot liefert leere precpu_stats
|
||||
- **Source:** Docker Integration Review #11
|
||||
- **Rust:** `docker_data/mod.rs:136-143` — `stream: false, one_shot: false` + `.take(1)`
|
||||
- **Go:** `docker/client.go:70` — `ContainerStatsOneShot` setzt `one-shot=true`
|
||||
- **Problem:** Mit `one-shot=true` liefert Docker leere `precpu_stats`, CPU-Berechnung ergibt 0
|
||||
- **Fix:** `ContainerStats` mit `stream=false` statt `ContainerStatsOneShot` verwenden
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C2. uint64 Underflow bei CPU-Berechnung
|
||||
- **Source:** Docker Integration Review #6/#7
|
||||
- **Rust:** `docker_data/mod.rs:86-91` — `saturating_sub` verhindert Underflow
|
||||
- **Go:** `app.go:1120-1121` — plain uint64 Subtraktion, kann wrappen
|
||||
- **Fix:** Guard: `if TotalUsage < PreTotalUsage { return 0 }`
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C3. --host Flag wird ignoriert
|
||||
- **Source:** Docker Integration Review #1
|
||||
- **Rust:** `main.rs:47-57` — Config Host > DOCKER_HOST env > default socket
|
||||
- **Go:** `docker/client.go:17-22` — nur `FromEnv`, ignoriert `Config.Host`
|
||||
- **Fix:** `docker.New()` soll optionalen Host akzeptieren, `client.WithHost(host)` verwenden
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C4. Logs ShowStderr Config ignoriert
|
||||
- **Source:** Docker Integration Review #8
|
||||
- **Rust:** `docker_data/mod.rs:256-258` — `config.show_std_err`
|
||||
- **Go:** `app.go:1089-1091` — hardcoded `ShowStderr: true`
|
||||
- **Fix:** `ShowStderr: a.Config.ShowStdErr`
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C5. Bandwidth-Chart zeigt kumulative Bytes statt Rate
|
||||
- **Source:** Data Model Review #8
|
||||
- **Rust:** `container_state.rs:627-672` — `NetworkBandwidth.to_vec_f64` berechnet Deltas
|
||||
- **Go:** `app.go:1493-1508` — `fmtRate(c.RxBytes)` auf kumulative Rohwerte
|
||||
- **Fix:** Bandwidth als Differenz zwischen aufeinanderfolgenden Einträgen berechnen
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C6. RunningUnhealthy — fehlt pause/restart in Commands
|
||||
- **Source:** Data Model Review #2, Main UI Review #6
|
||||
- **Rust:** `container_state.rs:451` — `Running(_) => [Pause, Restart, Stop, Delete]`
|
||||
- **Go:** `app.go:1749-1750` — `RunningUnhealthy` nur `["stop", "delete"]`
|
||||
- **Fix:** Gleiche Commands wie RunningHealthy
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C7. Enter führt immer ersten Command aus — kein Command-Selection
|
||||
- **Source:** Input Review #6, Data Model Review #4, Main UI Review #8
|
||||
- **Rust:** `input_handler/mod.rs:267` — prüft `panel == Commands`, nutzt `selected_docker_controls()`
|
||||
- **Go:** `app.go:922` — `cmds[0]` immer, unabhängig von Panel
|
||||
- **Fix:** `CmdSelectedIdx` Feld, nur bei `ActivePanel == PanelCommands` ausführen
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C8. Ctrl+C quit nicht in Filter/Search-Modus
|
||||
- **Source:** Input Review #9
|
||||
- **Rust:** `input_handler/mod.rs:762-763` — Ctrl+C immer quit
|
||||
- **Go:** `app.go:360-367` — Filter/Search early return, Ctrl+C wird nie erreicht
|
||||
- **Fix:** Ctrl+C Check vor Mode-Dispatch
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C9. Mouse-Events nicht blockiert in Overlay-Modi
|
||||
- **Source:** Input Review #10
|
||||
- **Rust:** `input_handler/mod.rs:74-84` — blockiert Mouse in Error/Help/Filter/Search/DeleteConfirm
|
||||
- **Go:** `app.go:638` — keine Mode-Checks, Mouse immer aktiv
|
||||
- **Fix:** Mode-Checks am Anfang von `handleMouse`
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### C10. Info-Popup Auto-Dismiss fehlt (4 Sekunden)
|
||||
- **Source:** Popups Review #9
|
||||
- **Rust:** `info.rs:52-54` — `instant.elapsed() > 4000ms`
|
||||
- **Go:** `app.go:1003-1006` — Kommentar sagt "handled by tick", aber kein Timer
|
||||
- **Fix:** `tea.Tick(4*time.Second)` → `InfoDismissMsg`
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
---
|
||||
|
||||
## HIGH (UI sieht falsch aus)
|
||||
|
||||
### H1. CPU-Format ohne Zero-Padding
|
||||
- **Source:** Main UI Review #2
|
||||
- **Rust:** `container_state.rs:521` — `{:05.2}%` → `00.00%`, `05.34%`
|
||||
- **Go:** `app.go:1325` — `%.2f%%` → `0.00%`, `5.34%`
|
||||
- **Fix:** `fmt.Sprintf("%05.2f%%", c.CPUPercent)`
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H2. Container-Spalten ohne Farben (Name/ID/Image/RX/TX)
|
||||
- **Source:** Main UI Review #10/#11
|
||||
- **Rust:** `containers.rs:26-33` — Name/ID/Image mit `containers.text` (Blue), RX/TX eigene Farben
|
||||
- **Go:** `app.go:1321-1332` — kein Styling
|
||||
- **Fix:** lipgloss.Foreground für Name/ID/Image und separate Farben für RX/TX
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H3. Command-Farben falsch
|
||||
- **Source:** Main UI Review #5
|
||||
- **Rust:** restart=Magenta, delete=Gray, resume=Blue, start=Green, stop=Red, pause=Yellow
|
||||
- **Go:** restart/resume/start=Green, stop/delete=Red, pause=Yellow
|
||||
- **Fix:** Jeder Command eigene Farbe
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H4. Layout-Proportionen falsch
|
||||
- **Source:** Main UI Review #17
|
||||
- **Rust:** `ui/mod.rs:400` — 75%/25% (oben/unten)
|
||||
- **Go:** `app.go:1664-1686` — 30% Container, 22% Charts, Rest Logs
|
||||
- **Fix:** 75%/25% Split, dann Container/Logs innerhalb der 75%
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H5. Ports: `::` und `0` statt leer
|
||||
- **Source:** Charts/Ports Review #15/#16
|
||||
- **Rust:** `container_state.rs:156-160` — None → leerer String
|
||||
- **Go:** `app.go:1522-1523` — `::` für leere IP, `0` für fehlenden Public Port
|
||||
- **Fix:** Leere Strings statt `::` und `0`
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H6. Falsches Highlight-Symbol
|
||||
- **Source:** Main UI Review #12
|
||||
- **Rust:** `mod.rs:47` — `⚪ ` (offener Kreis)
|
||||
- **Go:** `app.go:1315` — `●` (gefüllter Kreis, blau)
|
||||
- **Fix:** `⚪ ` verwenden
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H7. Default-Sort fehlt (nach CreatedAt)
|
||||
- **Source:** Data Model Review #13
|
||||
- **Rust:** `mod.rs:492-499` — sort by `created` + Name als Tiebreaker
|
||||
- **Go:** `app.go:829-831` — kein Sort bei `SortNone`
|
||||
- **Fix:** Bei `SortNone` nach `CreatedAt` sortieren, Name als Tiebreaker
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H8. Sort verliert Selection
|
||||
- **Source:** Data Model Review #6
|
||||
- **Rust:** `mod.rs:377-387` — re-selects Container by ID nach Sort
|
||||
- **Go:** `app.go:812-827` — SelectedIdx bleibt gleich, Container an Position ändert sich
|
||||
- **Fix:** Nach Sort alten Container per ID finden und SelectedIdx aktualisieren
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H9. Sort Tiebreaker by Name fehlt
|
||||
- **Source:** Data Model Review #15
|
||||
- **Rust:** `mod.rs:437-484` — `.then_with(|| name.cmp(name))`
|
||||
- **Go:** `app.go:834-866` — kein Tiebreaker
|
||||
- **Fix:** Name-Vergleich als Tiebreaker bei Gleichheit
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H10. State-Reihenfolge beim Sort falsch
|
||||
- **Source:** Data Model Review #14
|
||||
- **Rust:** `container_state.rs:316-327` — Healthy=0, Unhealthy=1, Paused=2, Restarting=3, etc.
|
||||
- **Go:** iota-Reihenfolge weicht ab (Exited vor Restarting)
|
||||
- **Fix:** `order()` Methode mit Rust-Mapping
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H11. Search-Highlight: ganze Zeile statt Substring
|
||||
- **Source:** Charts/Ports Review #22
|
||||
- **Rust:** Substring-Level Highlighting
|
||||
- **Go:** `app.go:1422-1423` — ganze Zeile gelb
|
||||
- **Fix:** Nur gematchten Substring highlighten
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H12. Help wird als Vollbild gerendert statt Overlay
|
||||
- **Source:** Popups Review #11
|
||||
- **Rust:** `mod.rs:477-480` — Overlay auf Base View
|
||||
- **Go:** `app.go:1175-1177` — ersetzt komplett den View
|
||||
- **Fix:** Base View rendern, dann Help darüber legen
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H13. Log-Zeilen: Byte-Length statt Rune-Length für UTF-8
|
||||
- **Source:** Main UI Review #28
|
||||
- **Rust:** `.chars().count()` für Width
|
||||
- **Go:** `app.go:1409-1414` — `len(line)` (Bytes)
|
||||
- **Fix:** `[]rune(line)` oder `runewidth` verwenden
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H14. Bandwidth-Chart: Keine historischen Daten gerendert
|
||||
- **Source:** Charts/Ports Review #6
|
||||
- **Rust:** `chart_bandwidth.rs:92-109` — zwei Line-Datasets (RX/TX)
|
||||
- **Go:** `app.go:1493-1508` — nur Text-Labels, kein Sparkline
|
||||
- **Fix:** Sparklines für RX/TX History hinzufügen
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### H15. LogHeight Default: 40 statt 75
|
||||
- **Source:** Popups Review #23
|
||||
- **Rust:** `gui_state.rs:220` — default `75`
|
||||
- **Go:** `app.go:213` — default `40`
|
||||
- **Fix:** Default auf `75` ändern
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM (Verhaltensunterschiede)
|
||||
|
||||
### M1. Binate Stats-Sammlung fehlt (keine Deduplizierung)
|
||||
- **Source:** Docker Integration Review #2
|
||||
- **Rust:** `docker_data/mod.rs:44-61` — Binate-Enum, Spawn-Deduplizierung
|
||||
- **Go:** `app.go:1043-1081` — keine Tracking, Duplikate möglich
|
||||
- **Fix:** `pendingStats map[string]bool` für In-Flight Tracking
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### M2. Logs: Tail-500 statt inkrementell (since-Timestamp)
|
||||
- **Source:** Docker Integration Review #9
|
||||
- **Rust:** `docker_data/mod.rs:256-273` — `since` Parameter, akkumuliert
|
||||
- **Go:** `app.go:1091` — `Tail: "500"`, ersetzt jedes Mal
|
||||
- **Status:** [x] FIXED — uses `since` for incremental fetching after initial load
|
||||
|
||||
### M3. Per-Container Log-Speicher fehlt
|
||||
- **Source:** Data Model Review #3
|
||||
- **Rust:** `ContainerItem.logs` — HashSet mit Dedup, per-Container Scroll
|
||||
- **Go:** `App.LogLines` — einzelner globaler Slice
|
||||
- **Status:** [x] FIXED — Container.Logs/LogScroll/LogHScroll with save/restore on selection change
|
||||
|
||||
### M4. Container-Selbstfilterung (oxker-Container verstecken) fehlt
|
||||
- **Source:** Docker Integration Review #12, Data Model Review #9
|
||||
- **Rust:** `docker_data/mod.rs:229-234` — ENTRY_POINT Filter
|
||||
- **Go:** keine Selbstfilterung
|
||||
- **Status:** [x] FIXED — respects Config.ShowSelf
|
||||
|
||||
### M5. Docker Ping-Check bei Startup fehlt
|
||||
- **Source:** Docker Integration Review #15
|
||||
- **Rust:** `main.rs:68-88` — Ping + Error mit Host-Pfad
|
||||
- **Go:** `app.go:207` — `docker.New()` Error wird ignoriert
|
||||
- **Status:** [x] FIXED — Ping + Host in error message
|
||||
|
||||
### M6. Filter-Mode: `/` beendet nicht den Modus (wird als Text eingegeben)
|
||||
- **Source:** Input Review #14
|
||||
- **Rust:** `input_handler:523-528` — `/` beendet Filter-Mode
|
||||
- **Go:** `app.go:570-590` — `/` wird als Zeichen eingefügt
|
||||
- **Status:** [x] FIXED (already implemented — `/` in enter case)
|
||||
|
||||
### M7. Search-Mode: `#` beendet nicht den Modus
|
||||
- **Source:** Input Review #15
|
||||
- **Rust:** `input_handler:409-414` — `#` beendet Search-Mode
|
||||
- **Go:** `app.go:598-631` — `#` wird als Zeichen eingefügt
|
||||
- **Status:** [x] FIXED (already implemented — `#` in enter case)
|
||||
|
||||
### M8. Direkte Action-Keys (s/x/r/d/p/u) existieren in Rust nicht
|
||||
- **Source:** Input Review #17
|
||||
- **Rust:** Actions nur über Commands-Panel mit Enter
|
||||
- **Go:** `app.go:478-489` — direkte Shortcuts
|
||||
- **Note:** Go-Erweiterung, könnte gewollt sein, aber kollidiert mit Rust-Keybindings
|
||||
- **Status:** [x] WONTFIX — bewusste Erweiterung, Shortcuts sind nützlich
|
||||
|
||||
### M9. Mouse-Scroll-Amount: 3 statt 1 (mit 10x Modifier)
|
||||
- **Source:** Input Review #19
|
||||
- **Rust:** `input_handler:828-829` — Scroll by 1 oder 10 mit Ctrl
|
||||
- **Go:** `app.go:639-644` — hardcoded 3
|
||||
- **Status:** [x] FIXED (already 1)
|
||||
|
||||
### M10. Mouse-Scroll in Inspect-Mode nicht unterstützt
|
||||
- **Source:** Input Review #20
|
||||
- **Rust:** `input_handler:810-818` — Mouse-Scroll im Inspect
|
||||
- **Go:** `handleMouse` hat keinen Inspect-Check
|
||||
- **Status:** [x] FIXED (via C9)
|
||||
|
||||
### M11. Error-Mode: andere Keys nicht blockiert
|
||||
- **Source:** Input Review #24
|
||||
- **Rust:** `handle_error` — nur clear/quit Keys
|
||||
- **Go:** Sort-Keys, Scroll-Keys etc. funktionieren während Error
|
||||
- **Status:** [x] FIXED (via C8)
|
||||
|
||||
### M12. Unknown-State: falsche Commands
|
||||
- **Source:** Main UI Review #7
|
||||
- **Rust:** `container_state.rs:452` — `Unknown => [Delete]` nur
|
||||
- **Go:** `app.go:1753-1754` — `Unknown => [start, restart, delete]`
|
||||
- **Status:** [x] FIXED (via C6)
|
||||
|
||||
### M13. Commands-Panel Scroll fehlt
|
||||
- **Source:** Input Review #5
|
||||
- **Rust:** `docker_controls_scroll` bewegt Command-Selection
|
||||
- **Go:** Scroll im Commands-Panel ändert Container-Selection
|
||||
- **Status:** [x] FIXED (via C7)
|
||||
|
||||
### M14. Commands-Panel Breite: fest 14 statt 10%
|
||||
- **Source:** Main UI Review #4
|
||||
- **Rust:** `ui/mod.rs:419` — `Percentage(10)`
|
||||
- **Go:** `app.go:1278` — `cmdW := 14`
|
||||
- **Status:** [x] FIXED — 10% mit min 12
|
||||
|
||||
### M15. DockerConnect Error: kein Countdown, kein Auto-Exit
|
||||
- **Source:** Popups Review #6/#17
|
||||
- **Rust:** 5-Sekunden Countdown, Auto-Exit, Host-Anzeige
|
||||
- **Go:** Standard-Error-Popup
|
||||
- **Status:** [x] FIXED — 5s countdown + auto-exit + host in error message
|
||||
|
||||
### M16. Delete-Dialog: Name nicht bold/highlighted
|
||||
- **Source:** Popups Review #1
|
||||
- **Rust:** `delete_confirm.rs:44-53` — Name bold + highlighted
|
||||
- **Go:** `app.go:1542` — einfacher String
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### M17. Help-View: stark reduzierter Inhalt
|
||||
- **Source:** Popups Review #12
|
||||
- **Rust:** ~570 Zeilen, Logo, Version, Config-Pfad, Timezone, alle Keys
|
||||
- **Go:** ~20 Zeilen, nur Basis-Keys
|
||||
- **Status:** [x] FIXED — erweitert mit Config-Pfad, Panel-Info, vollständiger Keybinding-Liste
|
||||
|
||||
### M18. Container-Name: führender `/` wird nicht entfernt
|
||||
- **Source:** Charts/Ports Review #30
|
||||
- **Rust:** `inspect.rs:35-38` — Strip `/` Prefix
|
||||
- **Go:** Name direkt verwendet
|
||||
- **Status:** [x] FIXED (already done via TrimPrefix)
|
||||
|
||||
### M19. Inspect: Wrapping statt Truncation
|
||||
- **Source:** Charts/Ports Review #29
|
||||
- **Rust:** `inspect.rs:133` — `Wrap { trim: false }`
|
||||
- **Go:** `app.go:1603` — Truncation
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### M20. Spaltenbreiten fest statt dynamisch
|
||||
- **Source:** Main UI Review #1, Data Model Review #17
|
||||
- **Rust:** `mod.rs:845-879` — max Width pro Spalte aus Daten
|
||||
- **Go:** `app.go:1264-1273` — feste Prozent-Widths
|
||||
- **Status:** [x] FIXED — dynamic widths based on container data
|
||||
|
||||
### M21. State-Icons: andere Unicode-Zeichen
|
||||
- **Source:** Main UI Review #24
|
||||
- **Rust:** `✓`, `✖`, `॥`, `↻`, `!`, `?`
|
||||
- **Go:** `✔`, `✖`, `‖`, `↻`, `!`, `?`
|
||||
- **Status:** [x] FIXED — ✓ statt ✔
|
||||
|
||||
### M22. Search-Navigation: Ctrl+N/P statt Down/Up
|
||||
- **Source:** Input Review #16
|
||||
- **Rust:** Down/Up Arrow für nächsten/vorherigen Match
|
||||
- **Go:** Ctrl+N/Ctrl+P
|
||||
- **Status:** [x] FIXED (already supports both Down/Up and Ctrl+N/P)
|
||||
|
||||
### M23. Log-Search Case-Sensitivity nicht konfigurierbar
|
||||
- **Source:** Data Model Review #19
|
||||
- **Rust:** `config.log_search_case_sensitive`
|
||||
- **Go:** immer case-insensitive
|
||||
- **Status:** [x] FIXED — respects Config.LogSearchCaseSensitive
|
||||
|
||||
### M24. Filter wraps around (Go), Rust stoppt an Grenzen
|
||||
- **Source:** Data Model Review #16
|
||||
- **Rust:** `FilterBy::next()/prev()` gibt None an Grenzen
|
||||
- **Go:** Modulo-Wrapping
|
||||
- **Status:** [x] FIXED — stoppt an Grenzen
|
||||
|
||||
### M25. Overlay-Popups: `base` Parameter wird ignoriert
|
||||
- **Source:** Popups Review #16
|
||||
- **Go:** `overlayError(base)`, `overlayInfo(base)` — base wird nie verwendet
|
||||
- **Status:** [x] FIXED — overlay placement with transparent bg
|
||||
|
||||
---
|
||||
|
||||
## LOW (Kosmetische/Konfigurationsunterschiede)
|
||||
|
||||
### L1. Alle Farben hardcoded statt aus Config
|
||||
- Betrifft: alle Panels, Popups, Charts, Headers
|
||||
- Config `AppColors` existiert, wird aber nie gelesen
|
||||
- **Status:** [x] FIXED — colorOr() resolver, borders/commands/state colors from config
|
||||
|
||||
### L2. Custom Keymaps nicht implementiert
|
||||
- Config `Keymap` existiert, wird aber nie verwendet
|
||||
- Alle Keys sind hardcoded Strings
|
||||
- **Status:** [x] FIXED — keyMatch() checks config keymap with hardcoded fallbacks
|
||||
|
||||
### L3. Mouse-Capture Toggle (`m` Key) fehlt
|
||||
- **Source:** Input Review #1
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### L4. Force-Redraw Key (`f`) fehlt
|
||||
- **Source:** Input Review #2
|
||||
- **Status:** [x] FIXED — tea.ClearScreen
|
||||
|
||||
### L5. Container-Name/Image Truncation bei 30 Zeichen
|
||||
- **Source:** Data Model Review #18
|
||||
- **Status:** [x] FIXED (handled by dynamic column widths M20)
|
||||
|
||||
### L6. CpuStats Epsilon: 0.001 statt 0.01
|
||||
- **Source:** Data Model Review #24
|
||||
- **Status:** [x] FIXED (already 0.001)
|
||||
|
||||
### L7. fmtRate fehlt GB/s Tier
|
||||
- **Source:** Data Model Review #23
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### L8. InspectData: Name/ID nicht beim Inspizieren gespeichert
|
||||
- **Source:** Data Model Review #21
|
||||
- **Status:** [x] FIXED (already shown in viewInspect title)
|
||||
|
||||
### L9. Scroll-Speed Modifier: Shift statt Ctrl
|
||||
- **Source:** Input Review #3
|
||||
- **Status:** [x] FIXED (already uses J/K = Shift+j/k)
|
||||
|
||||
### L10. Horizontal-Scroll in Search-Mode fehlt
|
||||
- **Source:** Input Review #8
|
||||
- **Status:** [x] FIXED — left/right in search mode
|
||||
|
||||
### L11. Header: "show help" → "exit help" Toggle fehlt
|
||||
- **Source:** Main UI Review #19
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### L12. Header: `↓ rx` / `↑ tx` Pfeile existieren nicht in Rust
|
||||
- **Source:** Main UI Review #20
|
||||
- **Status:** [x] FIXED — removed arrows
|
||||
|
||||
### L13. Charts: State-abhängige Farben fehlen
|
||||
- **Source:** Charts/Ports Review #4/#8
|
||||
- **Status:** [x] FIXED — dim color for non-running states
|
||||
|
||||
### L14. Charts: Y-Achse Max-Wert fehlt
|
||||
- **Source:** Charts/Ports Review #2
|
||||
- **Status:** [x] FIXED — max label shown
|
||||
|
||||
### L15. Filter-Bar: kein dediziertes UI (nur inline Text)
|
||||
- **Source:** Charts/Ports Review #17/#18
|
||||
- **Status:** [x] FIXED — dedicated styled filter bar
|
||||
|
||||
### L16. Search-Bar: kein dediziertes UI (nur inline Text)
|
||||
- **Source:** Charts/Ports Review #20/#21
|
||||
- **Status:** [x] FIXED — dedicated styled search bar
|
||||
|
||||
### L17. Ports: Title nicht zentriert, nicht als Border-Title
|
||||
- **Source:** Charts/Ports Review #31
|
||||
- **Status:** [x] FIXED — centered title
|
||||
|
||||
### L18. Inspect: Scroll-Clamping fehlt
|
||||
- **Source:** Charts/Ports Review #27
|
||||
- **Status:** [x] FIXED — clampInspectScroll()
|
||||
|
||||
### L19. "No containers" Message fehlt
|
||||
- **Source:** Main UI Review #29
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### L20. Minimum Terminal-Size Handling fehlt
|
||||
- **Source:** Main UI Review #30
|
||||
- **Status:** [x] FIXED — 60x10 minimum with message
|
||||
|
||||
### L21. Logs: "parsing logs" Init-State fehlt
|
||||
- **Source:** Main UI Review #14
|
||||
- **Status:** [x] FIXED
|
||||
|
||||
### L22. Logs: scroll_padding fehlt
|
||||
- **Source:** Main UI Review #15
|
||||
- **Status:** [x] FIXED — auto-follow when at bottom
|
||||
|
||||
### L23. Logs: color_logs Toggle fehlt
|
||||
- **Source:** Main UI Review #16
|
||||
- **Status:** [x] FIXED — stripAnsi when ColorLogs=false
|
||||
|
||||
### L24. appendMax Slice-Leak (kein Ring-Buffer)
|
||||
- **Source:** Data Model Review #22
|
||||
- **Status:** [x] FIXED — in-place copy instead of reslice
|
||||
|
||||
### L25. Delete-Dialog: Container-Not-Found Guard fehlt
|
||||
- **Source:** Popups Review #2
|
||||
- **Status:** [x] FIXED — guard + default name
|
||||
|
||||
### L26. Network: alle Interfaces statt nur eth0
|
||||
- **Source:** Docker Integration Review #3
|
||||
- **Note:** Go-Verhalten ist besser, bewusste Abweichung
|
||||
- **Status:** [x] WONTFIX — bewusste Verbesserung
|
||||
@@ -1,2 +0,0 @@
|
||||
[default.extend-words]
|
||||
ratatui = "ratatui"
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for Oxker Go TUI
|
||||
# This script builds the Go application
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "Building Oxker..."
|
||||
go build -o oxker ./cmd/oxker
|
||||
echo "Build complete: ./oxker"
|
||||
|
||||
echo "Running go vet..."
|
||||
go vet ./...
|
||||
|
||||
echo "Running go mod tidy..."
|
||||
go mod tidy
|
||||
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/oxker/oxker/internal/config"
|
||||
)
|
||||
|
||||
// CLIArgs represents parsed command-line arguments
|
||||
type CLIArgs struct {
|
||||
DockerIntervalMs int
|
||||
RawLogs bool
|
||||
ColorLogs bool
|
||||
ShowTimestamp bool
|
||||
ShowSelf bool
|
||||
DebugMode bool
|
||||
ConfigFile string
|
||||
Host string
|
||||
NoStdErr bool
|
||||
SaveDir string
|
||||
Timezone string
|
||||
UseCLI bool
|
||||
}
|
||||
|
||||
// ParseCLIArgs parses command-line arguments
|
||||
func ParseCLIArgs() (*CLIArgs, error) {
|
||||
args := &CLIArgs{}
|
||||
|
||||
// Define flags
|
||||
flag.IntVar(&args.DockerIntervalMs, "d", 1000, "Minimum update interval for docker information in milliseconds")
|
||||
flag.BoolVar(&args.RawLogs, "r", false, "Show raw logs (no ANSI formatting)")
|
||||
flag.BoolVar(&args.ColorLogs, "c", false, "Attempt to color the logs")
|
||||
flag.BoolVar(&args.ShowTimestamp, "t", false, "Remove timestamps from each log entry")
|
||||
flag.BoolVar(&args.ShowSelf, "s", false, "Display the oxker container")
|
||||
flag.BoolVar(&args.DebugMode, "g", false, "No TUI, debugging mode")
|
||||
flag.StringVar(&args.ConfigFile, "config-file", "", "Location of config file (TOML/JSON/JSONC)")
|
||||
flag.StringVar(&args.Host, "host", "", "Connect to Docker with a custom hostname")
|
||||
flag.BoolVar(&args.NoStdErr, "no-stderr", false, "Do not include stderr output in logs")
|
||||
flag.StringVar(&args.SaveDir, "save-dir", "", "Save exported logs into a custom directory")
|
||||
flag.StringVar(&args.Timezone, "timezone", "", "Display Docker logs timestamps in a given timezone")
|
||||
flag.BoolVar(&args.UseCLI, "use-cli", false, "Use Docker CLI when exec-ing into a container")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Validate docker interval
|
||||
if args.DockerIntervalMs < 1 {
|
||||
return nil, fmt.Errorf("docker interval must be greater than 0")
|
||||
}
|
||||
|
||||
// Handle conflicting log options
|
||||
if args.RawLogs && args.ColorLogs {
|
||||
args.RawLogs = false
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from file or returns defaults
|
||||
func LoadConfig(cliArgs *CLIArgs) (*config.Config, error) {
|
||||
cfg := config.NewConfig()
|
||||
|
||||
// If config file specified, try to load it
|
||||
if cliArgs.ConfigFile != "" {
|
||||
path := cliArgs.ConfigFile
|
||||
// Check if absolute or relative path
|
||||
if !filepath.IsAbs(path) {
|
||||
// Try relative to current directory
|
||||
fullPath := filepath.Join(".", path)
|
||||
if err := config.LoadConfigPath(fullPath, cfg); err != nil {
|
||||
// Try relative to config directory
|
||||
home, _ := os.UserHomeDir()
|
||||
fullPath = filepath.Join(home, ".config", "oxker", path)
|
||||
if err := config.LoadConfigPath(fullPath, cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to load config file: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := config.LoadConfigPath(path, cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to load config file: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Override with CLI args
|
||||
if cliArgs.DockerIntervalMs > 0 {
|
||||
cfg.DockerIntervalMs = cliArgs.DockerIntervalMs
|
||||
}
|
||||
cfg.RawLogs = cliArgs.RawLogs
|
||||
cfg.ColorLogs = cliArgs.ColorLogs
|
||||
cfg.ShowTimestamp = !cliArgs.ShowTimestamp
|
||||
cfg.UseCLI = cliArgs.UseCLI
|
||||
cfg.GUI = !cliArgs.DebugMode
|
||||
if cliArgs.Host != "" {
|
||||
cfg.Host = cliArgs.Host
|
||||
}
|
||||
cfg.DirConfig = cliArgs.ConfigFile
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// ShowDebugInfo displays information in debug mode (no TUI)
|
||||
func ShowDebugInfo(containers interface{}) {
|
||||
// Debug mode - display basic information without TUI
|
||||
fmt.Println("Oxker Debug Mode - Container Information")
|
||||
fmt.Println("=========================================")
|
||||
// TODO: Implement debug mode display
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/oxker/oxker/internal/app"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse command-line arguments
|
||||
cliArgs, err := ParseCLIArgs()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing CLI args: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
cfg, err := LoadConfig(cliArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Debug mode - no TUI
|
||||
if !cfg.GUI {
|
||||
ShowDebugInfo(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create and start the Bubble Tea program
|
||||
// AltScreen and MouseMode are now controlled via View() return value
|
||||
p := tea.NewProgram(app.New(cfg))
|
||||
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppStart(t *testing.T) {
|
||||
// Test that the application can be instantiated
|
||||
// This is a simple smoke test to verify the app compiles and starts
|
||||
|
||||
// Parse default CLI args
|
||||
cliArgs, err := ParseCLIArgs()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse CLI args: %v", err)
|
||||
}
|
||||
|
||||
if cliArgs == nil {
|
||||
t.Fatal("CLI args should not be nil")
|
||||
}
|
||||
|
||||
// Test config loading
|
||||
cfg, err := LoadConfig(cliArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
t.Fatal("Config should not be nil")
|
||||
}
|
||||
|
||||
// Test that debug mode works
|
||||
cfg.GUI = false
|
||||
if !cfg.GUI {
|
||||
// Debug mode - should not error
|
||||
}
|
||||
|
||||
// Test that GUI mode is enabled by default
|
||||
cfg.GUI = true
|
||||
if !cfg.GUI {
|
||||
t.Fatal("GUI mode should be enabled")
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
<p align="center">
|
||||
<img src='https://raw.githubusercontent.com/mrjackwills/oxker/main/.github/logo.svg' width='100px'/>
|
||||
<h1 align="center">oxker</h1>
|
||||
<div align="center">
|
||||
A simple tui to view & control docker containers
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/mrjackwills/oxer/main/.github/demo_01.webp" target='_blank' rel='noopener noreferrer'>
|
||||
<img src='https://raw.githubusercontent.com/mrjackwills/oxker/main/.github/demo_01.webp' width='450px'/>
|
||||
</a>
|
||||
<br>
|
||||
<sub>
|
||||
<a href="https://raw.githubusercontent.com/mrjackwills/oxker/main/.github/screenshot_01.png" target='_blank' rel='noopener noreferrer'>
|
||||
link to alternative screenshot
|
||||
</a>
|
||||
</sub>
|
||||
</p>
|
||||
|
||||
## Run
|
||||
|
||||
Images built for `linux/amd64`, `linux/arm64`, and `linux/arm/v6`
|
||||
|
||||
`docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always mrjackwills/oxker`
|
||||
|
||||
## Help
|
||||
|
||||
visit the <a href="https://github.com/mrjackwills/oxker" target='_blank' rel='noopener noreferrer'>Github repo</a>
|
||||
@@ -1,61 +0,0 @@
|
||||
#############
|
||||
## Builder ##
|
||||
#############
|
||||
|
||||
FROM --platform=$BUILDPLATFORM rust:slim AS builder
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
# These are build platform dependant, but will be ignored if not needed
|
||||
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="aarch64-linux-gnu-gcc"
|
||||
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-lgcc"
|
||||
ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_LINKER="arm-linux-gnueabihf-ld"
|
||||
|
||||
COPY ./containerised/target.sh .
|
||||
|
||||
RUN chmod +x ./target.sh && ./target.sh
|
||||
|
||||
RUN apt-get update && apt-get install $(cat /.compiler) -y
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Create blank project
|
||||
RUN cargo new oxker
|
||||
|
||||
# We want dependencies cached, so copy those first
|
||||
COPY Cargo.* /usr/src/oxker/
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /usr/src/oxker
|
||||
|
||||
# Install target platform (Cross-Compilation)
|
||||
RUN rustup target add $(cat /.target)
|
||||
|
||||
# This is a dummy build to get the dependencies cached - probably not needed - as run via a github action
|
||||
RUN cargo build --target $(cat /.target) --release
|
||||
|
||||
# Now copy in the rest of the sources
|
||||
COPY src /usr/src/oxker/src/
|
||||
|
||||
# Touch main.rs to prevent cached release build
|
||||
RUN touch /usr/src/oxker/src/main.rs
|
||||
|
||||
# This is the actual application build
|
||||
RUN cargo build --release --target $(cat /.target)
|
||||
|
||||
RUN cp /usr/src/oxker/target/$(cat /.target)/release/oxker /
|
||||
|
||||
#############
|
||||
## Runtime ##
|
||||
#############
|
||||
|
||||
FROM scratch
|
||||
|
||||
# Set an ENV to indicate that we're running in a container
|
||||
ENV OXKER_RUNTIME=container
|
||||
|
||||
COPY --from=builder /oxker /app/
|
||||
|
||||
# Run the application
|
||||
# this is used in the application itself so DO NOT EDIT
|
||||
ENTRYPOINT [ "/app/oxker"]
|
||||
@@ -1,39 +0,0 @@
|
||||
#############
|
||||
## Runtime ##
|
||||
#############
|
||||
FROM scratch
|
||||
|
||||
# Set env that we're running in a container
|
||||
ENV OXKER_RUNTIME=container
|
||||
|
||||
# Copy application binary from builder image
|
||||
COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/
|
||||
|
||||
# Run the application
|
||||
# this is used in the application itself, to stop itself show when running from a docker container, so DO NOT EDIT
|
||||
ENTRYPOINT [ "/app/oxker"]
|
||||
|
||||
|
||||
# Dev build for testing
|
||||
# docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
||||
|
||||
# Dev build one liner, x86 host
|
||||
# docker image prune -a; cargo build --release --target x86_64-unknown-linux-musl && docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
||||
|
||||
# Buildx command to build musl version for all three platforms, should probably be executed in create_release
|
||||
# docker buildx create --use
|
||||
# docker buildx build --platform linux/arm/v6,linux/arm64,linux/amd64 -t oxker_dev_all -o type=tar,dest=/tmp/oxker_dev_all.tar -f containerised/Dockerfile .
|
||||
|
||||
### Build docker files and save to .tar file
|
||||
|
||||
# docker build --platform linux/amd64 -t oxker_amd64 -f containerised/Dockerfile .; docker save -o ./oxker_amd64.tar oxker_amd64
|
||||
# docker load -i oxker_amd64.tar
|
||||
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_amd64
|
||||
|
||||
# docker build --platform linux/arm64 -t oxker_arm64 -f containerised/Dockerfile .; docker save -o ./oxker_arm64.tar oxker_arm64
|
||||
# docker load -i oxker_arm64.tar
|
||||
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_arm64
|
||||
|
||||
# docker build --platform linux/arm/v6 -t oxker_armv6 -f containerised/Dockerfile .; docker save -o ./oxker_armv6.tar oxker_armv6
|
||||
# docker load -i oxker_armv6.tar
|
||||
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_armv6
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Used in Docker build to set platform dependent variables
|
||||
|
||||
case $TARGETARCH in
|
||||
|
||||
"amd64")
|
||||
echo "x86_64-unknown-linux-musl" >/.target
|
||||
echo "" >/.compiler
|
||||
;;
|
||||
"arm64")
|
||||
echo "aarch64-unknown-linux-musl" >/.target
|
||||
echo "gcc-aarch64-linux-gnu" >/.compiler
|
||||
;;
|
||||
"arm")
|
||||
echo "arm-unknown-linux-musleabihf" >/.target
|
||||
echo "gcc-arm-linux-gnueabihf" >/.compiler
|
||||
;;
|
||||
esac
|
||||
@@ -1,541 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# rust create_release v0.6.3
|
||||
# 2025-09-20
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Check that dialog is installed
|
||||
if ! [ -x "$(command -v dialog)" ]; then
|
||||
error_close "dialog is not installed"
|
||||
fi
|
||||
|
||||
# $1 string - question to ask
|
||||
# $1 question to ask
|
||||
# Ask a yes no question, only accepts `y` or `n` as a valid answer, returns 0 for yes, 1 for no
|
||||
ask_yn() {
|
||||
while true; do
|
||||
printf "\n%b%s? [y/N]:%b " "${GREEN}" "$1" "${RESET}"
|
||||
read -r answer
|
||||
if [[ "$answer" == "y" ]]; then
|
||||
return 0
|
||||
elif [[ "$answer" == "n" ]]; then
|
||||
return 1
|
||||
else
|
||||
echo -e "${RED}\nPlease enter 'y' or 'n'${RESET}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ask continue, or quit
|
||||
ask_continue() {
|
||||
if ! ask_yn "continue"; then
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
# semver major update
|
||||
update_major() {
|
||||
local bumped_major
|
||||
bumped_major=$((MAJOR + 1))
|
||||
echo "${bumped_major}.0.0"
|
||||
}
|
||||
|
||||
# semver minor update
|
||||
update_minor() {
|
||||
local bumped_minor
|
||||
bumped_minor=$((MINOR + 1))
|
||||
MINOR=bumped_minor
|
||||
echo "${MAJOR}.${bumped_minor}.0"
|
||||
}
|
||||
|
||||
# semver patch update
|
||||
update_patch() {
|
||||
local bumped_patch
|
||||
bumped_patch=$((PATCH + 1))
|
||||
PATCH=bumped_patch
|
||||
echo "${MAJOR}.${MINOR}.${bumped_patch}"
|
||||
}
|
||||
|
||||
# Get the url of the github repo, strip .git from the end of it
|
||||
get_git_remote_url() {
|
||||
GIT_REPO_URL="$(git config --get remote.origin.url | sed 's/\.git$//')"
|
||||
}
|
||||
|
||||
# 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}"
|
||||
|
||||
if ask_yn "accept release body"; then
|
||||
update_release_body_and_changelog "$RELEASE_BODY_TEXT"
|
||||
else
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
# Edit the release-body to include new lines 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"
|
||||
|
||||
# Put new changelog entries into release-body, add link to changelog
|
||||
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
|
||||
|
||||
# Add subheading with release version and date of release
|
||||
echo -e "# <a href='${GIT_REPO_URL}/releases/tag/${NEW_TAG_WITH_V}'>${NEW_TAG_WITH_V}</a>\n${DATE_SUBHEADING}${CHANGELOG_ADDITION}$(cat CHANGELOG.md)" >CHANGELOG.md
|
||||
|
||||
# Update changelog to add links to commits [hex:8](url_with_full_commit)
|
||||
# "[aaaaaaaaaabbbbbbbbbbccccccccccddddddddd]" -> "[aaaaaaaa](https:/www.../commit/aaaaaaaaaabbbbbbbbbbccccccccccddddddddd)"
|
||||
sed -i -E "s=(\s)\[([0-9a-f]{8})([0-9a-f]{32})\]= [\2](${GIT_REPO_URL}/commit/\2\3)=g" CHANGELOG.md
|
||||
|
||||
# Update changelog to add links to closed issues
|
||||
# "closes #1" -> "closes [#1](https:/www.../issues/1)""
|
||||
sed -i -r -E "s=closes \#([0-9]+)=closes [#\1](${GIT_REPO_URL}/issues/\1)=g" CHANGELOG.md
|
||||
|
||||
# Update changelog to add links to merged PR's
|
||||
# "merges #1" -> "merges [#1](https:/www.../pull/1)""
|
||||
sed -i -r -E "s=merges \#([0-9]+)=merges [#\1](${GIT_REPO_URL}/pull/\1)=g" CHANGELOG.md
|
||||
}
|
||||
|
||||
# update version in cargo.toml, to match selected current version
|
||||
update_version_number_in_files() {
|
||||
sed -i "s|^version = .*|version = \"${MAJOR}.${MINOR}.${PATCH}\"|" Cargo.toml
|
||||
}
|
||||
|
||||
# Work out the current version, based on git tags
|
||||
# create new semver version based on user input
|
||||
# Set MAJOR MINOR PATCH
|
||||
check_tag() {
|
||||
LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)")
|
||||
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
|
||||
OP_MAJOR="major___v$(update_major)"
|
||||
OP_MINOR="minor___v$(update_minor)"
|
||||
OP_PATCH="patch___v$(update_patch)"
|
||||
OPTIONS=("$OP_MAJOR" "$OP_MINOR" "$OP_PATCH")
|
||||
select choice in "${OPTIONS[@]}"; do
|
||||
case $choice in
|
||||
"$OP_MAJOR")
|
||||
MAJOR=$((MAJOR + 1))
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
break
|
||||
;;
|
||||
"$OP_MINOR")
|
||||
MINOR=$((MINOR + 1))
|
||||
PATCH=0
|
||||
break
|
||||
;;
|
||||
"$OP_PATCH")
|
||||
PATCH=$((PATCH + 1))
|
||||
break
|
||||
;;
|
||||
*)
|
||||
error_close "invalid option $REPLY"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# run all tests
|
||||
cargo_test() {
|
||||
cargo test -- --test-threads=1
|
||||
ask_continue
|
||||
}
|
||||
|
||||
# Simulate publishing to crates.io
|
||||
cargo_publish_dry_run() {
|
||||
echo -e "${PURPLE}cargo publish --dry-run${RESET}"
|
||||
cargo publish --dry-run
|
||||
ask_continue
|
||||
}
|
||||
|
||||
# Check to see if cross is installed - if not then install
|
||||
check_cross() {
|
||||
if ! [ -x "$(command -v cross)" ]; then
|
||||
echo -e "${GREEN}cargo install cross${RESET}"
|
||||
cargo install cross
|
||||
fi
|
||||
}
|
||||
|
||||
# Build, using cross-rs, for linux x86 musl
|
||||
cross_build_x86_linux() {
|
||||
check_cross
|
||||
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
|
||||
cross build --target x86_64-unknown-linux-musl --release
|
||||
}
|
||||
|
||||
# Build, using cross-rs, for linux arm64 musl
|
||||
cross_build_aarch64_linux() {
|
||||
check_cross
|
||||
echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
|
||||
cross build --target aarch64-unknown-linux-musl --release
|
||||
}
|
||||
|
||||
# Build, using cross-rs, for linux armv6 musl
|
||||
cross_build_armv6_linux() {
|
||||
check_cross
|
||||
echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
|
||||
cross build --target arm-unknown-linux-musleabihf --release
|
||||
}
|
||||
|
||||
# Build, using cross-rs, for windows x86
|
||||
cross_build_x86_windows() {
|
||||
check_cross
|
||||
echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
|
||||
cross build --target x86_64-pc-windows-gnu --release
|
||||
}
|
||||
|
||||
# Build, using zig-build, for Apple silicon
|
||||
zig_build_aarch64_apple() {
|
||||
# mkdir /workspace/oxker/target
|
||||
echo -e "${YELLOW}docker run --rm -v $(pwd):/io -w /io ghcr.io/rust-cross/cargo-zigbuild bash -e -c 'rustup update stable && rustup default stable && rustup target add aarch64-apple-darwin && cargo zigbuild --release --target aarch64-apple-darwin${RESET}"
|
||||
|
||||
docker run --rm -v "$(pwd):/io" -w /io \
|
||||
ghcr.io/rust-cross/cargo-zigbuild \
|
||||
bash -ec 'rustup update stable && rustup default stable && rustup target add aarch64-apple-darwin && cargo zigbuild --release --target aarch64-apple-darwin'
|
||||
|
||||
if ask_yn "sudo chown $(pwd)/target"; then
|
||||
echo -e "${YELLOW}sudo chown -R vscode:vscode $(pwd)/target${RESET}"
|
||||
sudo chown -R vscode:vscode "$(pwd)/target"
|
||||
fi
|
||||
}
|
||||
|
||||
cargo_clean() {
|
||||
echo -e "${YELLOW}cargo clean${RESET}"
|
||||
cargo clean
|
||||
}
|
||||
|
||||
# Build all releases that GitHub workflow would
|
||||
# This will download GB's of docker images
|
||||
# $1 is 0 or 1, if 1 won't run ask_continue
|
||||
cross_build_all() {
|
||||
if ask_yn "cargo clean"; then
|
||||
cargo_clean
|
||||
fi
|
||||
skip_confirm=$1
|
||||
cross_build_armv6_linux
|
||||
[ "$skip_confirm" -ne 1 ] && ask_continue
|
||||
cross_build_aarch64_linux
|
||||
[ "$skip_confirm" -ne 1 ] && ask_continue
|
||||
cross_build_x86_linux
|
||||
[ "$skip_confirm" -ne 1 ] && ask_continue
|
||||
cross_build_x86_windows
|
||||
[ "$skip_confirm" -ne 1 ] && ask_continue
|
||||
zig_build_aarch64_apple
|
||||
[ "$skip_confirm" -ne 1 ] && ask_continue
|
||||
}
|
||||
|
||||
# $1 text to colourise
|
||||
release_continue() {
|
||||
echo -e "\n${PURPLE}$1${RESET}"
|
||||
ask_continue
|
||||
}
|
||||
|
||||
# Check repository for typos
|
||||
check_typos() {
|
||||
echo -e "\n${PURPLE}check typos${RESET}"
|
||||
typos
|
||||
ask_continue
|
||||
}
|
||||
|
||||
# Make sure the unused lint isn't used
|
||||
check_allow_unused() {
|
||||
matches_any=$(find . -type d \( -name .git -o -name target \) -prune -o -type f -exec grep -lE '^#!\[allow\(unused\)\]$' {} +)
|
||||
matches_cargo=$(grep "^unused = \"allow\"" ./Cargo.toml)
|
||||
if [ -n "$matches_any" ]; then
|
||||
echo "\"#[allow(unused)]\" in ${matches_any}"
|
||||
ask_continue
|
||||
elif [ -n "$matches_cargo" ]; then
|
||||
echo "\"unused = \"allow\"\" in Cargo.toml"
|
||||
ask_continue
|
||||
fi
|
||||
}
|
||||
|
||||
# build container for amd64 platform
|
||||
build_container_amd64() {
|
||||
echo -e "${YELLOW}docker build --platform linux/amd64 --no-cache -t oxker_amd64 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_amd64.tar oxker_amd64${RESET}"
|
||||
docker build --platform linux/amd64 --no-cache -t oxker_amd64 -f containerised/Dockerfile .
|
||||
docker save -o /tmp/oxker_amd64.tar oxker_amd64
|
||||
}
|
||||
|
||||
# build container for aarm64 platform
|
||||
build_container_arm64() {
|
||||
echo -e "${YELLOW}docker build --platform linux/arm64 --no-cache -t oxker_arm64 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_arm64.tar oxker_arm64${RESET}"
|
||||
docker build --platform linux/arm64 --no-cache -t oxker_arm64 -f containerised/Dockerfile .
|
||||
docker save -o /tmp/oxker_arm64.tar oxker_arm64
|
||||
}
|
||||
|
||||
# build container for armv6 platform
|
||||
build_container_armv6() {
|
||||
echo -e "${YELLOW}docker build --platform linux/arm/v6 --no-cache -t oxker_armv6 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_armv6.tar oxker_armv6${RESET}"
|
||||
docker build --platform linux/arm/v6 --no-cache -t oxker_armv6 -f containerised/Dockerfile .
|
||||
docker save -o /tmp/oxker_armv6.tar oxker_armv6
|
||||
}
|
||||
|
||||
# Build all the containers, this get executed in the github action
|
||||
# $1 is 0 or 1, if 1 won't run ask_continue
|
||||
build_container_all() {
|
||||
skip_confirm=$1
|
||||
build_container_amd64
|
||||
[ "$skip_confirm" -ne 1 ] && ask_continue
|
||||
build_container_arm64
|
||||
[ "$skip_confirm" -ne 1 ] && ask_continue
|
||||
build_container_armv6
|
||||
[ "$skip_confirm" -ne 1 ] && ask_continue
|
||||
}
|
||||
|
||||
# Full flow to create a new release
|
||||
release_flow() {
|
||||
check_allow_unused
|
||||
check_typos
|
||||
|
||||
check_git
|
||||
get_git_remote_url
|
||||
|
||||
cargo_test
|
||||
cross_build_all 0
|
||||
build_container_all 0
|
||||
cargo_publish_dry_run
|
||||
|
||||
cd "${CWD}" || error_close "Can't find ${CWD}"
|
||||
check_tag
|
||||
|
||||
NEW_TAG_WITH_V="v${MAJOR}.${MINOR}.${PATCH}"
|
||||
printf "\nnew tag chosen: %s\n\n" "${NEW_TAG_WITH_V}"
|
||||
|
||||
RELEASE_BRANCH=release-$NEW_TAG_WITH_V
|
||||
echo -e
|
||||
ask_changelog_update
|
||||
|
||||
release_continue "checkout ${RELEASE_BRANCH}"
|
||||
git checkout -b "$RELEASE_BRANCH"
|
||||
|
||||
release_continue "update_version_number_in_files"
|
||||
update_version_number_in_files
|
||||
|
||||
echo -e "\ncargo fmt"
|
||||
cargo fmt
|
||||
|
||||
echo -e "\n${PURPLE}cargo check${RESET}\n"
|
||||
cargo check
|
||||
|
||||
release_continue "git add ."
|
||||
git add .
|
||||
|
||||
release_continue "git commit -m \"chore: release \"${NEW_TAG_WITH_V}\""
|
||||
git commit -m "chore: release ${NEW_TAG_WITH_V}"
|
||||
|
||||
release_continue "git checkout main"
|
||||
git checkout main
|
||||
|
||||
echo -e "${PURPLE}git pull origin main${RESET}"
|
||||
git pull origin main
|
||||
|
||||
echo -e "${PURPLE}git merge --no-ff \"${RELEASE_BRANCH}\" -m \"chore: merge ${RELEASE_BRANCH} into main\"${RESET}"
|
||||
git merge --no-ff "$RELEASE_BRANCH" -m "chore: merge ${RELEASE_BRANCH} into main"
|
||||
|
||||
echo -e "\n${PURPLE}cargo check${RESET}\n"
|
||||
cargo check
|
||||
|
||||
release_continue "git tag -am \"${RELEASE_BRANCH}\" \"$NEW_TAG_WITH_V\""
|
||||
git tag -am "${RELEASE_BRANCH}" "$NEW_TAG_WITH_V"
|
||||
|
||||
release_continue "git push --atomic origin main \"$NEW_TAG_WITH_V\""
|
||||
git push --atomic origin main "$NEW_TAG_WITH_V"
|
||||
|
||||
release_continue "git checkout dev"
|
||||
git checkout dev
|
||||
|
||||
release_continue "git merge --no-ff main -m \"chore: merge main into dev\""
|
||||
git merge --no-ff main -m "chore: merge main into dev"
|
||||
|
||||
release_continue "git push origin dev"
|
||||
git push origin dev
|
||||
|
||||
release_continue "git branch -d \"$RELEASE_BRANCH\""
|
||||
git branch -d "$RELEASE_BRANCH"
|
||||
}
|
||||
|
||||
build_choice() {
|
||||
cmd=(dialog --backtitle "Choose option" --keep-tite --radiolist "choose" 14 80 16)
|
||||
options=(
|
||||
1 "x86 musl linux" off
|
||||
2 "aarch64 musl linux" off
|
||||
3 "armv6 musl linux" off
|
||||
4 "aarch64 apple" off
|
||||
5 "x86 windows" off
|
||||
6 "all" off
|
||||
7 "all automatic" off
|
||||
)
|
||||
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
|
||||
exitStatus=$?
|
||||
if [ $exitStatus -ne 0 ]; then
|
||||
exit
|
||||
fi
|
||||
for choice in $choices; do
|
||||
case $choice in
|
||||
0)
|
||||
exit
|
||||
;;
|
||||
1)
|
||||
cross_build_x86_linux
|
||||
exit
|
||||
;;
|
||||
2)
|
||||
cross_build_aarch64_linux
|
||||
exit
|
||||
;;
|
||||
3)
|
||||
cross_build_armv6_linux
|
||||
exit
|
||||
;;
|
||||
4)
|
||||
zig_build_aarch64_apple
|
||||
exit
|
||||
;;
|
||||
5)
|
||||
cross_build_x86_windows
|
||||
exit
|
||||
;;
|
||||
6)
|
||||
cross_build_all 0
|
||||
exit
|
||||
;;
|
||||
7)
|
||||
cross_build_all 1
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
build_container_choice() {
|
||||
cmd=(dialog --backtitle "Choose option" --keep-tite --radiolist "choose" 14 80 16)
|
||||
options=(
|
||||
1 "x86 " off
|
||||
2 "aarch64" off
|
||||
3 "armv6" off
|
||||
4 "all" off
|
||||
5 "all automatic" off
|
||||
)
|
||||
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
|
||||
exitStatus=$?
|
||||
if [ $exitStatus -ne 0 ]; then
|
||||
exit
|
||||
fi
|
||||
for choice in $choices; do
|
||||
case $choice in
|
||||
0)
|
||||
exit
|
||||
;;
|
||||
1)
|
||||
build_container_amd64
|
||||
exit
|
||||
;;
|
||||
2)
|
||||
build_container_arm64
|
||||
exit
|
||||
;;
|
||||
3)
|
||||
build_container_armv6
|
||||
exit
|
||||
;;
|
||||
4)
|
||||
build_container_all 0
|
||||
exit
|
||||
;;
|
||||
5)
|
||||
build_container_all 1
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
}
|
||||
|
||||
main() {
|
||||
cmd=(dialog --backtitle "Choose option" --keep-tite --radiolist "choose" 14 80 16)
|
||||
options=(
|
||||
1 "test" off
|
||||
2 "release" off
|
||||
3 "build" off
|
||||
4 "docker builds" off
|
||||
)
|
||||
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
|
||||
exitStatus=$?
|
||||
if [ $exitStatus -ne 0 ]; then
|
||||
exit
|
||||
fi
|
||||
for choice in $choices; do
|
||||
case $choice in
|
||||
0)
|
||||
exit
|
||||
;;
|
||||
1)
|
||||
cargo_test
|
||||
main
|
||||
break
|
||||
;;
|
||||
2)
|
||||
release_flow
|
||||
break
|
||||
;;
|
||||
3)
|
||||
build_choice
|
||||
main
|
||||
break
|
||||
;;
|
||||
4)
|
||||
build_container_choice
|
||||
main
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
main
|
||||
@@ -1,17 +0,0 @@
|
||||
# Use an official lightweight image as a base
|
||||
FROM alpine:latest
|
||||
|
||||
# Install a simple utility (e.g., curl) to run as a health check
|
||||
RUN apk --no-cache add curl speedtest-cli
|
||||
|
||||
# Create a dummy file that we will use in our health check
|
||||
RUN touch /tmp/healthy
|
||||
|
||||
# Define a simple health check
|
||||
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
|
||||
CMD [ ! -f /tmp/healthy ] || exit 1
|
||||
|
||||
# Start a basic loop that keeps the container running
|
||||
CMD ["sh", "-c", "speedtest-cli; while :; do >&2 echo 'Container is running but will be unhealthy (also printing to stderr)'; sleep 30; done"]
|
||||
|
||||
# docker build -t unhealthy-container . -f Dockerfile.unhealthy; docker run -d --name unhealthy unhealthy-container
|
||||
@@ -1,58 +0,0 @@
|
||||
|
||||
networks:
|
||||
oxker-example-net:
|
||||
name: oxker-examaple-net
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18-alpine
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=never_use_this_password_in_production
|
||||
ipc: private
|
||||
restart: always
|
||||
shm_size: 256MB
|
||||
networks:
|
||||
- oxker-example-net
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1024M
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
ipc: private
|
||||
restart: always
|
||||
networks:
|
||||
- oxker-example-net
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 384M
|
||||
rabbitmq:
|
||||
image: rabbitmq:3
|
||||
container_name: rabbitmq
|
||||
ipc: private
|
||||
restart: always
|
||||
networks:
|
||||
- oxker-example-net
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
some_container:
|
||||
container_name: some_container
|
||||
image: some_container
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.unhealthy
|
||||
ipc: private
|
||||
restart: always
|
||||
networks:
|
||||
- oxker-example-net
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 128M
|
||||
|
||||
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
{
|
||||
// Example JSONC config file
|
||||
// This needs to be renamed to "config.jsonc" ("config.json" will also work, even if the file is actually a jsonc) in order for oxker to automatically load
|
||||
// oxker will also read .toml and .json files which use the same key/value structure & format as this file
|
||||
// Every key is optional, with defaults that oxker will choose if missing or invalid
|
||||
// The `--config-file` cli argument can be used to load configuration files from any readable location
|
||||
// Docker update interval in ms, minimum effectively 1000
|
||||
"docker_interval": 1000,
|
||||
// Attempt to colorize the logs, conflicts with "raw"
|
||||
"color_logs": false,
|
||||
// Show raw logs, default is to remove ansi formatting, conflicts with "color"
|
||||
"raw_logs": false,
|
||||
// Show self (the oxker container) when running as a docker container
|
||||
"show_self": false,
|
||||
// Show std_err in logs
|
||||
"show_std_err": true,
|
||||
// Show a timestamp for every log entry
|
||||
"show_timestamp": true,
|
||||
// Don't draw gui - for debugging - mostly pointless
|
||||
"gui": true,
|
||||
// Docker host location. Will take priority over a DOCKER_HOST env.
|
||||
// "host": "/var/run/docker.sock",
|
||||
// Display the timestamp in a custom format, if given option is invalid, it will default to %Y-%m-%dT%H:%M:%S.%8f -> 2025-02-18T12:34:56.01234567
|
||||
// *Should* accept any valid strftime string up to 32 chars, see https://strftime.org/
|
||||
"timestamp_format": "%Y-%m-%dT%H:%M:%S.%8f",
|
||||
// Display the container logs timestamp with a given timezone, if timezone is unknown, defaults to UTC
|
||||
"timezone": "Etc/UTC",
|
||||
// Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
||||
// "save_dir": "$HOME",
|
||||
// Force use of docker cli when execing into containers, honestly mostly pointless
|
||||
"use_cli": false,
|
||||
// Show the logs section - this can be changed during operation with the log_section_toggle key
|
||||
"show_logs": true,
|
||||
// Use case-sensitive matching for logs
|
||||
"log_search_case_sensitive": true,
|
||||
//////////////////
|
||||
// Custom Keymap //
|
||||
//////////////////
|
||||
// Available keys are;
|
||||
// 1) a-z and A-Z
|
||||
// 2) 0-9
|
||||
// WARNING if using the \ key, it needs to be escaped, e.g. "log_section_toggle": ["\\"]
|
||||
// 3) / \ , . # ' [ ] ; = -
|
||||
// 3) F1-F12
|
||||
// 4) backspace, tab, backtab, delete, end, esc, home, insert, pagedown, pageup, left, right, up, down
|
||||
// Each definition can have two keys associated with it
|
||||
// WARNING "scroll_many" only accepts control, alt, shift, with no secondary option
|
||||
// If any key clashes are found, oxker will revert to it's default keymap
|
||||
"keymap": {
|
||||
// Clear any popup boxes, filter panel, or help panel
|
||||
"clear": [
|
||||
"c",
|
||||
"esc"
|
||||
],
|
||||
// Cancel delete - clear also works here
|
||||
"delete_deny": [
|
||||
"n"
|
||||
],
|
||||
// Confirm Delete
|
||||
"delete_confirm": [
|
||||
"y"
|
||||
],
|
||||
// Exec into the selected container
|
||||
"exec": [
|
||||
"e"
|
||||
],
|
||||
// Enter filter mode
|
||||
"filter_mode": [
|
||||
"/",
|
||||
"F1"
|
||||
],
|
||||
// Enter log search mode
|
||||
"log_search_mode": [
|
||||
"#"
|
||||
],
|
||||
// Quit at anytime
|
||||
"quit": [
|
||||
"q"
|
||||
],
|
||||
// Save logs of selected container to file on disk
|
||||
"save_logs": [
|
||||
"s"
|
||||
],
|
||||
// Scroll down a list by one item
|
||||
"scroll_down": [
|
||||
"down",
|
||||
"j"
|
||||
],
|
||||
// Scroll down to the end of a list
|
||||
"scroll_end": [
|
||||
"end"
|
||||
],
|
||||
// Modifier to scroll by 10 lines instead of one, used in conjunction with scroll_up/scroll_down
|
||||
"scroll_many": [
|
||||
"control"
|
||||
],
|
||||
// Scroll up to the start of a list
|
||||
"scroll_start": [
|
||||
"home"
|
||||
],
|
||||
// Scroll up a list by one item
|
||||
"scroll_up": [
|
||||
"up",
|
||||
"k"
|
||||
],
|
||||
// Horizontal scroll of the logs
|
||||
"scroll_forward": [
|
||||
"right"
|
||||
],
|
||||
"scroll_back": [
|
||||
"left"
|
||||
],
|
||||
// Select next panel
|
||||
"select_next_panel": [
|
||||
"tab"
|
||||
],
|
||||
// Select previous panel
|
||||
"select_previous_panel": [
|
||||
"backtab"
|
||||
],
|
||||
// Sort the containers based on specific column
|
||||
"sort_by_name": [
|
||||
"1"
|
||||
],
|
||||
"sort_by_state": [
|
||||
"2"
|
||||
],
|
||||
"sort_by_status": [
|
||||
"3"
|
||||
],
|
||||
"sort_by_cpu": [
|
||||
"4"
|
||||
],
|
||||
"sort_by_memory": [
|
||||
"5"
|
||||
],
|
||||
"sort_by_id": [
|
||||
"6"
|
||||
],
|
||||
"sort_by_image": [
|
||||
"7"
|
||||
],
|
||||
"sort_by_rx": [
|
||||
"8"
|
||||
],
|
||||
"sort_by_tx": [
|
||||
"9"
|
||||
],
|
||||
// Reset the sorted containers
|
||||
"sort_reset": [
|
||||
"0"
|
||||
],
|
||||
// Toggle the help panel
|
||||
"toggle_help": [
|
||||
"h"
|
||||
],
|
||||
// Toggle mouse capture
|
||||
"toggle_mouse_capture": [
|
||||
"m"
|
||||
],
|
||||
// Reduce the height of the logs list section
|
||||
"log_section_height_decrease": [
|
||||
"-"
|
||||
],
|
||||
// Increase the height of the logs list section
|
||||
"log_section_height_increase": [
|
||||
"+"
|
||||
],
|
||||
// Toggle visibility of the log section
|
||||
"log_section_toggle": [
|
||||
"\\"
|
||||
],
|
||||
// Toggle to inspect container screen
|
||||
"inspect": [
|
||||
"i"
|
||||
],
|
||||
// Force a complete clear & redraw of the screen
|
||||
"force_redraw": [
|
||||
"f"
|
||||
]
|
||||
},
|
||||
////////////////////
|
||||
// Custom Colors //
|
||||
////////////////////
|
||||
// Colors be listed as either;
|
||||
// 1) named ANSI: 'red', case insensitive, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
// 2) Hex values: '#FF0000', case insensitive
|
||||
// 3) 'reset' for transparency
|
||||
// Some background/foreground combinations don't work, I *think* this is an issue/feature of ratatui - but I may have just made a mistake somewhere
|
||||
"colors": {
|
||||
// The single line bar at the uppermost of the display. Contains name/state/status headings etc
|
||||
"headers_bar": {
|
||||
// Background color of the entire line
|
||||
"background": "magenta",
|
||||
// Animated loading icon at the start of the bar
|
||||
"loading_spinner": "white",
|
||||
// Text color
|
||||
"text": "black",
|
||||
// Text color of a selected header
|
||||
"text_selected": "gray"
|
||||
},
|
||||
// The borders around the selectable panels - Containers, Commands, Logs
|
||||
"borders": {
|
||||
// Border when selected
|
||||
"selected": "lightcyan",
|
||||
// Border when not selected
|
||||
"unselected": "grey"
|
||||
},
|
||||
// The containers sections, in the future more color customization options should be made available in this section
|
||||
"containers": {
|
||||
// The icon use to illustrate which container is currently selected
|
||||
"icon": "white",
|
||||
// Background color of panel
|
||||
"background": "reset",
|
||||
// At the moment, this will only change the color of the name, id, and image columns
|
||||
"text": "blue",
|
||||
// Text color of the RX column
|
||||
"text_rx": "#FFE9C1",
|
||||
// Text color of the TX column
|
||||
"text_tx": "#CD8C8C"
|
||||
},
|
||||
// Each state of a container has a color, which is used in multiple places, i.e. chart titles, state/status/cpu/memory columns in the container section
|
||||
"container_state": {
|
||||
"dead": "red",
|
||||
"exited": "red",
|
||||
"paused": "yellow",
|
||||
"removing": "lightred",
|
||||
"restarting": "lightgreen",
|
||||
"running_healthy": "green",
|
||||
"running_unhealthy": "#FFB224",
|
||||
"unknown": "red"
|
||||
},
|
||||
// The color the of Docker commands available for each container
|
||||
"commands": {
|
||||
// Background color of panel
|
||||
"background": "reset",
|
||||
"pause": "yellow",
|
||||
"restart": "magenta",
|
||||
"stop": "red",
|
||||
"delete": "gray",
|
||||
"resume": "blue",
|
||||
"start": "green"
|
||||
},
|
||||
// The cpu chart
|
||||
"chart_cpu": {
|
||||
// Background color of panel
|
||||
"background": "reset",
|
||||
// Border color
|
||||
"border": "white",
|
||||
// Chart title
|
||||
"title": "green",
|
||||
// Maximum CPU percentage
|
||||
"max": "#FFB224",
|
||||
// Points on the chart
|
||||
"points": "magenta",
|
||||
// The charts y-axis
|
||||
"y_axis": "white"
|
||||
},
|
||||
// The memory chart
|
||||
"chart_memory": {
|
||||
// Background color of panel
|
||||
"background": "reset",
|
||||
// Border color
|
||||
"border": "white",
|
||||
// Chart title
|
||||
"title": "green",
|
||||
// Maximum memory use
|
||||
"max": "#FFB224",
|
||||
// Points on the chart
|
||||
"points": "cyan",
|
||||
// The charts y-axis
|
||||
"y_axis": "white"
|
||||
},
|
||||
// The ports chart
|
||||
"chart_ports": {
|
||||
// Background color of panel
|
||||
"background": "reset",
|
||||
// Border color
|
||||
"border": "white",
|
||||
// Chart title
|
||||
"title": "green",
|
||||
// Private/Public/IP headings
|
||||
"headings": "yellow",
|
||||
// Ports & IP listing text
|
||||
"text": "white"
|
||||
},
|
||||
// The bandwidth chart
|
||||
"chart_bandwidth": {
|
||||
//Background color of panel
|
||||
"background": "reset",
|
||||
// Border color
|
||||
"border": "white",
|
||||
// Maximum RX value - again paused & stopped colors not yet customizable
|
||||
"max_rx": "#FFE9C1",
|
||||
// # Maximum TX value - again paused & stopped colors not yet customizable
|
||||
"max_tx": "#CD8C8C",
|
||||
// RX points on the chart - again paused & stopped colors not yet customizable
|
||||
"points_rx": "#FFE9C1",
|
||||
// TX points on the chart - again paused & stopped colors not yet customizable
|
||||
"points_tx": "#CD8C8C",
|
||||
// RX title color
|
||||
"title_rx": "#FFE9C1",
|
||||
// TX title color
|
||||
"title_tx": "#CD8C8C",
|
||||
// The charts y-axis
|
||||
"y_axis": "white"
|
||||
},
|
||||
// The filter panel
|
||||
"filter": {
|
||||
// Background color of panel
|
||||
"background": "reset",
|
||||
// color of text
|
||||
"text": "gray",
|
||||
// background color of the selected filter by item (Name/Image/Status/All)
|
||||
"selected_filter_background": "gray",
|
||||
// text color of the selected filter by item (Name/Image/Status/All)
|
||||
"selected_filter_text": "black",
|
||||
// Highlighted text color
|
||||
"highlight": "magenta"
|
||||
},
|
||||
// The log search panel
|
||||
"log_search": {
|
||||
// Background color of panel
|
||||
"background": "reset",
|
||||
// color of text
|
||||
"text": "gray",
|
||||
// text color of the buttons text
|
||||
"button_text": "black",
|
||||
// Highlighted text color
|
||||
"highlight": "magenta"
|
||||
},
|
||||
// The logs panel, will only be applied if color_logs is false
|
||||
"logs": {
|
||||
// Background color of panel
|
||||
"background": "reset",
|
||||
// text color
|
||||
"text": "reset"
|
||||
},
|
||||
// The help popup
|
||||
"popup_help": {
|
||||
// Background color
|
||||
"background": "magenta",
|
||||
// Text color
|
||||
"text": "black",
|
||||
// Highlighted text color
|
||||
"text_highlight": "white"
|
||||
},
|
||||
// The info popup - used to display small messages - such as saving logs to disk, or change of mouse capture settings
|
||||
"popup_info": {
|
||||
// Background color
|
||||
"background": "blue",
|
||||
// Text color
|
||||
"text": "white"
|
||||
},
|
||||
// The delete popup - used to display a confirmation warning when about to delete a container
|
||||
"popup_delete": {
|
||||
// Background color
|
||||
"background": "white",
|
||||
// Text color
|
||||
"text": "black",
|
||||
// Highlighted text color
|
||||
"text_highlight": "red"
|
||||
},
|
||||
// The error popup - hopefully you'll never have to see this
|
||||
"popup_error": {
|
||||
// Background color
|
||||
"background": "red",
|
||||
// Text color
|
||||
"text": "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
# oxker config file
|
||||
# oxker will also read .jsonc and .json files which use the same key/value structure & format as this file
|
||||
# Every key is optional, with defaults that oxker will choose if missing or invalid
|
||||
# The `--config-file` cli argument can be used to load configuration files from any readable location
|
||||
|
||||
# Docker update interval in ms, minimum effectively 1000
|
||||
docker_interval = 1000
|
||||
|
||||
# Attempt to colorize the logs, conflicts with "raw"
|
||||
color_logs = false
|
||||
|
||||
# Show raw logs, default is to remove ansi formatting, conflicts with "color"
|
||||
raw_logs = false
|
||||
|
||||
# Show self (the oxker container) when running as a docker container
|
||||
show_self = false
|
||||
|
||||
# Show std_err in logs
|
||||
show_std_err = true
|
||||
|
||||
# Show a timestamp for every log entry
|
||||
show_timestamp = true
|
||||
|
||||
# Don't draw gui - for debugging - mostly pointless
|
||||
gui = true
|
||||
|
||||
# Docker host location. Will take priority over a DOCKER_HOST env.
|
||||
# host = "/var/run/docker.sock"
|
||||
|
||||
# Display the container logs timestamp with a given timezone, if timezone is unknown, defaults to UTC
|
||||
timezone = "Etc/UTC"
|
||||
|
||||
# Display the timestamp in a custom format, if given option is invalid, it will default to %Y-%m-%dT%H:%M:%S.%8f -> 2025-02-18T12:34:56.012345678Z
|
||||
# *Should* accept any valid strftime string up to 32 chars, see https://strftime.org/
|
||||
timestamp_format = "%Y-%m-%dT%H:%M:%S.%8f"
|
||||
|
||||
# Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
||||
# save_dir = "$HOME"
|
||||
|
||||
# Force use of docker cli when execing into containers, honestly mostly pointless
|
||||
use_cli = false
|
||||
|
||||
# Show the logs section - this can be changed during operation with the log_section_toggle key
|
||||
show_logs = true
|
||||
|
||||
# Use case-sensitive matching for logs
|
||||
log_search_case_sensitive = true
|
||||
|
||||
#################
|
||||
# Custom Keymap #
|
||||
#################
|
||||
|
||||
# Available keys are;
|
||||
# 1) a-z and A-Z
|
||||
# 2) 0-9
|
||||
# WARNING if using the \ key, it needs to be escaped, e.g. log_section_toggle = ["\\"]
|
||||
# 3) / \ , . # ' [ ] ; = -
|
||||
# 3) F1-F12
|
||||
# 4) backspace, tab, backtab, delete, end, esc, home, insert, pagedown, pageup, left, right, up, down
|
||||
|
||||
# Each definition can have two keys associated with it
|
||||
|
||||
# WARNING "scroll_many" only accepts control, alt, shift, with no secondary option
|
||||
|
||||
# If any key clashes are found, oxker will revert to it's default keymap
|
||||
|
||||
[keymap]
|
||||
# Clear any popup boxes, filter panel, or help panel
|
||||
clear = ["c", "esc"]
|
||||
# Cancel delete - clear also works here
|
||||
delete_deny = ["n"]
|
||||
# Confirm Delete
|
||||
delete_confirm = ["y"]
|
||||
# Exec into the selected container
|
||||
exec = ["e"]
|
||||
# Enter filter mode
|
||||
filter_mode = ["/", "F1"]
|
||||
|
||||
# Enter log search mode
|
||||
log_search_mode = ["#"]
|
||||
|
||||
# Quit at anytime
|
||||
quit = ["q"]
|
||||
# Save logs of selected container to file on disk
|
||||
save_logs = ["s"]
|
||||
# scroll down a list by one item
|
||||
scroll_down = ["down", "j"]
|
||||
|
||||
# scroll down to the end of a list
|
||||
scroll_end = ["end"]
|
||||
# Modifier to scroll by 10 lines instead of one, used in conjunction with scroll_up/scroll_down
|
||||
scroll_many = ["control"]
|
||||
# scroll up to the start of a list
|
||||
scroll_start = ["home"]
|
||||
# scroll up a list by one item
|
||||
scroll_up = ["up", "k"]
|
||||
# Horizontal scroll of the logs
|
||||
scroll_forward = ["right"]
|
||||
scroll_back = ["left"]
|
||||
# Select next panel
|
||||
select_next_panel = ["tab"]
|
||||
# Select previous panel
|
||||
select_previous_panel = ["backtab"]
|
||||
# Sort the containers based on specific column
|
||||
sort_by_name = ["1"]
|
||||
sort_by_state = ["2"]
|
||||
sort_by_status = ["3"]
|
||||
sort_by_cpu = ["4"]
|
||||
sort_by_memory = ["5"]
|
||||
sort_by_id = ["6"]
|
||||
sort_by_image = ["7"]
|
||||
sort_by_rx = ["8"]
|
||||
sort_by_tx = ["9"]
|
||||
# Reset the sorted containers
|
||||
sort_reset = ["0"]
|
||||
# Toggle the help panel
|
||||
toggle_help = ["h"]
|
||||
# Toggle mouse capture
|
||||
toggle_mouse_capture = ["m"]
|
||||
# Reduce the height of the logs list section
|
||||
log_section_height_decrease = ["-"]
|
||||
log_section_height_increase = ["+"]
|
||||
# Toggle visibility of the log section
|
||||
log_section_toggle = ["\\"]
|
||||
# Toggle to inspect container screen
|
||||
inspect = ["i"]
|
||||
|
||||
|
||||
|
||||
# Force a complete clear & redraw of the screen
|
||||
force_redraw = ["f"]
|
||||
|
||||
#################
|
||||
# Custom Colors #
|
||||
#################
|
||||
|
||||
# Colors be listed as either;
|
||||
# 1) named ANSI: 'red', case insensitive, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
# 2) Hex values: '#FF0000', case insensitive
|
||||
# 3) 'reset' for transparency
|
||||
|
||||
# Some background/foreground combinations don't work, I *think* this is an issue/feature of ratatui - but I may have just made a mistake somewhere
|
||||
|
||||
# The single line bar at the uppermost of the display. Contains name/state/status headings etc
|
||||
[colors.headers_bar]
|
||||
# Background color of the entire line
|
||||
background = "magenta"
|
||||
# Animated loading icon at the start of the bar
|
||||
loading_spinner = "white"
|
||||
# Text color
|
||||
text = "black"
|
||||
# Text color of a selected header
|
||||
text_selected = "gray"
|
||||
|
||||
# The borders around the selectable panels - Containers, Commands, Logs
|
||||
[colors.borders]
|
||||
# Border when selected
|
||||
selected = "lightcyan"
|
||||
# Border when not selected
|
||||
unselected = "grey"
|
||||
|
||||
# The containers sections, in the future more color customization options should be made available in this section
|
||||
[colors.containers]
|
||||
# The icon use to illustrate which container is currently selected - at the moment the TUI library, ratatui, doesn't seem allow changing the color of the highlight symbol
|
||||
icon = "white"
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# At the moment, this will only change the color of the name, id, and image columns
|
||||
text = "blue"
|
||||
# Text color of the RX column
|
||||
text_rx = "#FFE9C1"
|
||||
# Text color of the TX column
|
||||
text_tx = "#CD8C8C"
|
||||
|
||||
# The logs panel, will only be applied if color_logs is false
|
||||
[colors.logs]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# text color
|
||||
text = "reset"
|
||||
|
||||
# Each state of a container has a color, which is used in multiple places, i.e. chart titles, state/status/cpu/memory columns in the container section
|
||||
[colors.container_state]
|
||||
dead = "red"
|
||||
exited = "red"
|
||||
paused = "yellow"
|
||||
removing = "lightred"
|
||||
restarting = "lightgreen"
|
||||
running_healthy = "green"
|
||||
running_unhealthy = "#FFB224"
|
||||
unknown = "red"
|
||||
|
||||
# The filter panel
|
||||
[colors.filter]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# color of text
|
||||
text = "gray"
|
||||
# background color of the selected filter by item (Name/Image/Status/All)
|
||||
selected_filter_background = "gray"
|
||||
# text color of the selected filter by item (Name/Image/Status/All)
|
||||
selected_filter_text = "black"
|
||||
# Highlighted text color
|
||||
highlight = "magenta"
|
||||
|
||||
|
||||
# The log search panel
|
||||
[colors.log_search]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# color of text
|
||||
text = "gray"
|
||||
# text color of the buttons text
|
||||
button_text = "black"
|
||||
# Highlighted text color
|
||||
highlight = "magenta"
|
||||
|
||||
# The color the of Docker commands available for each container
|
||||
[colors.commands]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
pause = "yellow"
|
||||
restart = "magenta"
|
||||
stop = "red"
|
||||
delete = "gray"
|
||||
resume = "blue"
|
||||
start = "green"
|
||||
|
||||
# The cpu chart
|
||||
[colors.chart_cpu]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border = "white"
|
||||
# Chart title - only whilst container is running, paused & stopped colors not yet customizable - or could just use state color?
|
||||
title = "green"
|
||||
# Maximum CPU percentage - again paused & stopped colors not yet customizable
|
||||
max = "#FFB224"
|
||||
# Points on the chart - again paused & stopped colors not yet customizable
|
||||
points = "magenta"
|
||||
# The charts y-axis
|
||||
y_axis = "white"
|
||||
|
||||
# The memory chart
|
||||
[colors.chart_memory]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border = "white"
|
||||
# Chart title - only whilst container is running, paused & stopped will use colors.container_state
|
||||
title = "green"
|
||||
# Maximum memory use - again paused & stopped will use colors.container_state
|
||||
max = "#FFB224"
|
||||
# Points on the chart - again paused & stopped will use colors.container_state
|
||||
points = "cyan"
|
||||
# The charts y-axis
|
||||
y_axis = "white"
|
||||
|
||||
# The bandwidth chart
|
||||
[colors.chart_bandwidth]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border = "white"
|
||||
# Maximum RX value - again paused & stopped colors not yet customizable
|
||||
max_rx = "#FFE9C1"
|
||||
# Maximum TX value - again paused & stopped colors not yet customizable
|
||||
max_tx = "#CD8C8C"
|
||||
# RX points on the chart - again paused & stopped colors not yet customizable
|
||||
points_rx = "#FFE9C1"
|
||||
# TX points on the chart - again paused & stopped colors not yet customizable
|
||||
points_tx = "#CD8C8C"
|
||||
# TX title color
|
||||
title_tx = "#FFE9C1"
|
||||
# RX title color
|
||||
title_rx = "#CD8C8C"
|
||||
# The charts y-axis
|
||||
y_axis = "white"
|
||||
|
||||
# The ports chart
|
||||
[colors.chart_ports]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border = "white"
|
||||
# Chart title - only whilst container is running, paused & stopped will use colors.container_state
|
||||
title = "green"
|
||||
# Private/Public/IP headings
|
||||
headings = "yellow"
|
||||
# Ports & IP listing text
|
||||
text = "white"
|
||||
|
||||
# The help popup
|
||||
[colors.popup_help]
|
||||
# Background color
|
||||
background = "magenta"
|
||||
# Text color
|
||||
text = "black"
|
||||
# Highlighted text color
|
||||
text_highlight = "white"
|
||||
|
||||
# The info popup - used to display small messages - such as saving logs to disk, or change of mouse capture settings
|
||||
[colors.popup_info]
|
||||
# Background color
|
||||
background = "blue"
|
||||
# Text color
|
||||
text = "white"
|
||||
|
||||
# The delete popup - used to display a confirmation warning when about to delete a container
|
||||
[colors.popup_delete]
|
||||
# Background color
|
||||
background = "white"
|
||||
# Text color
|
||||
text = "black"
|
||||
# Highlighted text color
|
||||
text_highlight = "red"
|
||||
|
||||
# The error popup - hopefully you'll never have to see this
|
||||
[colors.popup_error]
|
||||
# Background color
|
||||
background = "red"
|
||||
# Text color
|
||||
text = "white"
|
||||
@@ -0,0 +1,63 @@
|
||||
module github.com/oxker/oxker
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
charm.land/bubbletea/v2 v2.0.2
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
@@ -0,0 +1,157 @@
|
||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f h1:UytXHv0UxnsDFmL/7Z9Q5SBYPwSuRLXHbwx+6LycZ2w=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241212170349-ad4b7ae0f25f/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY=
|
||||
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
|
||||
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
UNAME_CMD="$(uname -m)"
|
||||
case "$UNAME_CMD" in
|
||||
x86_64) SUFFIX="x86_64" ;;
|
||||
aarch64) SUFFIX="aarch64" ;;
|
||||
armv6l) SUFFIX="armv6" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$SUFFIX" ]; then
|
||||
OXKER_GZ="oxker_linux_${SUFFIX}.tar.gz"
|
||||
curl -L -O "https://github.com/mrjackwills/oxker/releases/latest/download/${OXKER_GZ}"
|
||||
tar xzvf "${OXKER_GZ}" oxker
|
||||
install -Dm 755 oxker -t "${HOME}/.local/bin"
|
||||
rm "${OXKER_GZ}" oxker
|
||||
fi
|
||||
+3255
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,645 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type AppColors struct {
|
||||
Borders Borders `toml:"borders,omitempty" json:"borders,omitempty"`
|
||||
ChartCPU ChartCPU `toml:"chart_cpu,omitempty" json:"chart_cpu,omitempty"`
|
||||
ChartMemory ChartMemory `toml:"chart_memory,omitempty" json:"chart_memory,omitempty"`
|
||||
ChartBandwidth ChartBandwidth `toml:"chart_bandwidth,omitempty" json:"chart_bandwidth,omitempty"`
|
||||
ChartPorts ChartPorts `toml:"chart_ports,omitempty" json:"chart_ports,omitempty"`
|
||||
Commands Commands `toml:"commands,omitempty" json:"commands,omitempty"`
|
||||
ContainerState ContainerState `toml:"container_state,omitempty" json:"container_state,omitempty"`
|
||||
Containers Containers `toml:"containers,omitempty" json:"containers,omitempty"`
|
||||
LogSearch LogSearch `toml:"log_search,omitempty" json:"log_search,omitempty"`
|
||||
Filter Filter `toml:"filter,omitempty" json:"filter,omitempty"`
|
||||
HeadersBar HeadersBar `toml:"headers_bar,omitempty" json:"headers_bar,omitempty"`
|
||||
Logs Logs `toml:"logs,omitempty" json:"logs,omitempty"`
|
||||
PopupDelete PopupDelete `toml:"popup_delete,omitempty" json:"popup_delete,omitempty"`
|
||||
PopupError PopupError `toml:"popup_error,omitempty" json:"popup_error,omitempty"`
|
||||
PopupHelp PopupHelp `toml:"popup_help,omitempty" json:"popup_help,omitempty"`
|
||||
PopupInfo PopupInfo `toml:"popup_info,omitempty" json:"popup_info,omitempty"`
|
||||
}
|
||||
|
||||
type Borders struct {
|
||||
Selected string `toml:"selected,omitempty" json:"selected,omitempty"`
|
||||
Unselected string `toml:"unselected,omitempty" json:"unselected,omitempty"`
|
||||
}
|
||||
|
||||
type ChartCPU struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Border string `toml:"border,omitempty" json:"border,omitempty"`
|
||||
Title string `toml:"title,omitempty" json:"title,omitempty"`
|
||||
Max string `toml:"max,omitempty" json:"max,omitempty"`
|
||||
Points string `toml:"points,omitempty" json:"points,omitempty"`
|
||||
YAxis string `toml:"y_axis,omitempty" json:"y_axis,omitempty"`
|
||||
}
|
||||
|
||||
type ChartMemory struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Border string `toml:"border,omitempty" json:"border,omitempty"`
|
||||
Title string `toml:"title,omitempty" json:"title,omitempty"`
|
||||
Max string `toml:"max,omitempty" json:"max,omitempty"`
|
||||
Points string `toml:"points,omitempty" json:"points,omitempty"`
|
||||
YAxis string `toml:"y_axis,omitempty" json:"y_axis,omitempty"`
|
||||
}
|
||||
|
||||
type ChartBandwidth struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Border string `toml:"border,omitempty" json:"border,omitempty"`
|
||||
MaxRX string `toml:"max_rx,omitempty" json:"max_rx,omitempty"`
|
||||
MaxTX string `toml:"max_tx,omitempty" json:"max_tx,omitempty"`
|
||||
PointsRX string `toml:"points_rx,omitempty" json:"points_rx,omitempty"`
|
||||
PointsTX string `toml:"points_tx,omitempty" json:"points_tx,omitempty"`
|
||||
TitleRX string `toml:"title_rx,omitempty" json:"title_rx,omitempty"`
|
||||
TitleTX string `toml:"title_tx,omitempty" json:"title_tx,omitempty"`
|
||||
YAxis string `toml:"y_axis,omitempty" json:"y_axis,omitempty"`
|
||||
}
|
||||
|
||||
type ChartPorts struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Border string `toml:"border,omitempty" json:"border,omitempty"`
|
||||
Title string `toml:"title,omitempty" json:"title,omitempty"`
|
||||
Headings string `toml:"headings,omitempty" json:"headings,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type Commands struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Pause string `toml:"pause,omitempty" json:"pause,omitempty"`
|
||||
Restart string `toml:"restart,omitempty" json:"restart,omitempty"`
|
||||
Stop string `toml:"stop,omitempty" json:"stop,omitempty"`
|
||||
Delete string `toml:"delete,omitempty" json:"delete,omitempty"`
|
||||
Resume string `toml:"resume,omitempty" json:"resume,omitempty"`
|
||||
Start string `toml:"start,omitempty" json:"start,omitempty"`
|
||||
}
|
||||
|
||||
type ContainerState struct {
|
||||
Dead string `toml:"dead,omitempty" json:"dead,omitempty"`
|
||||
Exited string `toml:"exited,omitempty" json:"exited,omitempty"`
|
||||
Paused string `toml:"paused,omitempty" json:"paused,omitempty"`
|
||||
Removing string `toml:"removing,omitempty" json:"removing,omitempty"`
|
||||
Restarting string `toml:"restarting,omitempty" json:"restarting,omitempty"`
|
||||
RunningHealthy string `toml:"running_healthy,omitempty" json:"running_healthy,omitempty"`
|
||||
RunningUnhealthy string `toml:"running_unhealthy,omitempty" json:"running_unhealthy,omitempty"`
|
||||
Unknown string `toml:"unknown,omitempty" json:"unknown,omitempty"`
|
||||
}
|
||||
|
||||
type Containers struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Icon string `toml:"icon,omitempty" json:"icon,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
TextRX string `toml:"text_rx,omitempty" json:"text_rx,omitempty"`
|
||||
TextTX string `toml:"text_tx,omitempty" json:"text_tx,omitempty"`
|
||||
}
|
||||
|
||||
type LogSearch struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
ButtonText string `toml:"button_text,omitempty" json:"button_text,omitempty"`
|
||||
Highlight string `toml:"highlight,omitempty" json:"highlight,omitempty"`
|
||||
}
|
||||
|
||||
type Filter struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
SelectedFilterBackground string `toml:"selected_filter_background,omitempty" json:"selected_filter_background,omitempty"`
|
||||
SelectedFilterText string `toml:"selected_filter_text,omitempty" json:"selected_filter_text,omitempty"`
|
||||
Highlight string `toml:"highlight,omitempty" json:"highlight,omitempty"`
|
||||
}
|
||||
|
||||
type HeadersBar struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
LoadingSpinner string `toml:"loading_spinner,omitempty" json:"loading_spinner,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
TextSelected string `toml:"text_selected,omitempty" json:"text_selected,omitempty"`
|
||||
}
|
||||
|
||||
type Logs struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type PopupDelete struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
TextHighlight string `toml:"text_highlight,omitempty" json:"text_highlight,omitempty"`
|
||||
}
|
||||
|
||||
type PopupError struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type PopupHelp struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
TextHighlight string `toml:"text_highlight,omitempty" json:"text_highlight,omitempty"`
|
||||
}
|
||||
|
||||
type PopupInfo struct {
|
||||
Background string `toml:"background,omitempty" json:"background,omitempty"`
|
||||
Text string `toml:"text,omitempty" json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type Keymap struct {
|
||||
Clear []string `toml:"clear,omitempty" json:"clear,omitempty"`
|
||||
ClearSecondary []string `toml:"-" json:"-"`
|
||||
DeleteConfirm []string `toml:"delete_confirm,omitempty" json:"delete_confirm,omitempty"`
|
||||
DeleteConfirmSecondary []string `toml:"-" json:"-"`
|
||||
DeleteDeny []string `toml:"delete_deny,omitempty" json:"delete_deny,omitempty"`
|
||||
DeleteDenySecondary []string `toml:"-" json:"-"`
|
||||
Exec []string `toml:"exec,omitempty" json:"exec,omitempty"`
|
||||
ExecSecondary []string `toml:"-" json:"-"`
|
||||
FilterMode []string `toml:"filter_mode,omitempty" json:"filter_mode,omitempty"`
|
||||
FilterModeSecondary []string `toml:"-" json:"-"`
|
||||
Inspect []string `toml:"inspect,omitempty" json:"inspect,omitempty"`
|
||||
InspectSecondary []string `toml:"-" json:"-"`
|
||||
ForceRedraw []string `toml:"force_redraw,omitempty" json:"force_redraw,omitempty"`
|
||||
ForceRedrawSecondary []string `toml:"-" json:"-"`
|
||||
ScrollBack []string `toml:"scroll_back,omitempty" json:"scroll_back,omitempty"`
|
||||
ScrollBackSecondary []string `toml:"-" json:"-"`
|
||||
ScrollForward []string `toml:"scroll_forward,omitempty" json:"scroll_forward,omitempty"`
|
||||
ScrollForwardSecondary []string `toml:"-" json:"-"`
|
||||
LogSearchMode []string `toml:"log_search_mode,omitempty" json:"log_search_mode,omitempty"`
|
||||
LogSearchModeSecondary []string `toml:"-" json:"-"`
|
||||
LogSectionHeightDecrease []string `toml:"log_section_height_decrease,omitempty" json:"log_section_height_decrease,omitempty"`
|
||||
LogSectionHeightDecreaseSecondary []string `toml:"-" json:"-"`
|
||||
LogSectionHeightIncrease []string `toml:"log_section_height_increase,omitempty" json:"log_section_height_increase,omitempty"`
|
||||
LogSectionHeightIncreaseSecondary []string `toml:"-" json:"-"`
|
||||
LogSectionToggle []string `toml:"log_section_toggle,omitempty" json:"log_section_toggle,omitempty"`
|
||||
LogSectionToggleSecondary []string `toml:"-" json:"-"`
|
||||
Quit []string `toml:"quit,omitempty" json:"quit,omitempty"`
|
||||
QuitSecondary []string `toml:"-" json:"-"`
|
||||
SaveLogs []string `toml:"save_logs,omitempty" json:"save_logs,omitempty"`
|
||||
SaveLogsSecondary []string `toml:"-" json:"-"`
|
||||
ScrollDown []string `toml:"scroll_down,omitempty" json:"scroll_down,omitempty"`
|
||||
ScrollDownSecondary []string `toml:"-" json:"-"`
|
||||
ScrollEnd []string `toml:"scroll_end,omitempty" json:"scroll_end,omitempty"`
|
||||
ScrollEndSecondary []string `toml:"-" json:"-"`
|
||||
ScrollStart []string `toml:"scroll_start,omitempty" json:"scroll_start,omitempty"`
|
||||
ScrollStartSecondary []string `toml:"-" json:"-"`
|
||||
ScrollUp []string `toml:"scroll_up,omitempty" json:"scroll_up,omitempty"`
|
||||
ScrollUpSecondary []string `toml:"-" json:"-"`
|
||||
SelectNextPanel []string `toml:"select_next_panel,omitempty" json:"select_next_panel,omitempty"`
|
||||
SelectNextPanelSecondary []string `toml:"-" json:"-"`
|
||||
SelectPreviousPanel []string `toml:"select_previous_panel,omitempty" json:"select_previous_panel,omitempty"`
|
||||
SelectPreviousPanelSecondary []string `toml:"-" json:"-"`
|
||||
SortByName []string `toml:"sort_by_name,omitempty" json:"sort_by_name,omitempty"`
|
||||
SortByNameSecondary []string `toml:"-" json:"-"`
|
||||
SortByState []string `toml:"sort_by_state,omitempty" json:"sort_by_state,omitempty"`
|
||||
SortByStateSecondary []string `toml:"-" json:"-"`
|
||||
SortByStatus []string `toml:"sort_by_status,omitempty" json:"sort_by_status,omitempty"`
|
||||
SortByStatusSecondary []string `toml:"-" json:"-"`
|
||||
SortByCPU []string `toml:"sort_by_cpu,omitempty" json:"sort_by_cpu,omitempty"`
|
||||
SortByCPUSecondary []string `toml:"-" json:"-"`
|
||||
SortByMemory []string `toml:"sort_by_memory,omitempty" json:"sort_by_memory,omitempty"`
|
||||
SortByMemorySecondary []string `toml:"-" json:"-"`
|
||||
SortByID []string `toml:"sort_by_id,omitempty" json:"sort_by_id,omitempty"`
|
||||
SortByIDSecondary []string `toml:"-" json:"-"`
|
||||
SortByImage []string `toml:"sort_by_image,omitempty" json:"sort_by_image,omitempty"`
|
||||
SortByImageSecondary []string `toml:"-" json:"-"`
|
||||
SortByRX []string `toml:"sort_by_rx,omitempty" json:"sort_by_rx,omitempty"`
|
||||
SortByRXSecondary []string `toml:"-" json:"-"`
|
||||
SortByTX []string `toml:"sort_by_tx,omitempty" json:"sort_by_tx,omitempty"`
|
||||
SortByTXSecondary []string `toml:"-" json:"-"`
|
||||
SortReset []string `toml:"sort_reset,omitempty" json:"sort_reset,omitempty"`
|
||||
SortResetSecondary []string `toml:"-" json:"-"`
|
||||
ToggleHelp []string `toml:"toggle_help,omitempty" json:"toggle_help,omitempty"`
|
||||
ToggleHelpSecondary []string `toml:"-" json:"-"`
|
||||
ToggleMouseCapture []string `toml:"toggle_mouse_capture,omitempty" json:"toggle_mouse_capture,omitempty"`
|
||||
ToggleMouseCaptureSecondary []string `toml:"-" json:"-"`
|
||||
LogSectionToggleAlt []string `toml:"-" json:"-"`
|
||||
ScrollMany string `toml:"scroll_many,omitempty" json:"scroll_many,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
AppColors AppColors `toml:"colors,omitempty" json:"colors,omitempty"`
|
||||
ColorLogs bool `toml:"color_logs,omitempty" json:"color_logs,omitempty"`
|
||||
DirSave string `toml:"save_dir,omitempty" json:"save_dir,omitempty"`
|
||||
DirConfig string `toml:"-" json:"-"`
|
||||
DockerIntervalMs int `toml:"docker_interval,omitempty" json:"docker_interval,omitempty"`
|
||||
GUI bool `toml:"gui,omitempty" json:"gui,omitempty"`
|
||||
Host string `toml:"host,omitempty" json:"host,omitempty"`
|
||||
InContainer bool `toml:"-" json:"-"`
|
||||
Keymap Keymap `toml:"keymap,omitempty" json:"keymap,omitempty"`
|
||||
LogSearchCaseSensitive bool `toml:"log_search_case_sensitive,omitempty" json:"log_search_case_sensitive,omitempty"`
|
||||
RawLogs bool `toml:"raw_logs,omitempty" json:"raw_logs,omitempty"`
|
||||
ShowLogs bool `toml:"show_logs,omitempty" json:"show_logs,omitempty"`
|
||||
ShowSelf bool `toml:"show_self,omitempty" json:"show_self,omitempty"`
|
||||
ShowStdErr bool `toml:"show_std_err,omitempty" json:"show_std_err,omitempty"`
|
||||
ShowTimestamp bool `toml:"show_timestamp,omitempty" json:"show_timestamp,omitempty"`
|
||||
TimestampFormat string `toml:"timestamp_format,omitempty" json:"timestamp_format,omitempty"`
|
||||
Timezone string `toml:"timezone,omitempty" json:"timezone,omitempty"`
|
||||
UseCLI bool `toml:"use_cli,omitempty" json:"use_cli,omitempty"`
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
DockerIntervalMs: 1000,
|
||||
ShowLogs: true,
|
||||
ColorLogs: false,
|
||||
RawLogs: false,
|
||||
ShowTimestamp: true,
|
||||
TimestampFormat: "%Y-%m-%dT%H:%M:%S.%8f",
|
||||
Timezone: "UTC",
|
||||
UseCLI: false,
|
||||
GUI: true,
|
||||
LogSearchCaseSensitive: true,
|
||||
ShowSelf: false,
|
||||
ShowStdErr: true,
|
||||
DirSave: "",
|
||||
Host: "",
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfigPath(path string, cfg *Config) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
|
||||
if strings.HasSuffix(path, ".toml") {
|
||||
_, err = toml.Decode(content, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if strings.HasSuffix(path, ".jsonc") || strings.HasSuffix(path, ".json") {
|
||||
err = parseJSONC(content, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cfg.DirConfig = path
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
cfg := NewConfig()
|
||||
err := LoadConfigPath(path, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func parseJSONC(content string, cfg *Config) error {
|
||||
var raw map[string]interface{}
|
||||
commentsRemoved := removeJSONCComments(content)
|
||||
err := json.Unmarshal([]byte(commentsRemoved), &raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if colors, ok := raw["colors"].(map[string]interface{}); ok {
|
||||
cfg.AppColors = parseAppColors(colors)
|
||||
}
|
||||
|
||||
if keymap, ok := raw["keymap"].(map[string]interface{}); ok {
|
||||
cfg.Keymap = parseKeymap(keymap)
|
||||
}
|
||||
|
||||
if val, ok := raw["color_logs"].(bool); ok {
|
||||
cfg.ColorLogs = val
|
||||
}
|
||||
if val, ok := raw["docker_interval"].(float64); ok {
|
||||
cfg.DockerIntervalMs = int(val)
|
||||
}
|
||||
if val, ok := raw["gui"].(bool); ok {
|
||||
cfg.GUI = val
|
||||
}
|
||||
if val, ok := raw["host"].(string); ok {
|
||||
cfg.Host = val
|
||||
}
|
||||
if val, ok := raw["log_search_case_sensitive"].(bool); ok {
|
||||
cfg.LogSearchCaseSensitive = val
|
||||
}
|
||||
if val, ok := raw["raw_logs"].(bool); ok {
|
||||
cfg.RawLogs = val
|
||||
}
|
||||
if val, ok := raw["save_dir"].(string); ok {
|
||||
cfg.DirSave = val
|
||||
}
|
||||
if val, ok := raw["show_logs"].(bool); ok {
|
||||
cfg.ShowLogs = val
|
||||
}
|
||||
if val, ok := raw["show_self"].(bool); ok {
|
||||
cfg.ShowSelf = val
|
||||
}
|
||||
if val, ok := raw["show_std_err"].(bool); ok {
|
||||
cfg.ShowStdErr = val
|
||||
}
|
||||
if val, ok := raw["show_timestamp"].(bool); ok {
|
||||
cfg.ShowTimestamp = val
|
||||
}
|
||||
if val, ok := raw["timestamp_format"].(string); ok {
|
||||
cfg.TimestampFormat = val
|
||||
}
|
||||
if val, ok := raw["timezone"].(string); ok {
|
||||
cfg.Timezone = val
|
||||
}
|
||||
if val, ok := raw["use_cli"].(bool); ok {
|
||||
cfg.UseCLI = val
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeJSONCComments(content string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") || strings.HasPrefix(trimmed, "*/") {
|
||||
continue
|
||||
}
|
||||
if idx := strings.Index(trimmed, "//"); idx >= 0 {
|
||||
result = append(result, strings.TrimSpace(line[:idx]))
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func parseAppColors(colors map[string]interface{}) AppColors {
|
||||
acc := AppColors{}
|
||||
if borders, ok := colors["borders"].(map[string]interface{}); ok {
|
||||
acc.Borders = Borders{
|
||||
Selected: toString(borders["selected"]),
|
||||
Unselected: toString(borders["unselected"]),
|
||||
}
|
||||
}
|
||||
if chartCPU, ok := colors["chart_cpu"].(map[string]interface{}); ok {
|
||||
acc.ChartCPU = ChartCPU{
|
||||
Background: toString(chartCPU["background"]),
|
||||
Border: toString(chartCPU["border"]),
|
||||
Title: toString(chartCPU["title"]),
|
||||
Max: toString(chartCPU["max"]),
|
||||
Points: toString(chartCPU["points"]),
|
||||
YAxis: toString(chartCPU["y_axis"]),
|
||||
}
|
||||
}
|
||||
if chartMemory, ok := colors["chart_memory"].(map[string]interface{}); ok {
|
||||
acc.ChartMemory = ChartMemory{
|
||||
Background: toString(chartMemory["background"]),
|
||||
Border: toString(chartMemory["border"]),
|
||||
Title: toString(chartMemory["title"]),
|
||||
Max: toString(chartMemory["max"]),
|
||||
Points: toString(chartMemory["points"]),
|
||||
YAxis: toString(chartMemory["y_axis"]),
|
||||
}
|
||||
}
|
||||
if chartBandwidth, ok := colors["chart_bandwidth"].(map[string]interface{}); ok {
|
||||
acc.ChartBandwidth = ChartBandwidth{
|
||||
Background: toString(chartBandwidth["background"]),
|
||||
Border: toString(chartBandwidth["border"]),
|
||||
MaxRX: toString(chartBandwidth["max_rx"]),
|
||||
MaxTX: toString(chartBandwidth["max_tx"]),
|
||||
PointsRX: toString(chartBandwidth["points_rx"]),
|
||||
PointsTX: toString(chartBandwidth["points_tx"]),
|
||||
TitleRX: toString(chartBandwidth["title_rx"]),
|
||||
TitleTX: toString(chartBandwidth["title_tx"]),
|
||||
YAxis: toString(chartBandwidth["y_axis"]),
|
||||
}
|
||||
}
|
||||
if chartPorts, ok := colors["chart_ports"].(map[string]interface{}); ok {
|
||||
acc.ChartPorts = ChartPorts{
|
||||
Background: toString(chartPorts["background"]),
|
||||
Border: toString(chartPorts["border"]),
|
||||
Title: toString(chartPorts["title"]),
|
||||
Headings: toString(chartPorts["headings"]),
|
||||
Text: toString(chartPorts["text"]),
|
||||
}
|
||||
}
|
||||
if commands, ok := colors["commands"].(map[string]interface{}); ok {
|
||||
acc.Commands = Commands{
|
||||
Background: toString(commands["background"]),
|
||||
Pause: toString(commands["pause"]),
|
||||
Restart: toString(commands["restart"]),
|
||||
Stop: toString(commands["stop"]),
|
||||
Delete: toString(commands["delete"]),
|
||||
Resume: toString(commands["resume"]),
|
||||
Start: toString(commands["start"]),
|
||||
}
|
||||
}
|
||||
if containerState, ok := colors["container_state"].(map[string]interface{}); ok {
|
||||
acc.ContainerState = ContainerState{
|
||||
Dead: toString(containerState["dead"]),
|
||||
Exited: toString(containerState["exited"]),
|
||||
Paused: toString(containerState["paused"]),
|
||||
Removing: toString(containerState["removing"]),
|
||||
Restarting: toString(containerState["restarting"]),
|
||||
RunningHealthy: toString(containerState["running_healthy"]),
|
||||
RunningUnhealthy: toString(containerState["running_unhealthy"]),
|
||||
Unknown: toString(containerState["unknown"]),
|
||||
}
|
||||
}
|
||||
if containers, ok := colors["containers"].(map[string]interface{}); ok {
|
||||
acc.Containers = Containers{
|
||||
Background: toString(containers["background"]),
|
||||
Icon: toString(containers["icon"]),
|
||||
Text: toString(containers["text"]),
|
||||
TextRX: toString(containers["text_rx"]),
|
||||
TextTX: toString(containers["text_tx"]),
|
||||
}
|
||||
}
|
||||
if logSearch, ok := colors["log_search"].(map[string]interface{}); ok {
|
||||
acc.LogSearch = LogSearch{
|
||||
Background: toString(logSearch["background"]),
|
||||
Text: toString(logSearch["text"]),
|
||||
ButtonText: toString(logSearch["button_text"]),
|
||||
Highlight: toString(logSearch["highlight"]),
|
||||
}
|
||||
}
|
||||
if filter, ok := colors["filter"].(map[string]interface{}); ok {
|
||||
acc.Filter = Filter{
|
||||
Background: toString(filter["background"]),
|
||||
Text: toString(filter["text"]),
|
||||
SelectedFilterBackground: toString(filter["selected_filter_background"]),
|
||||
SelectedFilterText: toString(filter["selected_filter_text"]),
|
||||
Highlight: toString(filter["highlight"]),
|
||||
}
|
||||
}
|
||||
if headersBar, ok := colors["headers_bar"].(map[string]interface{}); ok {
|
||||
acc.HeadersBar = HeadersBar{
|
||||
Background: toString(headersBar["background"]),
|
||||
LoadingSpinner: toString(headersBar["loading_spinner"]),
|
||||
Text: toString(headersBar["text"]),
|
||||
TextSelected: toString(headersBar["text_selected"]),
|
||||
}
|
||||
}
|
||||
if logs, ok := colors["logs"].(map[string]interface{}); ok {
|
||||
acc.Logs = Logs{
|
||||
Background: toString(logs["background"]),
|
||||
Text: toString(logs["text"]),
|
||||
}
|
||||
}
|
||||
if popupDelete, ok := colors["popup_delete"].(map[string]interface{}); ok {
|
||||
acc.PopupDelete = PopupDelete{
|
||||
Background: toString(popupDelete["background"]),
|
||||
Text: toString(popupDelete["text"]),
|
||||
TextHighlight: toString(popupDelete["text_highlight"]),
|
||||
}
|
||||
}
|
||||
if popupError, ok := colors["popup_error"].(map[string]interface{}); ok {
|
||||
acc.PopupError = PopupError{
|
||||
Background: toString(popupError["background"]),
|
||||
Text: toString(popupError["text"]),
|
||||
}
|
||||
}
|
||||
if popupHelp, ok := colors["popup_help"].(map[string]interface{}); ok {
|
||||
acc.PopupHelp = PopupHelp{
|
||||
Background: toString(popupHelp["background"]),
|
||||
Text: toString(popupHelp["text"]),
|
||||
TextHighlight: toString(popupHelp["text_highlight"]),
|
||||
}
|
||||
}
|
||||
if popupInfo, ok := colors["popup_info"].(map[string]interface{}); ok {
|
||||
acc.PopupInfo = PopupInfo{
|
||||
Background: toString(popupInfo["background"]),
|
||||
Text: toString(popupInfo["text"]),
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
func parseKeymap(keymap map[string]interface{}) Keymap {
|
||||
k := Keymap{}
|
||||
applyKeymapEntry(&k.Clear, keymap, "clear")
|
||||
applyKeymapEntry(&k.DeleteConfirm, keymap, "delete_confirm")
|
||||
applyKeymapEntry(&k.DeleteDeny, keymap, "delete_deny")
|
||||
applyKeymapEntry(&k.Exec, keymap, "exec")
|
||||
applyKeymapEntry(&k.FilterMode, keymap, "filter_mode")
|
||||
applyKeymapEntry(&k.Inspect, keymap, "inspect")
|
||||
applyKeymapEntry(&k.ForceRedraw, keymap, "force_redraw")
|
||||
applyKeymapEntry(&k.ScrollBack, keymap, "scroll_back")
|
||||
applyKeymapEntry(&k.ScrollForward, keymap, "scroll_forward")
|
||||
applyKeymapEntry(&k.LogSearchMode, keymap, "log_search_mode")
|
||||
applyKeymapEntry(&k.LogSectionHeightDecrease, keymap, "log_section_height_decrease")
|
||||
applyKeymapEntry(&k.LogSectionHeightIncrease, keymap, "log_section_height_increase")
|
||||
applyKeymapEntry(&k.LogSectionToggle, keymap, "log_section_toggle")
|
||||
applyKeymapEntry(&k.Quit, keymap, "quit")
|
||||
applyKeymapEntry(&k.SaveLogs, keymap, "save_logs")
|
||||
applyKeymapEntry(&k.ScrollDown, keymap, "scroll_down")
|
||||
applyKeymapEntry(&k.ScrollEnd, keymap, "scroll_end")
|
||||
applyKeymapEntry(&k.ScrollStart, keymap, "scroll_start")
|
||||
applyKeymapEntry(&k.ScrollUp, keymap, "scroll_up")
|
||||
applyKeymapEntry(&k.SelectNextPanel, keymap, "select_next_panel")
|
||||
applyKeymapEntry(&k.SelectPreviousPanel, keymap, "select_previous_panel")
|
||||
applyKeymapEntry(&k.SortByName, keymap, "sort_by_name")
|
||||
applyKeymapEntry(&k.SortByState, keymap, "sort_by_state")
|
||||
applyKeymapEntry(&k.SortByStatus, keymap, "sort_by_status")
|
||||
applyKeymapEntry(&k.SortByCPU, keymap, "sort_by_cpu")
|
||||
applyKeymapEntry(&k.SortByMemory, keymap, "sort_by_memory")
|
||||
applyKeymapEntry(&k.SortByID, keymap, "sort_by_id")
|
||||
applyKeymapEntry(&k.SortByImage, keymap, "sort_by_image")
|
||||
applyKeymapEntry(&k.SortByRX, keymap, "sort_by_rx")
|
||||
applyKeymapEntry(&k.SortByTX, keymap, "sort_by_tx")
|
||||
applyKeymapEntry(&k.SortReset, keymap, "sort_reset")
|
||||
applyKeymapEntry(&k.ToggleHelp, keymap, "toggle_help")
|
||||
applyKeymapEntry(&k.ToggleMouseCapture, keymap, "toggle_mouse_capture")
|
||||
if scrollMany, ok := keymap["scroll_many"].(string); ok {
|
||||
k.ScrollMany = scrollMany
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func applyKeymapEntry(target *[]string, keymap map[string]interface{}, key string) {
|
||||
if val, ok := keymap[key].([]interface{}); ok {
|
||||
for _, v := range val {
|
||||
if s, ok := v.(string); ok {
|
||||
*target = append(*target, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toString(val interface{}) string {
|
||||
if s, ok := val.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Config) SaveConfig() error {
|
||||
path := c.DirConfig
|
||||
if path == "" {
|
||||
var err error
|
||||
path, err = GetDefaultConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if strings.HasSuffix(path, ".toml") {
|
||||
return toml.NewEncoder(file).Encode(c)
|
||||
} else if strings.HasSuffix(path, ".jsonc") || strings.HasSuffix(path, ".json") {
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = file.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetDefaultConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
configDir := filepath.Join(home, ".config", "oxker")
|
||||
return filepath.Join(configDir, "config.toml"), nil
|
||||
}
|
||||
|
||||
func (c *Config) MergeCLIArgs(cliArgs *CLIArgs) {
|
||||
if cliArgs == nil {
|
||||
return
|
||||
}
|
||||
if cliArgs.DockerIntervalMs > 0 {
|
||||
c.DockerIntervalMs = cliArgs.DockerIntervalMs
|
||||
}
|
||||
c.RawLogs = cliArgs.RawLogs
|
||||
c.ColorLogs = cliArgs.ColorLogs
|
||||
c.ShowTimestamp = !cliArgs.ShowTimestamp
|
||||
c.UseCLI = cliArgs.UseCLI
|
||||
c.GUI = !cliArgs.DebugMode
|
||||
if cliArgs.Host != "" {
|
||||
c.Host = cliArgs.Host
|
||||
}
|
||||
c.DirConfig = cliArgs.ConfigFile
|
||||
}
|
||||
|
||||
type CLIArgs struct {
|
||||
DockerIntervalMs int
|
||||
RawLogs bool
|
||||
ColorLogs bool
|
||||
ShowTimestamp bool
|
||||
ShowSelf bool
|
||||
DebugMode bool
|
||||
ConfigFile string
|
||||
Host string
|
||||
NoStdErr bool
|
||||
SaveDir string
|
||||
Timezone string
|
||||
UseCLI bool
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cli *client.Client
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func New(host ...string) (*Client, error) {
|
||||
opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
|
||||
if len(host) > 0 && host[0] != "" {
|
||||
opts = append(opts, client.WithHost(host[0]))
|
||||
}
|
||||
cli, err := client.NewClientWithOpts(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Client{cli: cli, ctx: ctx, cancel: cancel}, nil
|
||||
}
|
||||
|
||||
// Cancel cancels all in-flight Docker API calls.
|
||||
func (c *Client) Cancel() {
|
||||
c.cancel()
|
||||
}
|
||||
|
||||
func (c *Client) Ping() error {
|
||||
_, err := c.cli.Ping(c.ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) ListContainers(all bool) ([]types.Container, error) {
|
||||
opts := container.ListOptions{All: all}
|
||||
return c.cli.ContainerList(c.ctx, opts)
|
||||
}
|
||||
|
||||
func (c *Client) InspectContainer(id string) (types.ContainerJSON, error) {
|
||||
return c.cli.ContainerInspect(c.ctx, id)
|
||||
}
|
||||
|
||||
func (c *Client) StartContainer(id string) error {
|
||||
return c.cli.ContainerStart(c.ctx, id, container.StartOptions{})
|
||||
}
|
||||
|
||||
func (c *Client) StopContainer(id string) error {
|
||||
return c.cli.ContainerStop(c.ctx, id, container.StopOptions{})
|
||||
}
|
||||
|
||||
func (c *Client) RestartContainer(id string) error {
|
||||
return c.cli.ContainerRestart(c.ctx, id, container.StopOptions{})
|
||||
}
|
||||
|
||||
func (c *Client) DeleteContainer(id string, force bool) error {
|
||||
return c.cli.ContainerRemove(c.ctx, id, container.RemoveOptions{Force: force})
|
||||
}
|
||||
|
||||
func (c *Client) PauseContainer(id string) error {
|
||||
return c.cli.ContainerPause(c.ctx, id)
|
||||
}
|
||||
|
||||
func (c *Client) UnpauseContainer(id string) error {
|
||||
return c.cli.ContainerUnpause(c.ctx, id)
|
||||
}
|
||||
|
||||
func (c *Client) Logs(id string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
return c.cli.ContainerLogs(c.ctx, id, options)
|
||||
}
|
||||
|
||||
// ContainerStats gets a single stats snapshot for a container.
|
||||
// Uses stream=false (NOT one-shot) so Docker fills in precpu_stats for accurate CPU delta.
|
||||
func (c *Client) ContainerStats(id string) (*container.StatsResponse, error) {
|
||||
resp, err := c.cli.ContainerStats(c.ctx, id, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var stats container.StatsResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
func (c *Client) ContainerExec(id string, cmd []string, tty bool) (types.HijackedResponse, error) {
|
||||
execConfig := container.ExecOptions{
|
||||
Cmd: cmd,
|
||||
Tty: tty,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
exec, err := c.cli.ContainerExecCreate(c.ctx, id, execConfig)
|
||||
if err != nil {
|
||||
return types.HijackedResponse{}, err
|
||||
}
|
||||
return c.cli.ContainerExecAttach(c.ctx, exec.ID, container.ExecStartOptions{})
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.cli.Close()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RateLimiter struct {
|
||||
lastTime time.Time
|
||||
minInterval time.Duration
|
||||
}
|
||||
|
||||
func NewRateLimiter(interval time.Duration) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
minInterval: interval,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RateLimiter) Allow() bool {
|
||||
now := time.Now()
|
||||
if now.Sub(r.lastTime) >= r.minInterval {
|
||||
r.lastTime = now
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *RateLimiter) Reset() {
|
||||
r.lastTime = time.Time{}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
type ScrollDirection int
|
||||
|
||||
const (
|
||||
ScrollUp ScrollDirection = iota
|
||||
ScrollDown
|
||||
ScrollPageUp
|
||||
ScrollPageDown
|
||||
ScrollHome
|
||||
ScrollEnd
|
||||
)
|
||||
|
||||
func (sd ScrollDirection) String() string {
|
||||
switch sd {
|
||||
case ScrollUp:
|
||||
return "up"
|
||||
case ScrollDown:
|
||||
return "down"
|
||||
case ScrollPageUp:
|
||||
return "pageup"
|
||||
case ScrollPageDown:
|
||||
return "pagedown"
|
||||
case ScrollHome:
|
||||
return "home"
|
||||
case ScrollEnd:
|
||||
return "end"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
type ContainerStats struct {
|
||||
ID string
|
||||
CPU float64
|
||||
Memory uint64
|
||||
MemoryLimit uint64
|
||||
RX uint64
|
||||
TX uint64
|
||||
}
|
||||
|
||||
func ContainerStatsFromDocker(stats interface{}, id string) *ContainerStats {
|
||||
var cpu float64
|
||||
var mem, memLimit, rx, tx uint64
|
||||
|
||||
return &ContainerStats{
|
||||
ID: id,
|
||||
CPU: cpu,
|
||||
Memory: mem,
|
||||
MemoryLimit: memLimit,
|
||||
RX: rx,
|
||||
TX: tx,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
)
|
||||
|
||||
func FormatTable(data [][]string) string {
|
||||
var sb strings.Builder
|
||||
table := tablewriter.NewWriter(&sb)
|
||||
table.SetHeader([]string{})
|
||||
table.SetBorder(false)
|
||||
table.SetColMinWidth(0, 10)
|
||||
table.SetColMinWidth(1, 20)
|
||||
table.SetColMinWidth(2, 25)
|
||||
table.SetColMinWidth(3, 15)
|
||||
table.SetColMinWidth(4, 20)
|
||||
|
||||
for _, row := range data {
|
||||
table.Append(row)
|
||||
}
|
||||
|
||||
table.Render()
|
||||
return sb.String()
|
||||
}
|
||||
Submodule
+1
Submodule source/oxker added at e020eb157c
File diff suppressed because it is too large
Load Diff
-2571
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
use crate::app_data::DockerCommand;
|
||||
use std::fmt;
|
||||
|
||||
/// app errors to set in global state
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum AppError {
|
||||
DockerCommand(DockerCommand),
|
||||
DockerExec,
|
||||
DockerLogs,
|
||||
DockerConnect,
|
||||
IO(String),
|
||||
MouseCapture(bool),
|
||||
Parse(String),
|
||||
Terminal,
|
||||
}
|
||||
|
||||
/// Convert errors into strings to display
|
||||
impl fmt::Display for AppError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::DockerCommand(s) => write!(f, "Unable to {s} container"),
|
||||
Self::DockerExec => write!(f, "Unable to exec into container"),
|
||||
Self::DockerLogs => write!(f, "Unable to save logs"),
|
||||
Self::DockerConnect => write!(f, "Unable to access docker daemon"),
|
||||
Self::IO(msg) => write!(f, "IO error with: {msg}"),
|
||||
Self::MouseCapture(x) => {
|
||||
let reason = if *x { "en" } else { "dis" };
|
||||
write!(f, "Unable to {reason}able mouse capture")
|
||||
}
|
||||
Self::Parse(msg) => write!(f, "Parsing error: {msg}"),
|
||||
Self::Terminal => write!(f, "Unable to fully render to terminal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,551 +0,0 @@
|
||||
use ratatui::style::Color;
|
||||
|
||||
static COLOR_RX: Color = Color::Rgb(255, 233, 193);
|
||||
static COLOR_TX: Color = Color::Rgb(205, 140, 140);
|
||||
|
||||
/// The macro accepts a list of struct names with key names
|
||||
/// Returns a struct where every key name is an Option<String>, with the correct derived attributes
|
||||
macro_rules! optional_config_struct {
|
||||
($($struct_name:ident, $($key_name:ident),*);*) => {
|
||||
$(
|
||||
#[derive(Debug, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
struct $struct_name {
|
||||
$(
|
||||
$key_name: Option<String>,
|
||||
)*
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
/// The macro accepts a list of struct names with key names
|
||||
macro_rules! config_struct {
|
||||
($($struct_name:ident, $($key_name:ident),*);*) => {
|
||||
$(
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub struct $struct_name {
|
||||
$(
|
||||
pub $key_name: Color,
|
||||
)*
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl AppColors {
|
||||
fn map_color(color_str: Option<&str>, setter: &mut Color) {
|
||||
color_str.map(|i| i.parse::<Color>().map(|i| *setter = i));
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<ConfigColors>> for AppColors {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn from(value: Option<ConfigColors>) -> Self {
|
||||
let mut app_colors = Self::new();
|
||||
|
||||
if let Some(config_colors) = value {
|
||||
// Heading bar
|
||||
if let Some(hb) = config_colors.headers_bar {
|
||||
Self::map_color(
|
||||
hb.background.as_deref(),
|
||||
&mut app_colors.headers_bar.background,
|
||||
);
|
||||
Self::map_color(
|
||||
hb.loading_spinner.as_deref(),
|
||||
&mut app_colors.headers_bar.loading_spinner,
|
||||
);
|
||||
Self::map_color(hb.text.as_deref(), &mut app_colors.headers_bar.text);
|
||||
Self::map_color(
|
||||
hb.text_selected.as_deref(),
|
||||
&mut app_colors.headers_bar.text_selected,
|
||||
);
|
||||
}
|
||||
|
||||
// Selectable panel borders
|
||||
if let Some(b) = config_colors.borders {
|
||||
Self::map_color(b.selected.as_deref(), &mut app_colors.borders.selected);
|
||||
Self::map_color(b.unselected.as_deref(), &mut app_colors.borders.unselected);
|
||||
}
|
||||
|
||||
// Error Popup
|
||||
if let Some(ep) = config_colors.popup_error {
|
||||
Self::map_color(
|
||||
ep.background.as_deref(),
|
||||
&mut app_colors.popup_error.background,
|
||||
);
|
||||
Self::map_color(ep.text.as_deref(), &mut app_colors.popup_error.text);
|
||||
}
|
||||
|
||||
// Filter panel
|
||||
if let Some(fc) = config_colors.filter {
|
||||
Self::map_color(fc.background.as_deref(), &mut app_colors.filter.background);
|
||||
Self::map_color(fc.highlight.as_deref(), &mut app_colors.filter.highlight);
|
||||
|
||||
Self::map_color(
|
||||
fc.selected_filter_background.as_deref(),
|
||||
&mut app_colors.filter.selected_filter_background,
|
||||
);
|
||||
Self::map_color(
|
||||
fc.selected_filter_text.as_deref(),
|
||||
&mut app_colors.filter.selected_filter_text,
|
||||
);
|
||||
Self::map_color(fc.text.as_deref(), &mut app_colors.filter.text);
|
||||
}
|
||||
|
||||
// Log search
|
||||
if let Some(ls) = config_colors.log_search {
|
||||
Self::map_color(
|
||||
ls.background.as_deref(),
|
||||
&mut app_colors.log_search.background,
|
||||
);
|
||||
Self::map_color(
|
||||
ls.highlight.as_deref(),
|
||||
&mut app_colors.log_search.highlight,
|
||||
);
|
||||
|
||||
Self::map_color(
|
||||
ls.button_text.as_deref(),
|
||||
&mut app_colors.log_search.button_text,
|
||||
);
|
||||
Self::map_color(ls.text.as_deref(), &mut app_colors.log_search.text);
|
||||
}
|
||||
|
||||
// Help Popup
|
||||
if let Some(hp) = config_colors.popup_help {
|
||||
Self::map_color(
|
||||
hp.background.as_deref(),
|
||||
&mut app_colors.popup_help.background,
|
||||
);
|
||||
Self::map_color(hp.text.as_deref(), &mut app_colors.popup_help.text);
|
||||
Self::map_color(
|
||||
hp.text_highlight.as_deref(),
|
||||
&mut app_colors.popup_help.text_highlight,
|
||||
);
|
||||
}
|
||||
|
||||
// Info Popup
|
||||
if let Some(ip) = config_colors.popup_info {
|
||||
Self::map_color(
|
||||
ip.background.as_deref(),
|
||||
&mut app_colors.popup_info.background,
|
||||
);
|
||||
Self::map_color(ip.text.as_deref(), &mut app_colors.popup_info.text);
|
||||
}
|
||||
|
||||
// Delete Popup
|
||||
if let Some(dp) = config_colors.popup_delete {
|
||||
Self::map_color(
|
||||
dp.background.as_deref(),
|
||||
&mut app_colors.popup_delete.background,
|
||||
);
|
||||
Self::map_color(dp.text.as_deref(), &mut app_colors.popup_delete.text);
|
||||
Self::map_color(
|
||||
dp.text_highlight.as_deref(),
|
||||
&mut app_colors.popup_delete.text_highlight,
|
||||
);
|
||||
}
|
||||
|
||||
// Chart Cpu
|
||||
if let Some(cc) = config_colors.chart_cpu {
|
||||
Self::map_color(
|
||||
cc.background.as_deref(),
|
||||
&mut app_colors.chart_cpu.background,
|
||||
);
|
||||
Self::map_color(cc.border.as_deref(), &mut app_colors.chart_cpu.border);
|
||||
Self::map_color(cc.max.as_deref(), &mut app_colors.chart_cpu.max);
|
||||
Self::map_color(cc.points.as_deref(), &mut app_colors.chart_cpu.points);
|
||||
Self::map_color(cc.title.as_deref(), &mut app_colors.chart_cpu.title);
|
||||
Self::map_color(cc.y_axis.as_deref(), &mut app_colors.chart_cpu.y_axis);
|
||||
}
|
||||
|
||||
// Chart Memory
|
||||
if let Some(cm) = config_colors.chart_memory {
|
||||
Self::map_color(
|
||||
cm.background.as_deref(),
|
||||
&mut app_colors.chart_memory.background,
|
||||
);
|
||||
Self::map_color(cm.border.as_deref(), &mut app_colors.chart_memory.border);
|
||||
Self::map_color(cm.max.as_deref(), &mut app_colors.chart_memory.max);
|
||||
Self::map_color(cm.points.as_deref(), &mut app_colors.chart_memory.points);
|
||||
Self::map_color(cm.title.as_deref(), &mut app_colors.chart_memory.title);
|
||||
Self::map_color(cm.y_axis.as_deref(), &mut app_colors.chart_memory.y_axis);
|
||||
}
|
||||
|
||||
// Chart ports
|
||||
if let Some(cp) = config_colors.chart_ports {
|
||||
Self::map_color(
|
||||
cp.background.as_deref(),
|
||||
&mut app_colors.chart_ports.background,
|
||||
);
|
||||
Self::map_color(cp.border.as_deref(), &mut app_colors.chart_ports.border);
|
||||
Self::map_color(cp.headings.as_deref(), &mut app_colors.chart_ports.headings);
|
||||
Self::map_color(cp.text.as_deref(), &mut app_colors.chart_ports.text);
|
||||
Self::map_color(cp.title.as_deref(), &mut app_colors.chart_ports.title);
|
||||
}
|
||||
|
||||
// Containers
|
||||
if let Some(c) = config_colors.containers {
|
||||
Self::map_color(
|
||||
c.background.as_deref(),
|
||||
&mut app_colors.containers.background,
|
||||
);
|
||||
Self::map_color(c.icon.as_deref(), &mut app_colors.containers.icon);
|
||||
Self::map_color(c.text.as_deref(), &mut app_colors.containers.text);
|
||||
Self::map_color(c.text_rx.as_deref(), &mut app_colors.containers.text_rx);
|
||||
Self::map_color(c.text_tx.as_deref(), &mut app_colors.containers.text_tx);
|
||||
}
|
||||
|
||||
// Commands
|
||||
if let Some(cc) = config_colors.commands {
|
||||
Self::map_color(
|
||||
cc.background.as_deref(),
|
||||
&mut app_colors.commands.background,
|
||||
);
|
||||
Self::map_color(cc.pause.as_deref(), &mut app_colors.commands.pause);
|
||||
Self::map_color(cc.restart.as_deref(), &mut app_colors.commands.restart);
|
||||
Self::map_color(cc.stop.as_deref(), &mut app_colors.commands.stop);
|
||||
Self::map_color(cc.delete.as_deref(), &mut app_colors.commands.start);
|
||||
Self::map_color(cc.resume.as_deref(), &mut app_colors.commands.resume);
|
||||
Self::map_color(cc.start.as_deref(), &mut app_colors.commands.start);
|
||||
}
|
||||
|
||||
// Logs panel
|
||||
if let Some(cl) = config_colors.logs {
|
||||
Self::map_color(cl.background.as_deref(), &mut app_colors.logs.background);
|
||||
Self::map_color(cl.text.as_deref(), &mut app_colors.logs.text);
|
||||
}
|
||||
|
||||
// Container State
|
||||
if let Some(cs) = config_colors.container_state {
|
||||
Self::map_color(cs.dead.as_deref(), &mut app_colors.container_state.dead);
|
||||
Self::map_color(cs.exited.as_deref(), &mut app_colors.container_state.exited);
|
||||
Self::map_color(cs.paused.as_deref(), &mut app_colors.container_state.paused);
|
||||
Self::map_color(
|
||||
cs.removing.as_deref(),
|
||||
&mut app_colors.container_state.removing,
|
||||
);
|
||||
Self::map_color(
|
||||
cs.restarting.as_deref(),
|
||||
&mut app_colors.container_state.restarting,
|
||||
);
|
||||
Self::map_color(
|
||||
cs.running_healthy.as_deref(),
|
||||
&mut app_colors.container_state.running_healthy,
|
||||
);
|
||||
Self::map_color(
|
||||
cs.running_unhealthy.as_deref(),
|
||||
&mut app_colors.container_state.running_unhealthy,
|
||||
);
|
||||
Self::map_color(
|
||||
cs.unknown.as_deref(),
|
||||
&mut app_colors.container_state.unknown,
|
||||
);
|
||||
}
|
||||
}
|
||||
app_colors
|
||||
}
|
||||
}
|
||||
|
||||
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||
|
||||
optional_config_struct!(
|
||||
ConfigBackgroundText, background, text;
|
||||
ConfigBackgroundTextHighlight, background, text, text_highlight;
|
||||
ConfigBorders, selected, unselected;
|
||||
ConfigChartBandwidth, background, border, max_rx, max_tx, title_tx, title_rx, points_rx, points_tx, y_axis;
|
||||
|
||||
ConfigChartCpu, background, border, order, title, max, points,y_axis;
|
||||
ConfigChartMemory, background, border, title, max, points, y_axis;
|
||||
ConfigChartPorts, background, border, title, headings, text;
|
||||
ConfigCommands, background, pause, restart, stop, delete, resume, start;
|
||||
ConfigContainers, background, icon, text, text_rx, text_tx;
|
||||
ConfigContainerState, background, dead, exited, paused, removing, restarting, running_healthy, running_unhealthy, unknown;
|
||||
ConfigFilter, background, text, selected_filter_background, selected_filter_text, highlight;
|
||||
ConfigLogSearch, background, text, button_text, highlight;
|
||||
ConfigHeadersBar, background, loading_spinner, text, text_selected;
|
||||
ConfigLogs, background, text
|
||||
);
|
||||
|
||||
config_struct!(
|
||||
Borders, selected, unselected;
|
||||
ChartCpu, background, border, title, max, points, y_axis;
|
||||
ChartMemory, background, border, title, max, points, y_axis;
|
||||
ChartBandwidth, background, border, max_rx, max_tx, title_rx, title_tx, points_rx, points_tx, y_axis;
|
||||
|
||||
ChartPorts, background, border, title, headings, text;
|
||||
Commands, background, pause, restart, stop, delete, resume, start;
|
||||
Containers, background, icon, text, text_rx, text_tx;
|
||||
ContainerState, dead, exited, paused, removing, restarting, running_healthy, running_unhealthy, unknown;
|
||||
Filter, background, text, selected_filter_background, selected_filter_text, highlight;
|
||||
LogSearch, background, text, button_text, highlight;
|
||||
HeadersBar, background, text_selected, loading_spinner, text;
|
||||
Logs, background, text;
|
||||
PopupDelete, background, text, text_highlight;
|
||||
PopupError, background, text;
|
||||
PopupHelp, background, text, text_highlight;
|
||||
PopupInfo, background, text
|
||||
);
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct ConfigColors {
|
||||
borders: Option<ConfigBorders>,
|
||||
chart_cpu: Option<ConfigChartCpu>,
|
||||
chart_memory: Option<ConfigChartMemory>,
|
||||
chart_bandwidth: Option<ConfigChartBandwidth>,
|
||||
chart_ports: Option<ConfigChartPorts>,
|
||||
commands: Option<ConfigCommands>,
|
||||
container_state: Option<ConfigContainerState>,
|
||||
containers: Option<ConfigContainers>,
|
||||
filter: Option<ConfigFilter>,
|
||||
log_search: Option<ConfigLogSearch>,
|
||||
headers_bar: Option<ConfigHeadersBar>,
|
||||
logs: Option<ConfigLogs>,
|
||||
popup_delete: Option<ConfigBackgroundTextHighlight>,
|
||||
popup_error: Option<ConfigBackgroundText>,
|
||||
popup_help: Option<ConfigBackgroundTextHighlight>,
|
||||
popup_info: Option<ConfigBackgroundText>,
|
||||
}
|
||||
|
||||
/// Default colours for the header bar
|
||||
impl HeadersBar {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Magenta,
|
||||
loading_spinner: Color::White,
|
||||
text: Color::Black,
|
||||
text_selected: Color::Gray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the borders
|
||||
impl Borders {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
selected: Color::LightCyan,
|
||||
unselected: Color::Gray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the delete popup
|
||||
impl Commands {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
pause: Color::Yellow,
|
||||
restart: Color::Magenta,
|
||||
stop: Color::Red,
|
||||
delete: Color::Gray,
|
||||
resume: Color::Blue,
|
||||
start: Color::Green,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the Bandwidth chart
|
||||
impl ChartBandwidth {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
border: Color::White,
|
||||
max_rx: COLOR_RX,
|
||||
title_rx: COLOR_RX,
|
||||
title_tx: COLOR_TX,
|
||||
max_tx: COLOR_TX,
|
||||
points_rx: COLOR_RX,
|
||||
points_tx: COLOR_TX,
|
||||
y_axis: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the CPU chart
|
||||
impl ChartCpu {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
border: Color::White,
|
||||
title: Color::Green,
|
||||
max: ORANGE,
|
||||
points: Color::Magenta,
|
||||
y_axis: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the help popup
|
||||
impl ChartMemory {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
border: Color::White,
|
||||
title: Color::Green,
|
||||
max: ORANGE,
|
||||
points: Color::Cyan,
|
||||
y_axis: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the help popup
|
||||
impl ChartPorts {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
border: Color::White,
|
||||
title: Color::Green,
|
||||
headings: Color::Yellow,
|
||||
text: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the help popup
|
||||
impl Containers {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
icon: Color::White,
|
||||
text: Color::Blue,
|
||||
text_rx: COLOR_RX,
|
||||
text_tx: COLOR_TX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the help popup
|
||||
impl ContainerState {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
paused: Color::Yellow,
|
||||
removing: Color::LightRed,
|
||||
restarting: Color::LightGreen,
|
||||
running_healthy: Color::Green,
|
||||
running_unhealthy: ORANGE,
|
||||
dead: Color::Red,
|
||||
exited: Color::Red,
|
||||
unknown: Color::Red,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the filter panel
|
||||
impl Filter {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
highlight: Color::Magenta,
|
||||
selected_filter_background: Color::Gray,
|
||||
selected_filter_text: Color::Black,
|
||||
text: Color::Gray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the log search
|
||||
impl LogSearch {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
highlight: Color::Magenta,
|
||||
button_text: Color::Black,
|
||||
text: Color::Gray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the logs panel, only applied if color_logs is false
|
||||
impl Logs {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
text: Color::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the Error popup
|
||||
impl PopupError {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Red,
|
||||
text: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the info popup
|
||||
impl PopupInfo {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Blue,
|
||||
text: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the help popup
|
||||
impl PopupHelp {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Magenta,
|
||||
text: Color::Black,
|
||||
text_highlight: Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the delete popup
|
||||
impl PopupDelete {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::White,
|
||||
text: Color::Black,
|
||||
text_highlight: Color::Red,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub struct AppColors {
|
||||
pub borders: Borders,
|
||||
pub chart_cpu: ChartCpu,
|
||||
pub chart_memory: ChartMemory,
|
||||
pub chart_bandwidth: ChartBandwidth,
|
||||
pub chart_ports: ChartPorts,
|
||||
pub commands: Commands,
|
||||
pub container_state: ContainerState,
|
||||
pub containers: Containers,
|
||||
pub log_search: LogSearch,
|
||||
pub filter: Filter,
|
||||
pub headers_bar: HeadersBar,
|
||||
pub logs: Logs,
|
||||
pub popup_delete: PopupDelete,
|
||||
pub popup_error: PopupError,
|
||||
pub popup_help: PopupHelp,
|
||||
pub popup_info: PopupInfo,
|
||||
}
|
||||
|
||||
impl AppColors {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
borders: Borders::new(),
|
||||
chart_cpu: ChartCpu::new(),
|
||||
chart_memory: ChartMemory::new(),
|
||||
chart_bandwidth: ChartBandwidth::new(),
|
||||
chart_ports: ChartPorts::new(),
|
||||
commands: Commands::new(),
|
||||
container_state: ContainerState::new(),
|
||||
containers: Containers::new(),
|
||||
log_search: LogSearch::new(),
|
||||
filter: Filter::new(),
|
||||
headers_bar: HeadersBar::new(),
|
||||
logs: Logs::new(),
|
||||
popup_delete: PopupDelete::new(),
|
||||
popup_error: PopupError::new(),
|
||||
popup_help: PopupHelp::new(),
|
||||
popup_info: PopupInfo::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
# oxker config file
|
||||
# oxker will also read .jsonc and .json files which use the same key/value structure & format as this file
|
||||
# Every key is optional, with defaults that oxker will choose if missing or invalid
|
||||
# The `--config-file` cli argument can be used to load configuration files from any readable location
|
||||
|
||||
# Docker update interval in ms, minimum effectively 1000
|
||||
docker_interval = 1000
|
||||
|
||||
# Attempt to colorize the logs, conflicts with "raw"
|
||||
color_logs = false
|
||||
|
||||
# Show raw logs, default is to remove ansi formatting, conflicts with "color"
|
||||
raw_logs = false
|
||||
|
||||
# Show self (the oxker container) when running as a docker container
|
||||
show_self = false
|
||||
|
||||
# Show std_err in logs
|
||||
show_std_err = true
|
||||
|
||||
# Show a timestamp for every log entry
|
||||
show_timestamp = true
|
||||
|
||||
# Don't draw gui - for debugging - mostly pointless
|
||||
gui = true
|
||||
|
||||
# Docker host location. Will take priority over a DOCKER_HOST env.
|
||||
# host = "/var/run/docker.sock"
|
||||
|
||||
# Display the container logs timestamp with a given timezone, if timezone is unknown, defaults to UTC
|
||||
timezone = "Etc/UTC"
|
||||
|
||||
# Display the timestamp in a custom format, if given option is invalid, it will default to %Y-%m-%dT%H:%M:%S.%8f -> 2025-02-18T12:34:56.012345678Z
|
||||
# *Should* accept any valid strftime string up to 32 chars, see https://strftime.org/
|
||||
timestamp_format = "%Y-%m-%dT%H:%M:%S.%8f"
|
||||
|
||||
# Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
||||
# save_dir = "$HOME"
|
||||
|
||||
# Force use of docker cli when execing into containers, honestly mostly pointless
|
||||
use_cli = false
|
||||
|
||||
# Show the logs section - this can be changed during operation with the log_section_toggle key
|
||||
show_logs = true
|
||||
|
||||
# Use case-sensitive matching for logs
|
||||
log_search_case_sensitive = true
|
||||
|
||||
#################
|
||||
# Custom Keymap #
|
||||
#################
|
||||
|
||||
# Available keys are;
|
||||
# 1) a-z and A-Z
|
||||
# 2) 0-9
|
||||
# WARNING if using the \ key, it needs to be escaped, e.g. log_section_toggle = ["\\"]
|
||||
# 3) / \ , . # ' [ ] ; = -
|
||||
# 3) F1-F12
|
||||
# 4) backspace, tab, backtab, delete, end, esc, home, insert, pagedown, pageup, left, right, up, down
|
||||
|
||||
# Each definition can have two keys associated with it
|
||||
|
||||
# WARNING "scroll_many" only accepts control, alt, shift, with no secondary option
|
||||
|
||||
# If any key clashes are found, oxker will revert to it's default keymap
|
||||
|
||||
[keymap]
|
||||
# Clear any popup boxes, filter panel, or help panel
|
||||
clear = ["c", "esc"]
|
||||
# Cancel delete - clear also works here
|
||||
delete_deny = ["n"]
|
||||
# Confirm Delete
|
||||
delete_confirm = ["y"]
|
||||
# Exec into the selected container
|
||||
exec = ["e"]
|
||||
# Enter filter mode
|
||||
filter_mode = ["/", "F1"]
|
||||
|
||||
# Enter log search mode
|
||||
log_search_mode = ["#"]
|
||||
|
||||
# Quit at anytime
|
||||
quit = ["q"]
|
||||
# Save logs of selected container to file on disk
|
||||
save_logs = ["s"]
|
||||
# scroll down a list by one item
|
||||
scroll_down = ["down", "j"]
|
||||
|
||||
# scroll down to the end of a list
|
||||
scroll_end = ["end"]
|
||||
# Modifier to scroll by 10 lines instead of one, used in conjunction with scroll_up/scroll_down
|
||||
scroll_many = ["control"]
|
||||
# scroll up to the start of a list
|
||||
scroll_start = ["home"]
|
||||
# scroll up a list by one item
|
||||
scroll_up = ["up", "k"]
|
||||
# Horizontal scroll of the logs
|
||||
scroll_forward = ["right"]
|
||||
scroll_back = ["left"]
|
||||
# Select next panel
|
||||
select_next_panel = ["tab"]
|
||||
# Select previous panel
|
||||
select_previous_panel = ["backtab"]
|
||||
# Sort the containers based on specific column
|
||||
sort_by_name = ["1"]
|
||||
sort_by_state = ["2"]
|
||||
sort_by_status = ["3"]
|
||||
sort_by_cpu = ["4"]
|
||||
sort_by_memory = ["5"]
|
||||
sort_by_id = ["6"]
|
||||
sort_by_image = ["7"]
|
||||
sort_by_rx = ["8"]
|
||||
sort_by_tx = ["9"]
|
||||
# Reset the sorted containers
|
||||
sort_reset = ["0"]
|
||||
# Toggle the help panel
|
||||
toggle_help = ["h"]
|
||||
# Toggle mouse capture
|
||||
toggle_mouse_capture = ["m"]
|
||||
# Reduce the height of the logs list section
|
||||
log_section_height_decrease = ["-"]
|
||||
log_section_height_increase = ["+"]
|
||||
# Toggle visibility of the log section
|
||||
log_section_toggle = ["\\"]
|
||||
# Toggle to inspect container screen
|
||||
inspect = ["i"]
|
||||
|
||||
# Force a complete clear & redraw of the screen
|
||||
force_redraw = ["f"]
|
||||
|
||||
#################
|
||||
# Custom Colors #
|
||||
#################
|
||||
|
||||
# Colors be listed as either;
|
||||
# 1) named ANSI: 'red', case insensitive, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
# 2) Hex values: '#FF0000', case insensitive
|
||||
# 3) 'reset' for transparency
|
||||
|
||||
# Some background/foreground combinations don't work, I *think* this is an issue/feature of ratatui - but I may have just made a mistake somewhere
|
||||
|
||||
# The single line bar at the uppermost of the display. Contains name/state/status headings etc
|
||||
[colors.headers_bar]
|
||||
# Background color of the entire line
|
||||
background = "magenta"
|
||||
# Animated loading icon at the start of the bar
|
||||
loading_spinner = "white"
|
||||
# Text color
|
||||
text = "black"
|
||||
# Text color of a selected header
|
||||
text_selected = "gray"
|
||||
|
||||
# The borders around the selectable panels - Containers, Commands, Logs
|
||||
[colors.borders]
|
||||
# Border when selected
|
||||
selected = "lightcyan"
|
||||
# Border when not selected
|
||||
unselected = "grey"
|
||||
|
||||
# The containers sections, in the future more color customization options should be made available in this section
|
||||
[colors.containers]
|
||||
# The icon use to illustrate which container is currently selected - at the moment the TUI library, ratatui, doesn't seem allow changing the color of the highlight symbol
|
||||
icon = "white"
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# At the moment, this will only change the color of the name, id, and image columns
|
||||
text = "blue"
|
||||
# Text color of the RX column
|
||||
text_rx = "#FFE9C1"
|
||||
# Text color of the TX column
|
||||
text_tx = "#CD8C8C"
|
||||
|
||||
# The logs panel, will only be applied if color_logs is false
|
||||
[colors.logs]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# text color
|
||||
text = "reset"
|
||||
|
||||
# Each state of a container has a color, which is used in multiple places, i.e. chart titles, state/status/cpu/memory columns in the container section
|
||||
[colors.container_state]
|
||||
dead = "red"
|
||||
exited = "red"
|
||||
paused = "yellow"
|
||||
removing = "lightred"
|
||||
restarting = "lightgreen"
|
||||
running_healthy = "green"
|
||||
running_unhealthy = "#FFB224"
|
||||
unknown = "red"
|
||||
|
||||
# The filter panel
|
||||
[colors.filter]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# color of text
|
||||
text = "gray"
|
||||
# background color of the selected filter by item (Name/Image/Status/All)
|
||||
selected_filter_background = "gray"
|
||||
# text color of the selected filter by item (Name/Image/Status/All)
|
||||
selected_filter_text = "black"
|
||||
# Highlighted text color
|
||||
highlight = "magenta"
|
||||
|
||||
|
||||
# The log search panel
|
||||
[colors.log_search]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# color of text
|
||||
text = "gray"
|
||||
# text color of the buttons text
|
||||
button_text = "black"
|
||||
# Highlighted text color
|
||||
highlight = "magenta"
|
||||
|
||||
# The color the of Docker commands available for each container
|
||||
[colors.commands]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
pause = "yellow"
|
||||
restart = "magenta"
|
||||
stop = "red"
|
||||
delete = "gray"
|
||||
resume = "blue"
|
||||
start = "green"
|
||||
|
||||
# The cpu chart
|
||||
[colors.chart_cpu]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border = "white"
|
||||
# Chart title - only whilst container is running, paused & stopped colors not yet customizable - or could just use state color?
|
||||
title = "green"
|
||||
# Maximum CPU percentage - again paused & stopped colors not yet customizable
|
||||
max = "#FFB224"
|
||||
# Points on the chart - again paused & stopped colors not yet customizable
|
||||
points = "magenta"
|
||||
# The charts y-axis
|
||||
y_axis = "white"
|
||||
|
||||
# The memory chart
|
||||
[colors.chart_memory]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border = "white"
|
||||
# Chart title - only whilst container is running, paused & stopped will use colors.container_state
|
||||
title = "green"
|
||||
# Maximum memory use - again paused & stopped will use colors.container_state
|
||||
max = "#FFB224"
|
||||
# Points on the chart - again paused & stopped will use colors.container_state
|
||||
points = "cyan"
|
||||
# The charts y-axis
|
||||
y_axis = "white"
|
||||
|
||||
# The bandwidth chart
|
||||
[colors.chart_bandwidth]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border = "white"
|
||||
# Maximum RX value - again paused & stopped colors not yet customizable
|
||||
max_rx = "#FFE9C1"
|
||||
# Maximum TX value - again paused & stopped colors not yet customizable
|
||||
max_tx = "#CD8C8C"
|
||||
# RX points on the chart - again paused & stopped colors not yet customizable
|
||||
points_rx = "#FFE9C1"
|
||||
# TX points on the chart - again paused & stopped colors not yet customizable
|
||||
points_tx = "#CD8C8C"
|
||||
# TX title color
|
||||
title_rx = "#FFE9C1"
|
||||
# RX title color
|
||||
title_tx = "#CD8C8C"
|
||||
# The charts y-axis
|
||||
y_axis = "white"
|
||||
|
||||
# The ports chart
|
||||
[colors.chart_ports]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border = "white"
|
||||
# Chart title - only whilst container is running, paused & stopped will use colors.container_state
|
||||
title = "green"
|
||||
# Private/Public/IP headings
|
||||
headings = "yellow"
|
||||
# Ports & IP listing text
|
||||
text = "white"
|
||||
|
||||
# The help popup
|
||||
[colors.popup_help]
|
||||
# Background color
|
||||
background = "magenta"
|
||||
# Text color
|
||||
text = "black"
|
||||
# Highlighted text color
|
||||
text_highlight = "white"
|
||||
|
||||
# The info popup - used to display small messages - such as saving logs to disk, or change of mouse capture settings
|
||||
[colors.popup_info]
|
||||
# Background color
|
||||
background = "blue"
|
||||
# Text color
|
||||
text = "white"
|
||||
|
||||
# The delete popup - used to display a confirmation warning when about to delete a container
|
||||
[colors.popup_delete]
|
||||
# Background color
|
||||
background = "white"
|
||||
# Text color
|
||||
text = "black"
|
||||
# Highlighted text color
|
||||
text_highlight = "red"
|
||||
|
||||
# The error popup - hopefully you'll never have to see this
|
||||
[colors.popup_error]
|
||||
# Background color
|
||||
background = "red"
|
||||
# Text color
|
||||
text = "white"
|
||||
@@ -1,514 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
|
||||
/// The macro accepts a list of struct names with key names
|
||||
/// Returns a struct where every key name is an Option<String>, with the correct derived attributes
|
||||
macro_rules! optional_config_struct {
|
||||
($($struct_name:ident, $($key_name:ident),*);*) => {
|
||||
$(
|
||||
#[derive(Debug, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct $struct_name {
|
||||
$(
|
||||
$key_name: Option<Vec<String>>,
|
||||
)*
|
||||
pub scroll_many: Option<Vec<String>>,
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
/// The macro accepts a list of struct names with key names
|
||||
/// Similar to the optional_config_struct macro as above, but returns struct where every key name is Color
|
||||
macro_rules! config_struct {
|
||||
($($struct_name:ident, $($key_name:ident),*);*) => {
|
||||
$(
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct $struct_name {
|
||||
$(
|
||||
pub $key_name: (KeyCode, Option<KeyCode>),
|
||||
)*
|
||||
pub scroll_many: KeyModifiers,
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
optional_config_struct!(
|
||||
ConfigKeymap,
|
||||
clear,
|
||||
delete_confirm,
|
||||
delete_deny,
|
||||
exec,
|
||||
filter_mode,
|
||||
force_redraw,
|
||||
inspect,
|
||||
scroll_back,
|
||||
scroll_forward,
|
||||
log_search_mode,
|
||||
log_section_height_decrease,
|
||||
log_section_height_increase,
|
||||
log_section_toggle,
|
||||
quit,
|
||||
save_logs,
|
||||
scroll_down,
|
||||
scroll_end,
|
||||
scroll_start,
|
||||
scroll_up,
|
||||
select_next_panel,
|
||||
select_previous_panel,
|
||||
sort_by_cpu,
|
||||
sort_by_id,
|
||||
sort_by_image,
|
||||
sort_by_memory,
|
||||
sort_by_name,
|
||||
sort_by_rx,
|
||||
sort_by_state,
|
||||
sort_by_status,
|
||||
sort_by_tx,
|
||||
sort_reset,
|
||||
toggle_help,
|
||||
toggle_mouse_capture
|
||||
);
|
||||
|
||||
config_struct!(
|
||||
Keymap,
|
||||
clear,
|
||||
delete_confirm,
|
||||
delete_deny,
|
||||
exec,
|
||||
filter_mode,
|
||||
inspect,
|
||||
force_redraw,
|
||||
scroll_back,
|
||||
scroll_forward,
|
||||
log_search_mode,
|
||||
log_section_height_decrease,
|
||||
log_section_height_increase,
|
||||
log_section_toggle,
|
||||
quit,
|
||||
save_logs,
|
||||
scroll_down,
|
||||
scroll_end,
|
||||
scroll_start,
|
||||
scroll_up,
|
||||
select_next_panel,
|
||||
select_previous_panel,
|
||||
sort_by_cpu,
|
||||
sort_by_id,
|
||||
sort_by_image,
|
||||
sort_by_memory,
|
||||
sort_by_name,
|
||||
sort_by_rx,
|
||||
sort_by_state,
|
||||
sort_by_status,
|
||||
sort_by_tx,
|
||||
sort_reset,
|
||||
toggle_help,
|
||||
toggle_mouse_capture
|
||||
);
|
||||
|
||||
impl Keymap {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
clear: (KeyCode::Char('c'), Some(KeyCode::Esc)),
|
||||
delete_confirm: (KeyCode::Char('y'), None),
|
||||
delete_deny: (KeyCode::Char('n'), None),
|
||||
exec: (KeyCode::Char('e'), None),
|
||||
inspect: (KeyCode::Char('i'), None),
|
||||
filter_mode: (KeyCode::Char('/'), Some(KeyCode::F(1))),
|
||||
force_redraw: (KeyCode::Char('f'), None),
|
||||
scroll_back: (KeyCode::Left, None),
|
||||
scroll_forward: (KeyCode::Right, None),
|
||||
log_search_mode: (KeyCode::Char('#'), None),
|
||||
log_section_height_decrease: (KeyCode::Char('-'), None),
|
||||
log_section_height_increase: (KeyCode::Char('='), None),
|
||||
log_section_toggle: (KeyCode::Char('\\'), None),
|
||||
quit: (KeyCode::Char('q'), None),
|
||||
save_logs: (KeyCode::Char('s'), None),
|
||||
scroll_down: (KeyCode::Down, Some(KeyCode::Char('j'))),
|
||||
scroll_end: (KeyCode::End, None),
|
||||
scroll_many: KeyModifiers::CONTROL,
|
||||
scroll_start: (KeyCode::Home, None),
|
||||
scroll_up: (KeyCode::Up, Some(KeyCode::Char('k'))),
|
||||
select_next_panel: (KeyCode::Tab, None),
|
||||
select_previous_panel: (KeyCode::BackTab, None),
|
||||
sort_by_cpu: (KeyCode::Char('4'), None),
|
||||
sort_by_id: (KeyCode::Char('6'), None),
|
||||
sort_by_image: (KeyCode::Char('7'), None),
|
||||
sort_by_memory: (KeyCode::Char('5'), None),
|
||||
sort_by_name: (KeyCode::Char('1'), None),
|
||||
sort_by_rx: (KeyCode::Char('8'), None),
|
||||
sort_by_state: (KeyCode::Char('2'), None),
|
||||
sort_by_status: (KeyCode::Char('3'), None),
|
||||
sort_by_tx: (KeyCode::Char('9'), None),
|
||||
sort_reset: (KeyCode::Char('0'), None),
|
||||
toggle_help: (KeyCode::Char('h'), None),
|
||||
toggle_mouse_capture: (KeyCode::Char('m'), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<ConfigKeymap>> for Keymap {
|
||||
/// Probably a better way to do this, but for now it works
|
||||
fn from(value: Option<ConfigKeymap>) -> Self {
|
||||
let mut keymap = Self::new();
|
||||
|
||||
let mut clash = HashSet::new();
|
||||
let mut counter = 0;
|
||||
|
||||
let mut update_keymap =
|
||||
|vec_str: Option<Vec<String>>,
|
||||
keymap_field: &mut (KeyCode, Option<KeyCode>),
|
||||
keymap_clash: &mut HashSet<KeyCode>| {
|
||||
if let Some(vec_str) = vec_str
|
||||
&& let Some(vec_keycode) = Self::try_parse_keycode(&vec_str)
|
||||
{
|
||||
if let Some(first) = vec_keycode.first() {
|
||||
keymap_clash.insert(*first);
|
||||
counter += 1;
|
||||
keymap_field.0 = *first;
|
||||
}
|
||||
if let Some(second) = vec_keycode.get(1) {
|
||||
keymap_clash.insert(*second);
|
||||
counter += 1;
|
||||
keymap_field.1 = Some(*second);
|
||||
} else {
|
||||
keymap_field.1 = None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ck) = value {
|
||||
update_keymap(ck.clear, &mut keymap.clear, &mut clash);
|
||||
update_keymap(ck.delete_deny, &mut keymap.delete_deny, &mut clash);
|
||||
update_keymap(ck.delete_confirm, &mut keymap.delete_confirm, &mut clash);
|
||||
update_keymap(
|
||||
ck.log_section_height_decrease,
|
||||
&mut keymap.log_section_height_decrease,
|
||||
&mut clash,
|
||||
);
|
||||
update_keymap(
|
||||
ck.log_section_height_increase,
|
||||
&mut keymap.log_section_height_increase,
|
||||
&mut clash,
|
||||
);
|
||||
update_keymap(
|
||||
ck.log_section_toggle,
|
||||
&mut keymap.log_section_toggle,
|
||||
&mut clash,
|
||||
);
|
||||
|
||||
update_keymap(ck.exec, &mut keymap.exec, &mut clash);
|
||||
update_keymap(ck.filter_mode, &mut keymap.filter_mode, &mut clash);
|
||||
update_keymap(ck.force_redraw, &mut keymap.force_redraw, &mut clash);
|
||||
update_keymap(ck.quit, &mut keymap.quit, &mut clash);
|
||||
update_keymap(ck.save_logs, &mut keymap.save_logs, &mut clash);
|
||||
update_keymap(ck.scroll_down, &mut keymap.scroll_down, &mut clash);
|
||||
update_keymap(ck.scroll_end, &mut keymap.scroll_end, &mut clash);
|
||||
update_keymap(ck.scroll_start, &mut keymap.scroll_start, &mut clash);
|
||||
update_keymap(ck.scroll_up, &mut keymap.scroll_up, &mut clash);
|
||||
update_keymap(ck.log_search_mode, &mut keymap.log_search_mode, &mut clash);
|
||||
update_keymap(ck.scroll_forward, &mut keymap.scroll_forward, &mut clash);
|
||||
update_keymap(ck.scroll_back, &mut keymap.scroll_back, &mut clash);
|
||||
update_keymap(
|
||||
ck.select_next_panel,
|
||||
&mut keymap.select_next_panel,
|
||||
&mut clash,
|
||||
);
|
||||
update_keymap(
|
||||
ck.select_previous_panel,
|
||||
&mut keymap.select_previous_panel,
|
||||
&mut clash,
|
||||
);
|
||||
update_keymap(ck.sort_by_name, &mut keymap.sort_by_name, &mut clash);
|
||||
update_keymap(ck.sort_by_state, &mut keymap.sort_by_state, &mut clash);
|
||||
update_keymap(ck.sort_by_status, &mut keymap.sort_by_status, &mut clash);
|
||||
update_keymap(ck.sort_by_cpu, &mut keymap.sort_by_cpu, &mut clash);
|
||||
update_keymap(ck.sort_by_memory, &mut keymap.sort_by_memory, &mut clash);
|
||||
update_keymap(ck.sort_by_id, &mut keymap.sort_by_id, &mut clash);
|
||||
update_keymap(ck.sort_by_image, &mut keymap.sort_by_image, &mut clash);
|
||||
update_keymap(ck.sort_by_rx, &mut keymap.sort_by_rx, &mut clash);
|
||||
update_keymap(ck.sort_by_tx, &mut keymap.sort_by_tx, &mut clash);
|
||||
update_keymap(ck.sort_reset, &mut keymap.sort_reset, &mut clash);
|
||||
update_keymap(ck.toggle_help, &mut keymap.toggle_help, &mut clash);
|
||||
update_keymap(
|
||||
ck.toggle_mouse_capture,
|
||||
&mut keymap.toggle_mouse_capture,
|
||||
&mut clash,
|
||||
);
|
||||
// TODO need to check for clashes when using additional modifiers
|
||||
if let Some(scroll_many) = Self::try_parse_modifier(ck.scroll_many) {
|
||||
keymap.scroll_many = scroll_many;
|
||||
}
|
||||
}
|
||||
// A very basic clash check, every key has been inserted into a hashset, and a counter has been increased
|
||||
// if the counter and hashet length don't match, then there's a clash, and we just return the default keymap
|
||||
if counter == clash.len() {
|
||||
keymap
|
||||
} else {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
// Allowable key modifiers are only `shift`, `control`, `alt`
|
||||
fn try_parse_modifier(input: Option<Vec<String>>) -> Option<KeyModifiers> {
|
||||
input.and_then(|input| {
|
||||
input
|
||||
.first()
|
||||
.and_then(|input| match input.to_lowercase().trim() {
|
||||
"control" => Some(KeyModifiers::CONTROL),
|
||||
"alt" => Some(KeyModifiers::ALT),
|
||||
"shift" => Some(KeyModifiers::SHIFT),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to parse a &[String] into a Vec of keycodes, at most the output will have 2 entries
|
||||
/// This might fail on MacOS due to Backspace and Delete working in a different manner as to how they work on Linux & Windows
|
||||
/// I think that on MacOS `Del` becomes `Fwd Del`, and `Backspace` becomes `Delete`
|
||||
fn try_parse_keycode(input: &[String]) -> Option<Vec<KeyCode>> {
|
||||
let mut output = vec![];
|
||||
|
||||
for key in input.iter().take(2) {
|
||||
if key.chars().count() == 1 {
|
||||
if let Some(first_char) = key.chars().next()
|
||||
&& let Some(first_char) = match first_char {
|
||||
x if x.is_ascii_alphabetic() || x.is_ascii_digit() => Some(first_char),
|
||||
'/' | '\\' | ',' | '.' | '#' | '\'' | '[' | ']' | ';' | '=' | '-' => {
|
||||
Some(first_char)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
{
|
||||
output.push(KeyCode::Char(first_char));
|
||||
}
|
||||
} else {
|
||||
let keycode = match key.to_lowercase().as_str() {
|
||||
"f1" => Some(KeyCode::F(1)),
|
||||
"f2" => Some(KeyCode::F(2)),
|
||||
"f3" => Some(KeyCode::F(3)),
|
||||
"f4" => Some(KeyCode::F(4)),
|
||||
"f5" => Some(KeyCode::F(5)),
|
||||
"f6" => Some(KeyCode::F(6)),
|
||||
"f7" => Some(KeyCode::F(7)),
|
||||
"f8" => Some(KeyCode::F(8)),
|
||||
"f9" => Some(KeyCode::F(9)),
|
||||
"f10" => Some(KeyCode::F(10)),
|
||||
"f11" => Some(KeyCode::F(11)),
|
||||
"f12" => Some(KeyCode::F(12)),
|
||||
// Might fail on MacOS, see note above
|
||||
"backspace" => Some(KeyCode::Backspace),
|
||||
"backtab" => Some(KeyCode::BackTab),
|
||||
// Might fail on MacOS, see note above
|
||||
"delete" => Some(KeyCode::Delete),
|
||||
"down" => Some(KeyCode::Down),
|
||||
"end" => Some(KeyCode::End),
|
||||
"esc" => Some(KeyCode::Esc),
|
||||
"home" => Some(KeyCode::Home),
|
||||
"insert" => Some(KeyCode::Insert),
|
||||
"left" => Some(KeyCode::Left),
|
||||
"pagedown" => Some(KeyCode::PageDown),
|
||||
"pageup" => Some(KeyCode::PageUp),
|
||||
"right" => Some(KeyCode::Right),
|
||||
"tab" => Some(KeyCode::Tab),
|
||||
"up" => Some(KeyCode::Up),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(a) = keycode {
|
||||
output.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
if output.is_empty() {
|
||||
None
|
||||
} else {
|
||||
// Remove any duplicates for a single definition
|
||||
if output.first() == output.get(1) {
|
||||
output.pop();
|
||||
}
|
||||
Some(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
|
||||
use crate::config::keymap_parser::ConfigKeymap;
|
||||
|
||||
use super::Keymap;
|
||||
|
||||
#[test]
|
||||
/// Only allow two definitions to be parsed
|
||||
fn test_return_max_two() {
|
||||
let result = Keymap::try_parse_keycode(&["a".to_owned(), "b".to_owned(), "c".to_owned()]);
|
||||
assert_eq!(result, Some(vec![KeyCode::Char('a'), KeyCode::Char('b')]));
|
||||
|
||||
let result = Keymap::try_parse_keycode(&["0".to_owned(), "1".to_owned(), "2".to_owned()]);
|
||||
assert_eq!(result, Some(vec![KeyCode::Char('0'), KeyCode::Char('1')]));
|
||||
|
||||
let result =
|
||||
Keymap::try_parse_keycode(&["esc".to_owned(), "tab".to_owned(), "backtab".to_owned()]);
|
||||
assert_eq!(result, Some(vec![KeyCode::Esc, KeyCode::Tab]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// If a single definition has two identical entries, just return a single entry
|
||||
fn test_duplicate_definition() {
|
||||
let result = Keymap::try_parse_keycode(&["c".to_owned(), "c".to_owned()]);
|
||||
assert_eq!(result, Some(vec![KeyCode::Char('c')]));
|
||||
|
||||
let result = Keymap::try_parse_keycode(&["0".to_owned(), "0".to_owned()]);
|
||||
assert_eq!(result, Some(vec![KeyCode::Char('0')]));
|
||||
|
||||
let result = Keymap::try_parse_keycode(&["esc".to_owned(), "esc".to_owned()]);
|
||||
assert_eq!(result, Some(vec![KeyCode::Esc]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Return None is invalid key definition is provided
|
||||
fn test_invalid_key() {
|
||||
let result = Keymap::try_parse_keycode(&["(".to_owned(), "*".to_owned()]);
|
||||
assert!(result.is_none());
|
||||
|
||||
let result = Keymap::try_parse_keycode(&["enter".to_owned(), "shift".to_owned()]);
|
||||
assert!(result.is_none());
|
||||
|
||||
let result = Keymap::try_parse_keycode(&["ö".to_owned(), "ä".to_owned()]);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// If any key definitions clash, just return the default keymap
|
||||
fn test_clash_returns_default() {
|
||||
let input = ConfigKeymap {
|
||||
clear: Some(vec!["s".to_owned()]),
|
||||
delete_deny: Some(vec!["s".to_owned()]),
|
||||
delete_confirm: None,
|
||||
exec: None,
|
||||
filter_mode: None,
|
||||
force_redraw: None,
|
||||
inspect: None,
|
||||
scroll_back: None,
|
||||
log_search_mode: None,
|
||||
scroll_forward: None,
|
||||
log_section_height_decrease: None,
|
||||
log_section_height_increase: None,
|
||||
log_section_toggle: None,
|
||||
quit: None,
|
||||
save_logs: None,
|
||||
scroll_down: None,
|
||||
scroll_end: None,
|
||||
scroll_start: None,
|
||||
scroll_many: None,
|
||||
scroll_up: None,
|
||||
select_next_panel: None,
|
||||
select_previous_panel: None,
|
||||
sort_by_cpu: None,
|
||||
sort_by_id: None,
|
||||
sort_by_image: None,
|
||||
sort_by_memory: None,
|
||||
sort_by_name: None,
|
||||
sort_by_rx: None,
|
||||
sort_by_state: None,
|
||||
sort_by_status: None,
|
||||
sort_by_tx: None,
|
||||
sort_reset: None,
|
||||
toggle_help: None,
|
||||
toggle_mouse_capture: None,
|
||||
};
|
||||
|
||||
let result = Keymap::from(Some(input));
|
||||
|
||||
assert_eq!(result, Keymap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap definition creation
|
||||
fn test_valid_custom_keymap() {
|
||||
let gen_v = |a: (&str, &str)| Some(vec![a.0.to_owned(), a.1.to_owned()]);
|
||||
|
||||
let input = ConfigKeymap {
|
||||
clear: gen_v(("a", "b")),
|
||||
delete_confirm: gen_v(("c", "d")),
|
||||
delete_deny: gen_v(("e", "f")),
|
||||
exec: gen_v(("g", "h")),
|
||||
filter_mode: gen_v(("i", "j")),
|
||||
force_redraw: gen_v(("k", "l")),
|
||||
inspect: gen_v(("m", "n")),
|
||||
scroll_back: gen_v(("s", "t")),
|
||||
scroll_forward: gen_v(("q", "r")),
|
||||
log_search_mode: gen_v(("1", "2")),
|
||||
log_section_height_decrease: gen_v(("m", "n")),
|
||||
log_section_height_increase: gen_v(("o", "p")),
|
||||
log_section_toggle: gen_v(("u", "v")),
|
||||
quit: gen_v(("w", "x")),
|
||||
save_logs: gen_v(("y", "z")),
|
||||
scroll_down: gen_v(("3", "4")),
|
||||
scroll_end: gen_v(("5", "6")),
|
||||
scroll_many: Some(vec!["alt".to_owned()]),
|
||||
scroll_start: gen_v(("7", "8")),
|
||||
scroll_up: gen_v(("F1", "F2")),
|
||||
select_next_panel: gen_v(("F3", "F4")),
|
||||
select_previous_panel: gen_v(("F5", "F6")),
|
||||
sort_by_cpu: gen_v(("F7", "F8")),
|
||||
sort_by_id: gen_v(("F9", "F10")),
|
||||
sort_by_image: gen_v(("F11", "F12")),
|
||||
sort_by_memory: gen_v(("HOME", "END")),
|
||||
sort_by_name: gen_v(("UP", "DOWN")),
|
||||
sort_by_rx: gen_v(("LEFT", "RIGHT")),
|
||||
sort_by_state: gen_v(("[", "]")),
|
||||
sort_by_status: gen_v(("INSERTt", "TAB")),
|
||||
sort_by_tx: gen_v(("PAGEDOWN", "PAGEUP")),
|
||||
sort_reset: gen_v((",", ".")),
|
||||
toggle_help: gen_v(("-", "=")),
|
||||
toggle_mouse_capture: gen_v(("\\", "/")),
|
||||
};
|
||||
|
||||
let result = Keymap::from(Some(input));
|
||||
|
||||
let expected = Keymap {
|
||||
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
||||
delete_confirm: (KeyCode::Char('c'), Some(KeyCode::Char('d'))),
|
||||
delete_deny: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
||||
exec: (KeyCode::Char('g'), Some(KeyCode::Char('h'))),
|
||||
filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))),
|
||||
force_redraw: (KeyCode::Char('k'), Some(KeyCode::Char('l'))),
|
||||
inspect: (KeyCode::Char('i'), None),
|
||||
scroll_back: (KeyCode::Char('s'), Some(KeyCode::Char('t'))),
|
||||
scroll_forward: (KeyCode::Char('q'), Some(KeyCode::Char('r'))),
|
||||
log_search_mode: (KeyCode::Char('1'), Some(KeyCode::Char('2'))),
|
||||
log_section_height_decrease: (KeyCode::Char('m'), Some(KeyCode::Char('n'))),
|
||||
log_section_height_increase: (KeyCode::Char('o'), Some(KeyCode::Char('p'))),
|
||||
log_section_toggle: (KeyCode::Char('u'), Some(KeyCode::Char('v'))),
|
||||
quit: (KeyCode::Char('w'), Some(KeyCode::Char('x'))),
|
||||
save_logs: (KeyCode::Char('y'), Some(KeyCode::Char('z'))),
|
||||
scroll_down: (KeyCode::Char('3'), Some(KeyCode::Char('4'))),
|
||||
scroll_end: (KeyCode::Char('5'), Some(KeyCode::Char('6'))),
|
||||
scroll_many: KeyModifiers::ALT,
|
||||
scroll_start: (KeyCode::Char('7'), Some(KeyCode::Char('8'))),
|
||||
scroll_up: (KeyCode::F(1), Some(KeyCode::F(2))),
|
||||
select_next_panel: (KeyCode::F(3), Some(KeyCode::F(4))),
|
||||
select_previous_panel: (KeyCode::F(5), Some(KeyCode::F(6))),
|
||||
sort_by_cpu: (KeyCode::F(7), Some(KeyCode::F(8))),
|
||||
sort_by_id: (KeyCode::F(9), Some(KeyCode::F(10))),
|
||||
sort_by_image: (KeyCode::F(11), Some(KeyCode::F(12))),
|
||||
sort_by_memory: (KeyCode::Home, Some(KeyCode::End)),
|
||||
sort_by_name: (KeyCode::Up, Some(KeyCode::Down)),
|
||||
sort_by_rx: (KeyCode::Left, Some(KeyCode::Right)),
|
||||
sort_by_state: (KeyCode::Char('['), Some(KeyCode::Char(']'))),
|
||||
sort_by_status: (KeyCode::Tab, None),
|
||||
sort_by_tx: (KeyCode::PageDown, Some(KeyCode::PageUp)),
|
||||
sort_reset: (KeyCode::Char(','), Some(KeyCode::Char('.'))),
|
||||
toggle_help: (KeyCode::Char('-'), Some(KeyCode::Char('='))),
|
||||
toggle_mouse_capture: (KeyCode::Char('\\'), Some(KeyCode::Char('/'))),
|
||||
};
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use jiff::tz::TimeZone;
|
||||
use parse_args::Args;
|
||||
use parse_config_file::ConfigFile;
|
||||
mod color_parser;
|
||||
mod keymap_parser;
|
||||
|
||||
use crate::{ENV_KEY, ENV_VALUE};
|
||||
pub use {color_parser::AppColors, keymap_parser::Keymap};
|
||||
|
||||
mod parse_args;
|
||||
mod parse_config_file;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct Config {
|
||||
pub app_colors: AppColors,
|
||||
pub color_logs: bool,
|
||||
pub docker_interval_ms: u32,
|
||||
pub gui: bool,
|
||||
pub host: Option<String>,
|
||||
pub in_container: bool,
|
||||
pub keymap: Keymap,
|
||||
pub log_search_case_sensitive: bool,
|
||||
pub raw_logs: bool,
|
||||
pub dir_config: Option<PathBuf>,
|
||||
pub dir_save: Option<PathBuf>,
|
||||
pub show_logs: bool,
|
||||
pub show_self: bool,
|
||||
pub show_std_err: bool,
|
||||
pub show_timestamp: bool,
|
||||
pub timestamp_format: String,
|
||||
pub timezone: Option<TimeZone>,
|
||||
pub use_cli: bool,
|
||||
}
|
||||
|
||||
impl From<&Args> for Config {
|
||||
fn from(args: &Args) -> Self {
|
||||
Self {
|
||||
app_colors: AppColors::new(),
|
||||
color_logs: args.color,
|
||||
docker_interval_ms: args.docker_interval,
|
||||
gui: !args.gui,
|
||||
host: args.host.clone(),
|
||||
in_container: Self::check_if_in_container(),
|
||||
keymap: Keymap::new(),
|
||||
log_search_case_sensitive: true,
|
||||
raw_logs: args.raw,
|
||||
dir_save: Self::try_get_logs_dir(args.save_dir.as_ref()),
|
||||
dir_config: args.config_file.as_ref().map(|i| PathBuf::from(&i)),
|
||||
show_logs: true,
|
||||
show_self: !args.show_self,
|
||||
show_std_err: !args.no_std_err,
|
||||
show_timestamp: !args.timestamp,
|
||||
timestamp_format: Self::parse_timestamp_format(None),
|
||||
timezone: Self::parse_timezone(args.timezone.clone()),
|
||||
use_cli: args.use_cli,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(ConfigFile, Option<PathBuf>)> for Config {
|
||||
fn from((config_file, dir): (ConfigFile, Option<PathBuf>)) -> Self {
|
||||
Self {
|
||||
app_colors: AppColors::from(config_file.colors),
|
||||
color_logs: config_file.color_logs.unwrap_or(false),
|
||||
docker_interval_ms: config_file.docker_interval.unwrap_or(1000),
|
||||
dir_config: dir,
|
||||
gui: config_file.gui.unwrap_or(true),
|
||||
host: config_file.host,
|
||||
in_container: Self::check_if_in_container(),
|
||||
keymap: Keymap::from(config_file.keymap),
|
||||
log_search_case_sensitive: config_file.log_search_case_sensitive.unwrap_or(true),
|
||||
raw_logs: config_file.raw_logs.unwrap_or(false),
|
||||
dir_save: Self::try_get_logs_dir(config_file.save_dir.as_ref()),
|
||||
show_logs: config_file.show_logs.unwrap_or(true),
|
||||
show_self: config_file.show_self.unwrap_or(false),
|
||||
show_std_err: config_file.show_std_err.unwrap_or(true),
|
||||
show_timestamp: config_file.show_timestamp.unwrap_or(true),
|
||||
timestamp_format: Self::parse_timestamp_format(config_file.timestamp_format),
|
||||
timezone: Self::parse_timezone(config_file.timezone),
|
||||
use_cli: config_file.use_cli.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// A basic timestampt format parser, will only take 32 chars, and checks if the parsed timestamp isn't identical to the given formatter
|
||||
fn parse_timestamp_format(input: Option<String>) -> String {
|
||||
let default = || "%Y-%m-%dT%H:%M:%S.%8f".to_owned();
|
||||
input.map_or_else(default, |input| {
|
||||
if input.chars().count() >= 32
|
||||
|| jiff::Timestamp::now().strftime(&input).to_string() == input
|
||||
{
|
||||
default()
|
||||
} else {
|
||||
input
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to parse a timezone into a jiff::tz::TimeZone
|
||||
/// Also return a format to display the timesampt in
|
||||
fn parse_timezone(input: Option<String>) -> Option<TimeZone> {
|
||||
let timezone_str = input?;
|
||||
let Ok(tz) = jiff::tz::TimeZone::get(&timezone_str) else {
|
||||
return None;
|
||||
};
|
||||
let current_ts = jiff::Timestamp::now();
|
||||
let offset = tz.to_offset(current_ts);
|
||||
if jiff::tz::TimeZone::UTC.to_offset(current_ts) == offset {
|
||||
None
|
||||
} else {
|
||||
Some(tz)
|
||||
}
|
||||
}
|
||||
/// Check if oxker is running inside of a container
|
||||
fn check_if_in_container() -> bool {
|
||||
std::env::var(ENV_KEY).is_ok_and(|i| i == ENV_VALUE)
|
||||
}
|
||||
|
||||
/// If a cli_arg is provided, create a pathbuf from it, else try to get home_dir automatically
|
||||
fn try_get_logs_dir(dir: Option<&String>) -> Option<PathBuf> {
|
||||
dir.as_ref()
|
||||
.map_or_else(Self::try_get_home_dir, |home_dir| {
|
||||
Some(std::path::Path::new(&home_dir).to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to get the home dir of the current user
|
||||
fn try_get_home_dir() -> Option<PathBuf> {
|
||||
directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned())
|
||||
}
|
||||
|
||||
/// Combine config from CLI into config file, the cli take priority
|
||||
/// and also make sure color_logs and raw_logs can't clash
|
||||
fn merge_args(mut self, config_from_cli: Self) -> Self {
|
||||
let default_args = Args::default();
|
||||
|
||||
if config_from_cli.color_logs != default_args.color {
|
||||
self.color_logs = config_from_cli.color_logs;
|
||||
self.raw_logs = !self.color_logs;
|
||||
}
|
||||
|
||||
if config_from_cli.raw_logs != default_args.raw {
|
||||
self.raw_logs = config_from_cli.raw_logs;
|
||||
self.color_logs = !self.raw_logs;
|
||||
}
|
||||
|
||||
if config_from_cli.gui != default_args.gui {
|
||||
self.gui = config_from_cli.gui;
|
||||
}
|
||||
|
||||
if config_from_cli.docker_interval_ms != default_args.docker_interval {
|
||||
self.docker_interval_ms = config_from_cli.docker_interval_ms;
|
||||
}
|
||||
|
||||
if config_from_cli.docker_interval_ms < 1000 {
|
||||
self.docker_interval_ms = default_args.docker_interval;
|
||||
}
|
||||
|
||||
if config_from_cli.raw_logs != default_args.raw {
|
||||
self.raw_logs = config_from_cli.raw_logs;
|
||||
}
|
||||
|
||||
if config_from_cli.show_self != default_args.show_self {
|
||||
self.show_self = config_from_cli.show_self;
|
||||
}
|
||||
|
||||
if config_from_cli.show_std_err != default_args.no_std_err {
|
||||
self.show_std_err = config_from_cli.show_std_err;
|
||||
}
|
||||
|
||||
if config_from_cli.show_timestamp != default_args.timestamp {
|
||||
self.show_timestamp = config_from_cli.show_timestamp;
|
||||
}
|
||||
|
||||
if config_from_cli.use_cli != default_args.use_cli {
|
||||
self.use_cli = config_from_cli.use_cli;
|
||||
}
|
||||
|
||||
if let Some(host) = config_from_cli.host {
|
||||
self.host = Some(host);
|
||||
}
|
||||
|
||||
if let Some(x) = config_from_cli.dir_save {
|
||||
self.dir_save = Some(x);
|
||||
}
|
||||
|
||||
if let Some(tz) = config_from_cli.timezone {
|
||||
self.timezone = Some(tz);
|
||||
}
|
||||
|
||||
if self.color_logs && self.raw_logs {
|
||||
self.raw_logs = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate a new config file
|
||||
/// First check cli args,
|
||||
/// then if a config file location is given check then
|
||||
/// Else check the default location
|
||||
/// else just return the default config + the cli args
|
||||
/// cli args will take precedence over config settings
|
||||
pub fn new() -> Self {
|
||||
let in_container = Self::check_if_in_container();
|
||||
|
||||
let args = Args::parse();
|
||||
let config_from_cli = Self::from(&args);
|
||||
|
||||
if let Some(dir_config_file) = &args.config_file
|
||||
&& let Some(config_file) =
|
||||
parse_config_file::ConfigFile::try_parse_from_file(dir_config_file)
|
||||
{
|
||||
return Self::from((config_file, Some(PathBuf::from(dir_config_file))))
|
||||
.merge_args(config_from_cli);
|
||||
}
|
||||
|
||||
if let Some((config_file, dir)) = parse_config_file::ConfigFile::try_parse(in_container) {
|
||||
return Self::from((config_file, Some(dir))).merge_args(config_from_cli);
|
||||
}
|
||||
config_from_cli
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use jiff::tz::TimeZone;
|
||||
|
||||
/// Test the basic timestamp_format parsing/checker function
|
||||
#[test]
|
||||
fn test_config_parse_timestamp_format() {
|
||||
let default = "%Y-%m-%dT%H:%M:%S.%8f";
|
||||
|
||||
let result = super::Config::parse_timestamp_format(None);
|
||||
assert_eq!(result, default);
|
||||
|
||||
let result = super::Config::parse_timestamp_format(Some(String::new()));
|
||||
assert_eq!(result, default);
|
||||
|
||||
let result = super::Config::parse_timestamp_format(Some(" ".to_owned()));
|
||||
assert_eq!(result, default);
|
||||
|
||||
let result = super::Config::parse_timestamp_format(Some(" ".to_owned()));
|
||||
assert_eq!(result, default);
|
||||
|
||||
let result =
|
||||
super::Config::parse_timestamp_format(Some("not a valid formatter".to_owned()));
|
||||
assert_eq!(result, default);
|
||||
|
||||
let result = super::Config::parse_timestamp_format(Some(
|
||||
"%A, %B %d, %Y %I:%M %p %A, %B %d, %Y %I:%M %p".to_owned(),
|
||||
));
|
||||
assert_eq!(result, default);
|
||||
|
||||
let input = "%Y-%m-%d %H:%M:%S";
|
||||
let result = super::Config::parse_timestamp_format(Some(input.to_owned()));
|
||||
assert_eq!(result, input);
|
||||
|
||||
let input = "%Y-%j";
|
||||
let result = super::Config::parse_timestamp_format(Some(input.to_owned()));
|
||||
assert_eq!(result, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test various timezones get parsed correctly
|
||||
fn test_config_parse_timezone() {
|
||||
// Timezone with no offset just return None
|
||||
for i in [None, Some("UTC".to_owned())] {
|
||||
assert!(super::Config::parse_timezone(i).is_none());
|
||||
}
|
||||
|
||||
let expected = Some(TimeZone::get("Asia/Tokyo").unwrap());
|
||||
// string case ignored
|
||||
for i in ["ASIA/TOKYO", "asia/tokyo", "aSiA/tOkYo"] {
|
||||
let result = super::Config::parse_timezone(Some(i.to_owned()));
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
use clap::Parser;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Parser, Debug, Clone, Deserialize)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[command(version, about)]
|
||||
pub struct Args {
|
||||
/// Docker update interval in ms, minimum effectively 1000
|
||||
#[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
|
||||
pub docker_interval: u32,
|
||||
|
||||
/// Remove timestamps from Docker logs
|
||||
#[clap(short = 't')]
|
||||
pub timestamp: bool,
|
||||
|
||||
/// Attempt to colorize the logs, conflicts with "-r"
|
||||
#[clap(short = 'c', conflicts_with = "raw")]
|
||||
pub color: bool,
|
||||
|
||||
/// Show raw logs, default is to remove ansi formatting, conflicts with "-c"
|
||||
#[clap(short = 'r', conflicts_with = "color")]
|
||||
pub raw: bool,
|
||||
|
||||
/// Show self when running as a docker container
|
||||
#[clap(short = 's')]
|
||||
pub show_self: bool,
|
||||
|
||||
/// Don't draw gui - for debugging - mostly pointless
|
||||
#[clap(short = 'g')]
|
||||
pub gui: bool,
|
||||
|
||||
/// Docker host, defaults to `/var/run/docker.sock`
|
||||
#[clap(long, short = None)]
|
||||
pub host: Option<String>,
|
||||
|
||||
/// Do not include stderr output in logs
|
||||
#[clap(long = "no-stderr")]
|
||||
pub no_std_err: bool,
|
||||
|
||||
/// Display the container logs timestamp with a given timezone, default is UTC
|
||||
#[clap(long="timezone", short = None)]
|
||||
pub timezone: Option<String>,
|
||||
|
||||
/// Directory for saving exported logs, defaults to `$HOME`
|
||||
#[clap(long="save-dir", short = None)]
|
||||
pub save_dir: Option<String>,
|
||||
|
||||
/// Path to a config file, readable as TOML, JSONC, or JSON
|
||||
#[clap(long="config-file", short = None)]
|
||||
pub config_file: Option<String>,
|
||||
|
||||
/// Force use of docker cli when execing into containers
|
||||
#[clap(long="use-cli", short = None)]
|
||||
pub use_cli: bool,
|
||||
}
|
||||
|
||||
impl Default for Args {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
docker_interval: 1000,
|
||||
timestamp: true,
|
||||
color: false,
|
||||
raw: false,
|
||||
show_self: false,
|
||||
gui: true,
|
||||
host: None,
|
||||
no_std_err: true,
|
||||
timezone: None,
|
||||
save_dir: None,
|
||||
config_file: None,
|
||||
use_cli: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::app_error::AppError;
|
||||
|
||||
use super::{color_parser::ConfigColors, keymap_parser::ConfigKeymap};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ConfigFileFormat {
|
||||
Toml,
|
||||
Jsonc,
|
||||
Json,
|
||||
JsoncAsJson,
|
||||
}
|
||||
|
||||
impl TryFrom<&PathBuf> for ConfigFileFormat {
|
||||
type Error = AppError;
|
||||
|
||||
/// Only allow toml, json, or jsonc files
|
||||
fn try_from(value: &PathBuf) -> Result<Self, AppError> {
|
||||
let err = || AppError::IO(format!("Can't parse give config file: {}", value.display()));
|
||||
let Some(ext) = value.extension() else {
|
||||
return Err(err());
|
||||
};
|
||||
let Some(ext) = ext.to_str() else {
|
||||
return Err(err());
|
||||
};
|
||||
match ext {
|
||||
"toml" => Ok(Self::Toml),
|
||||
"json" => Ok(Self::Json),
|
||||
"jsonc" => Ok(Self::Jsonc),
|
||||
_ => Err(err()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigFileFormat {
|
||||
/// Get the local config directory, to be used by default config parser
|
||||
fn get_config_dir(in_container: bool) -> Option<PathBuf> {
|
||||
if in_container {
|
||||
Some(PathBuf::from("/"))
|
||||
} else {
|
||||
directories::BaseDirs::new()
|
||||
.map(|base_dirs| base_dirs.config_local_dir().join(env!("CARGO_PKG_NAME")))
|
||||
}
|
||||
}
|
||||
/// Return the default filename + path for a given fileformat
|
||||
fn get_default_path_name(self, in_container: bool) -> PathBuf {
|
||||
let suffix = match self {
|
||||
Self::Json | Self::JsoncAsJson => "config.json",
|
||||
Self::Jsonc => "config.jsonc",
|
||||
Self::Toml => "config.toml",
|
||||
};
|
||||
Self::get_config_dir(in_container).map_or_else(|| PathBuf::from(suffix), |i| i.join(suffix))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct ConfigFile {
|
||||
pub color_logs: Option<bool>,
|
||||
pub colors: Option<ConfigColors>,
|
||||
pub docker_interval: Option<u32>,
|
||||
pub gui: Option<bool>,
|
||||
pub host: Option<String>,
|
||||
pub keymap: Option<ConfigKeymap>,
|
||||
pub log_search_case_sensitive: Option<bool>,
|
||||
pub raw_logs: Option<bool>,
|
||||
pub save_dir: Option<String>,
|
||||
pub show_logs: Option<bool>,
|
||||
pub show_self: Option<bool>,
|
||||
pub show_std_err: Option<bool>,
|
||||
pub show_timestamp: Option<bool>,
|
||||
pub timestamp_format: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
pub use_cli: Option<bool>,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
/// Attempt to create a config.toml file, will attempt to recursively create the directories as well
|
||||
fn create_config_file(in_container: bool) -> Result<(), AppError> {
|
||||
if in_container {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config_dir = ConfigFileFormat::get_config_dir(in_container)
|
||||
.ok_or_else(|| AppError::IO("config_dir".to_owned()))?;
|
||||
let file_name = config_dir.join("config.toml");
|
||||
|
||||
if !std::fs::exists(&file_name).map_err(|i| AppError::IO(i.to_string()))? {
|
||||
if !std::fs::exists(&config_dir).map_err(|i| AppError::IO(i.to_string()))? {
|
||||
std::fs::DirBuilder::new()
|
||||
.recursive(true)
|
||||
.create(&config_dir)
|
||||
.map_err(|i| AppError::IO(i.to_string()))?;
|
||||
}
|
||||
let mut file =
|
||||
std::fs::File::create_new(&file_name).map_err(|i| AppError::IO(i.to_string()))?;
|
||||
file.write_all(include_bytes!("./config.toml"))
|
||||
.map_err(|i| AppError::IO(i.to_string()))?;
|
||||
file.flush().map_err(|i| AppError::IO(i.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// parse a given &str (read from the configfile) into Self
|
||||
fn parse(file_format: ConfigFileFormat, input: &str) -> Result<Self, AppError> {
|
||||
match file_format {
|
||||
ConfigFileFormat::Json => {
|
||||
serde_json::from_str::<Self>(input).map_err(|i| AppError::Parse(i.to_string()))
|
||||
}
|
||||
ConfigFileFormat::Jsonc | ConfigFileFormat::JsoncAsJson => {
|
||||
serde_jsonc::from_str::<Self>(input).map_err(|i| AppError::Parse(i.to_string()))
|
||||
}
|
||||
ConfigFileFormat::Toml => {
|
||||
toml::from_str::<Self>(input).map_err(|i| AppError::Parse(i.message().to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the config file path to string, then attempt to parse
|
||||
fn parse_config_file(file_format: ConfigFileFormat, path: &PathBuf) -> Result<Self, AppError> {
|
||||
let mut file = std::fs::File::open(path).map_err(|_| {
|
||||
AppError::IO(
|
||||
path.to_str()
|
||||
.map_or_else(String::new, std::borrow::ToOwned::to_owned),
|
||||
)
|
||||
})?;
|
||||
let mut input = String::new();
|
||||
file.read_to_string(&mut input)
|
||||
.map_err(|i| AppError::IO(i.to_string()))?;
|
||||
Self::parse(file_format, &input)
|
||||
}
|
||||
|
||||
/// Try to parse the config file when the path is user supplied via cliargs
|
||||
pub fn try_parse_from_file(path: &str) -> Option<Self> {
|
||||
let path = PathBuf::from(path);
|
||||
let Ok(file_format) = ConfigFileFormat::try_from(&path) else {
|
||||
return None;
|
||||
};
|
||||
Self::parse_config_file(file_format, &path).ok()
|
||||
}
|
||||
|
||||
/// Parse a config file using default config_file location
|
||||
/// This is executed first, then the CLI args are read, and if they contain a "--config-file" entry, then Self::try_parse_from_file() is executed
|
||||
pub fn try_parse(in_container: bool) -> Option<(Self, PathBuf)> {
|
||||
let mut output = None;
|
||||
for file_format in [
|
||||
ConfigFileFormat::Toml,
|
||||
ConfigFileFormat::Jsonc,
|
||||
ConfigFileFormat::JsoncAsJson,
|
||||
ConfigFileFormat::Json,
|
||||
] {
|
||||
let path = file_format.get_default_path_name(in_container);
|
||||
if let Ok(config_file) = Self::parse_config_file(file_format, &path) {
|
||||
output = Some((config_file, path));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if output.is_none() {
|
||||
Self::create_config_file(in_container).ok();
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
|
||||
use crate::config::{AppColors, Keymap};
|
||||
|
||||
use super::ConfigFile;
|
||||
|
||||
#[test]
|
||||
/// ./config.toml parses fine - as this is used to write a file on disk, it's vital that this is always valid
|
||||
fn test_parse_config_toml_valid() {
|
||||
let example_toml = include_str!("./config.toml");
|
||||
let result = ConfigFile::parse(super::ConfigFileFormat::Toml, example_toml);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// make sure config.toml matches the default keymap
|
||||
fn test_parse_config_keymap_toml() {
|
||||
let example_toml = include_str!("./config.toml");
|
||||
let result = ConfigFile::parse(super::ConfigFileFormat::Toml, example_toml).unwrap();
|
||||
assert!(result.keymap.is_some());
|
||||
assert_eq!(Keymap::from(result.keymap), Keymap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// make sure example.config.jsonc matches the default keymap
|
||||
fn test_parse_config_keymap_jsonc() {
|
||||
let example_jsonc = include_str!("../../example_config/example.config.jsonc");
|
||||
let result = ConfigFile::parse(super::ConfigFileFormat::Jsonc, example_jsonc).unwrap();
|
||||
assert!(result.keymap.is_some());
|
||||
assert_eq!(Keymap::from(result.keymap), Keymap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// All configs parsed and are equal
|
||||
fn test_parse_config_keymap_all() {
|
||||
let example_jsonc = include_str!("../../example_config/example.config.jsonc");
|
||||
let result_jsonc =
|
||||
ConfigFile::parse(super::ConfigFileFormat::Jsonc, example_jsonc).unwrap();
|
||||
assert!(result_jsonc.keymap.is_some());
|
||||
let result_jsonc = result_jsonc.keymap.unwrap();
|
||||
|
||||
let example_toml = include_str!("./config.toml");
|
||||
let result_toml = ConfigFile::parse(super::ConfigFileFormat::Toml, example_toml).unwrap();
|
||||
assert!(result_toml.keymap.is_some());
|
||||
let result_toml = result_toml.keymap.unwrap();
|
||||
|
||||
assert_eq!(Keymap::from(Some(result_toml.clone())), Keymap::new());
|
||||
assert_eq!(result_toml, result_jsonc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// make sure config.toml matches the default app colors
|
||||
fn test_parse_config_colors_toml() {
|
||||
let example_toml = include_str!("./config.toml");
|
||||
let result = ConfigFile::parse(super::ConfigFileFormat::Toml, example_toml).unwrap();
|
||||
assert!(result.colors.is_some());
|
||||
assert_eq!(AppColors::from(result.colors), AppColors::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// make sure config.toml matches the default app colors
|
||||
fn test_parse_config_colors_jsonc() {
|
||||
let example_jsonc = include_str!("../../example_config/example.config.jsonc");
|
||||
let result = ConfigFile::parse(super::ConfigFileFormat::Jsonc, example_jsonc).unwrap();
|
||||
assert!(result.colors.is_some());
|
||||
assert_eq!(AppColors::from(result.colors), AppColors::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// All configs parsed and are equal
|
||||
fn test_parse_config_colors_all() {
|
||||
let example_jsonc = include_str!("../../example_config/example.config.jsonc");
|
||||
let result_jsonc =
|
||||
ConfigFile::parse(super::ConfigFileFormat::Jsonc, example_jsonc).unwrap();
|
||||
assert!(result_jsonc.colors.is_some());
|
||||
let result_jsonc = result_jsonc.colors.unwrap();
|
||||
|
||||
let example_toml = include_str!("./config.toml");
|
||||
let result_toml = ConfigFile::parse(super::ConfigFileFormat::Toml, example_toml).unwrap();
|
||||
assert!(result_toml.colors.is_some());
|
||||
let result_toml = result_toml.colors.unwrap();
|
||||
|
||||
assert_eq!(AppColors::from(Some(result_toml.clone())), AppColors::new());
|
||||
assert_eq!(result_toml, result_jsonc);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::app_data::{ContainerId, DockerCommand};
|
||||
use bollard::Docker;
|
||||
use tokio::sync::oneshot::Sender;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DockerMessage {
|
||||
ConfirmDelete(ContainerId),
|
||||
Control((DockerCommand, ContainerId)),
|
||||
Exec(Sender<Arc<Docker>>),
|
||||
Inspect(ContainerId),
|
||||
Update,
|
||||
}
|
||||
@@ -1,665 +0,0 @@
|
||||
use bollard::{
|
||||
Docker,
|
||||
query_parameters::{
|
||||
InspectContainerOptions, ListContainersOptions, LogsOptions, RemoveContainerOptions,
|
||||
RestartContainerOptions, StartContainerOptions, StatsOptions, StopContainerOptions,
|
||||
},
|
||||
secret::ContainerStatsResponse,
|
||||
service::ContainerSummary,
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
ENTRY_POINT,
|
||||
app_data::{AppData, ContainerId, DockerCommand, State},
|
||||
app_error::AppError,
|
||||
config::Config,
|
||||
ui::{GuiState, Status},
|
||||
};
|
||||
mod message;
|
||||
pub use message::DockerMessage;
|
||||
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
enum SpawnId {
|
||||
Stats((ContainerId, Binate)),
|
||||
Log(ContainerId),
|
||||
}
|
||||
|
||||
impl SpawnId {
|
||||
/// Extract the &ContainerId out of self
|
||||
const fn get_id(&self) -> &ContainerId {
|
||||
match self {
|
||||
Self::Log(id) | Self::Stats((id, _)) => id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cpu & Mem stats take twice as long as the update interval to get a value, so will have two being executed at the same time
|
||||
/// SpawnId::Stats takes container_id and binate value to enable both cycles of the same container_id to be inserted into the hashmap
|
||||
/// Binate value is toggled when all handles have been spawned off
|
||||
/// Also effectively means that the minimum docker_update interval will be 1000ms
|
||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
enum Binate {
|
||||
One,
|
||||
Two,
|
||||
}
|
||||
|
||||
impl Binate {
|
||||
const fn toggle(self) -> Self {
|
||||
match self {
|
||||
Self::One => Self::Two,
|
||||
Self::Two => Self::One,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DockerData {
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
binate: Binate,
|
||||
config: Config,
|
||||
docker: Arc<Docker>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
receiver: Receiver<DockerMessage>,
|
||||
spawns: Arc<Mutex<HashSet<SpawnId>>>,
|
||||
}
|
||||
|
||||
impl DockerData {
|
||||
/// Use docker stats to calculate current cpu usage
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn calculate_usage(stats: &ContainerStatsResponse) -> f64 {
|
||||
let mut cpu_percentage = 0.0;
|
||||
|
||||
let total_usage = stats.precpu_stats.as_ref().map_or(0, |i| {
|
||||
i.cpu_usage
|
||||
.as_ref()
|
||||
.map_or(0, |i| i.total_usage.unwrap_or_default())
|
||||
});
|
||||
|
||||
let cpu_delta = stats.cpu_stats.as_ref().map_or(0, |i| {
|
||||
i.cpu_usage.as_ref().map_or(0, |i| {
|
||||
i.total_usage
|
||||
.unwrap_or_default()
|
||||
.saturating_sub(total_usage)
|
||||
})
|
||||
}) as f64;
|
||||
|
||||
if let (Some(Some(cpu_stats_usage)), Some(Some(precpu_stats_usage))) = (
|
||||
stats.cpu_stats.as_ref().map(|i| i.system_cpu_usage),
|
||||
stats.precpu_stats.as_ref().map(|i| i.system_cpu_usage),
|
||||
) {
|
||||
let system_delta = cpu_stats_usage.saturating_sub(precpu_stats_usage) as f64;
|
||||
let online_cpus = f64::from(stats.cpu_stats.as_ref().map_or(0, |i| {
|
||||
i.online_cpus.unwrap_or_else(|| {
|
||||
u32::try_from(
|
||||
stats
|
||||
.cpu_stats
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.cpu_usage
|
||||
.unwrap_or_default()
|
||||
.percpu_usage
|
||||
.as_ref()
|
||||
.map_or(0, std::vec::Vec::len),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}));
|
||||
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 own thread
|
||||
/// remove if from spawns hashmap when complete
|
||||
/// 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 own thread
|
||||
/// remove if from spawns hashmap when complete
|
||||
async fn update_container_stat(
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
docker: Arc<Docker>,
|
||||
state: State,
|
||||
spawn_id: SpawnId,
|
||||
spawns: Arc<Mutex<HashSet<SpawnId>>>,
|
||||
) {
|
||||
let id = spawn_id.get_id();
|
||||
let mut stream = docker
|
||||
.stats(
|
||||
id.get(),
|
||||
Some(StatsOptions {
|
||||
stream: false,
|
||||
one_shot: false,
|
||||
}),
|
||||
)
|
||||
.take(1);
|
||||
|
||||
while let Some(Ok(stats)) = stream.next().await {
|
||||
// Memory stats are only collected if the container is alive - is this the behaviour we want?
|
||||
|
||||
let (mem_stat, cpu_stats) = if state.is_alive() {
|
||||
let mem_cache = stats.memory_stats.as_ref().map_or(&0, |i| {
|
||||
i.stats
|
||||
.as_ref()
|
||||
.map_or(&0, |i| i.get("inactive_file").unwrap_or(&0))
|
||||
});
|
||||
(
|
||||
Some(
|
||||
stats
|
||||
.memory_stats
|
||||
.as_ref()
|
||||
.map_or(0, |i| i.usage.unwrap_or_default())
|
||||
.saturating_sub(*mem_cache),
|
||||
),
|
||||
Some(Self::calculate_usage(&stats)),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// TODO is hardcoded eth0 a good idea here?
|
||||
let (rx, tx) = stats.networks.as_ref().map_or((0, 0), |i| {
|
||||
i.get("eth0").map_or((0, 0), |x| {
|
||||
(
|
||||
x.rx_bytes.unwrap_or_default(),
|
||||
x.tx_bytes.unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
app_data.lock().update_stats_by_id(
|
||||
id,
|
||||
cpu_stats,
|
||||
mem_stat,
|
||||
stats
|
||||
.memory_stats
|
||||
.unwrap_or_default()
|
||||
.limit
|
||||
.unwrap_or_default(),
|
||||
rx,
|
||||
tx,
|
||||
);
|
||||
}
|
||||
spawns.lock().remove(&spawn_id);
|
||||
}
|
||||
|
||||
/// Update all stats, spawn each container into own tokio::spawn thread
|
||||
fn update_all_container_stats(&mut self) {
|
||||
let all_ids = self.app_data.lock().get_all_id_state();
|
||||
for (state, id) in all_ids {
|
||||
let spawn_id = SpawnId::Stats((id, self.binate));
|
||||
|
||||
if !self.spawns.lock().contains(&spawn_id) {
|
||||
let app_data = Arc::clone(&self.app_data);
|
||||
let docker = Arc::clone(&self.docker);
|
||||
let spawns = Arc::clone(&self.spawns);
|
||||
tokio::spawn(Self::update_container_stat(
|
||||
app_data, docker, state, spawn_id, spawns,
|
||||
));
|
||||
}
|
||||
}
|
||||
self.binate = self.binate.toggle();
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set
|
||||
async fn update_all_containers(&self) {
|
||||
let containers = self
|
||||
.docker
|
||||
.list_containers(Some(ListContainersOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let output = containers
|
||||
.into_iter()
|
||||
.filter_map(|f| match f.id {
|
||||
Some(_) => {
|
||||
if self.config.in_container
|
||||
&& f.command
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.starts_with(ENTRY_POINT))
|
||||
&& self.config.show_self
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(f)
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
.collect::<Vec<ContainerSummary>>();
|
||||
self.app_data.lock().update_containers(output);
|
||||
}
|
||||
|
||||
/// Update single container logs
|
||||
/// remove it from spawns hashmap when complete
|
||||
async fn update_log(
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
docker: Arc<Docker>,
|
||||
id: ContainerId,
|
||||
since: u64,
|
||||
spawns: Arc<Mutex<HashSet<SpawnId>>>,
|
||||
stderr: bool,
|
||||
) {
|
||||
let options = Some(LogsOptions {
|
||||
stdout: true,
|
||||
stderr,
|
||||
timestamps: true,
|
||||
since: i32::try_from(since).unwrap_or_default(),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut logs = docker.logs(id.get(), options);
|
||||
let mut output = vec![];
|
||||
|
||||
while let Some(Ok(value)) = logs.next().await {
|
||||
let data = value.to_string();
|
||||
if !data.trim().is_empty() {
|
||||
output.push(data);
|
||||
}
|
||||
}
|
||||
app_data.lock().update_log_by_id(output, &id);
|
||||
spawns.lock().remove(&SpawnId::Log(id));
|
||||
}
|
||||
|
||||
/// Update all logs, spawn each container into own tokio::spawn thread
|
||||
fn init_all_logs(&self, all_ids: Vec<(State, ContainerId)>) -> Arc<AtomicUsize> {
|
||||
let init = Arc::new(AtomicUsize::new(0));
|
||||
for (_, id) in all_ids {
|
||||
let app_data: Arc<parking_lot::lock_api::Mutex<parking_lot::RawMutex, AppData>> =
|
||||
Arc::clone(&self.app_data);
|
||||
let docker = Arc::clone(&self.docker);
|
||||
let spawns = Arc::clone(&self.spawns);
|
||||
let std_err = self.config.show_std_err;
|
||||
let init = Arc::clone(&init);
|
||||
|
||||
self.spawns.lock().insert(SpawnId::Log(id.clone()));
|
||||
|
||||
tokio::spawn(async move {
|
||||
Self::update_log(app_data, docker, id, 0, spawns, std_err).await;
|
||||
init.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
init
|
||||
}
|
||||
|
||||
/// Initialize docker container data, before any messages are received
|
||||
async fn initialise_container_data(&mut self) {
|
||||
self.gui_state.lock().status_push(Status::Init);
|
||||
let loading_uuid = Uuid::new_v4();
|
||||
GuiState::start_loading_animation(&self.gui_state, loading_uuid);
|
||||
self.update_all_containers().await;
|
||||
let all_ids = self.app_data.lock().get_all_id_state();
|
||||
let all_ids_len = all_ids.len();
|
||||
let init = self.init_all_logs(all_ids);
|
||||
self.update_all_container_stats();
|
||||
|
||||
while init.load(std::sync::atomic::Ordering::SeqCst) != all_ids_len {
|
||||
self.app_data.lock().sort_containers();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
self.gui_state.lock().stop_loading_animation(loading_uuid);
|
||||
self.gui_state.lock().status_del(Status::Init);
|
||||
}
|
||||
|
||||
/// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed)
|
||||
async fn update_everything(&mut self) {
|
||||
self.update_all_containers().await;
|
||||
if let Some(container) = self.app_data.lock().get_selected_container() {
|
||||
let last_updated = container.last_updated;
|
||||
let spawn_id = SpawnId::Log(container.id.clone());
|
||||
// Only spawn if not already spawned with a given id/binate pair
|
||||
if !self.spawns.lock().contains(&spawn_id) {
|
||||
self.spawns.lock().insert(spawn_id.clone());
|
||||
tokio::spawn(Self::update_log(
|
||||
Arc::clone(&self.app_data),
|
||||
Arc::clone(&self.docker),
|
||||
container.id.clone(),
|
||||
last_updated,
|
||||
Arc::clone(&self.spawns),
|
||||
self.config.show_std_err,
|
||||
));
|
||||
}
|
||||
}
|
||||
self.update_all_container_stats();
|
||||
self.app_data.lock().sort_containers();
|
||||
}
|
||||
|
||||
/// Set the global error as the docker error, and set gui_state to error
|
||||
fn set_error(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
error: DockerCommand,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
app_data
|
||||
.lock()
|
||||
.set_error(AppError::DockerCommand(error), gui_state, Status::Error);
|
||||
}
|
||||
|
||||
/// Execute docker commands (start, stop etc) on it's own tokio thread
|
||||
async fn execute_command(&mut self, control: DockerCommand, id: ContainerId) {
|
||||
let (app_data, docker, gui_state) = (
|
||||
Arc::clone(&self.app_data),
|
||||
Arc::clone(&self.docker),
|
||||
Arc::clone(&self.gui_state),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
let uuid = Uuid::new_v4();
|
||||
GuiState::start_loading_animation(&gui_state, uuid);
|
||||
if match control {
|
||||
DockerCommand::Delete => {
|
||||
gui_state.lock().set_delete_container(None);
|
||||
docker
|
||||
.remove_container(
|
||||
id.get(),
|
||||
Some(RemoveContainerOptions {
|
||||
v: false,
|
||||
force: true,
|
||||
link: false,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
DockerCommand::Pause => docker.pause_container(id.get()).await,
|
||||
DockerCommand::Restart => {
|
||||
docker
|
||||
.restart_container(id.get(), None::<RestartContainerOptions>)
|
||||
.await
|
||||
}
|
||||
DockerCommand::Resume => docker.unpause_container(id.get()).await,
|
||||
DockerCommand::Start => {
|
||||
docker
|
||||
.start_container(id.get(), None::<StartContainerOptions>)
|
||||
.await
|
||||
}
|
||||
DockerCommand::Stop => {
|
||||
docker
|
||||
.stop_container(id.get(), None::<StopContainerOptions>)
|
||||
.await
|
||||
}
|
||||
}
|
||||
.is_err()
|
||||
{
|
||||
Self::set_error(&app_data, control, &gui_state);
|
||||
}
|
||||
gui_state.lock().stop_loading_animation(uuid);
|
||||
});
|
||||
|
||||
self.update_everything().await;
|
||||
}
|
||||
|
||||
/// Handle incoming messages, container controls & all container information update
|
||||
/// Spawn Docker commands off into own thread
|
||||
async fn message_handler(&mut self) {
|
||||
while let Some(message) = self.receiver.recv().await {
|
||||
match message {
|
||||
DockerMessage::ConfirmDelete(id) => {
|
||||
self.gui_state.lock().set_delete_container(Some(id));
|
||||
}
|
||||
DockerMessage::Control((command, id)) => self.execute_command(command, id).await,
|
||||
DockerMessage::Exec(docker_tx) => {
|
||||
docker_tx.send(Arc::clone(&self.docker)).ok();
|
||||
}
|
||||
DockerMessage::Update => self.update_everything().await,
|
||||
DockerMessage::Inspect(id) => {
|
||||
let t = self
|
||||
.docker
|
||||
.inspect_container(id.get(), Some(InspectContainerOptions { size: true }))
|
||||
.await;
|
||||
if let Ok(t) = t {
|
||||
self.app_data.lock().set_inspect_data(t);
|
||||
self.gui_state.lock().status_push(Status::Inspect);
|
||||
} else {
|
||||
// Set error here, can't inspect container
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send an update message every x ms, where x is the args.docker_interval
|
||||
fn heartbeat(config: &Config, docker_tx: Sender<DockerMessage>) {
|
||||
let update_duration =
|
||||
std::time::Duration::from_millis(u64::from(config.docker_interval_ms));
|
||||
let mut now = std::time::Instant::now();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
docker_tx.send(DockerMessage::Update).await.ok();
|
||||
if let Some(to_sleep) = update_duration.checked_sub(now.elapsed()) {
|
||||
tokio::time::sleep(to_sleep).await;
|
||||
}
|
||||
now = std::time::Instant::now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Initialise self, and start the message receiving loop
|
||||
pub async fn start(
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
docker: Docker,
|
||||
docker_rx: Receiver<DockerMessage>,
|
||||
docker_tx: Sender<DockerMessage>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let args = app_data.lock().config.clone();
|
||||
if app_data.lock().get_error().is_none() {
|
||||
let mut inner = Self {
|
||||
app_data,
|
||||
config: args,
|
||||
binate: Binate::One,
|
||||
docker: Arc::new(docker),
|
||||
gui_state,
|
||||
receiver: docker_rx,
|
||||
spawns: Arc::new(Mutex::new(HashSet::new())),
|
||||
};
|
||||
inner.initialise_container_data().await;
|
||||
Self::heartbeat(&inner.config, docker_tx);
|
||||
inner.message_handler().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
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::float_cmp)]
|
||||
mod tests {
|
||||
|
||||
use bollard::secret::{ContainerCpuStats, ContainerCpuUsage};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn gen_stats() -> ContainerStatsResponse {
|
||||
ContainerStatsResponse {
|
||||
read: None,
|
||||
os_type: None,
|
||||
preread: None,
|
||||
num_procs: Some(1),
|
||||
pids_stats: None,
|
||||
networks: None,
|
||||
memory_stats: None,
|
||||
blkio_stats: None,
|
||||
cpu_stats: Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![50]),
|
||||
usage_in_usermode: Some(10),
|
||||
total_usage: Some(100),
|
||||
usage_in_kernelmode: Some(20),
|
||||
}),
|
||||
system_cpu_usage: Some(400),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
}),
|
||||
precpu_stats: Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![50]),
|
||||
usage_in_usermode: Some(10),
|
||||
total_usage: Some(100),
|
||||
usage_in_kernelmode: Some(20),
|
||||
}),
|
||||
system_cpu_usage: Some(400),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
}),
|
||||
storage_stats: None,
|
||||
name: None,
|
||||
id: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_usage_50() {
|
||||
let mut stats = gen_stats();
|
||||
stats.precpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![50]),
|
||||
usage_in_usermode: Some(10),
|
||||
total_usage: Some(100),
|
||||
usage_in_kernelmode: Some(20),
|
||||
}),
|
||||
system_cpu_usage: Some(400),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
stats.cpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![150]),
|
||||
usage_in_usermode: Some(20),
|
||||
total_usage: Some(150),
|
||||
usage_in_kernelmode: Some(30),
|
||||
}),
|
||||
system_cpu_usage: Some(500),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||
assert_eq!(50.0, cpu_percentage);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_usage_25() {
|
||||
let mut stats = gen_stats();
|
||||
stats.precpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![50]),
|
||||
usage_in_usermode: Some(10),
|
||||
total_usage: Some(100),
|
||||
usage_in_kernelmode: Some(20),
|
||||
}),
|
||||
system_cpu_usage: Some(400),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
stats.cpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![75]),
|
||||
usage_in_usermode: Some(20),
|
||||
total_usage: Some(125),
|
||||
usage_in_kernelmode: Some(30),
|
||||
}),
|
||||
system_cpu_usage: Some(500),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||
assert_eq!(25.0, cpu_percentage);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_usage_75() {
|
||||
let mut stats = gen_stats();
|
||||
stats.precpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![50]),
|
||||
usage_in_usermode: Some(10),
|
||||
total_usage: Some(100),
|
||||
usage_in_kernelmode: Some(20),
|
||||
}),
|
||||
system_cpu_usage: Some(400),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
stats.cpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![175]),
|
||||
usage_in_usermode: Some(20),
|
||||
total_usage: Some(175),
|
||||
usage_in_kernelmode: Some(30),
|
||||
}),
|
||||
system_cpu_usage: Some(500),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||
assert_eq!(75.0, cpu_percentage);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_usage_100() {
|
||||
let mut stats = gen_stats();
|
||||
stats.precpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![50]),
|
||||
usage_in_usermode: Some(10),
|
||||
total_usage: Some(100),
|
||||
usage_in_kernelmode: Some(20),
|
||||
}),
|
||||
system_cpu_usage: Some(400),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
stats.cpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![200]),
|
||||
usage_in_usermode: Some(20),
|
||||
total_usage: Some(200),
|
||||
usage_in_kernelmode: Some(30),
|
||||
}),
|
||||
system_cpu_usage: Some(500),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||
assert_eq!(100.0, cpu_percentage);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_usage_175() {
|
||||
let mut stats = gen_stats();
|
||||
stats.precpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![50]),
|
||||
usage_in_usermode: Some(10),
|
||||
total_usage: Some(100),
|
||||
usage_in_kernelmode: Some(20),
|
||||
}),
|
||||
system_cpu_usage: Some(400),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
stats.cpu_stats = Some(ContainerCpuStats {
|
||||
cpu_usage: Some(ContainerCpuUsage {
|
||||
percpu_usage: Some(vec![275]),
|
||||
usage_in_usermode: Some(20),
|
||||
total_usage: Some(275),
|
||||
usage_in_kernelmode: Some(30),
|
||||
}),
|
||||
system_cpu_usage: Some(500),
|
||||
online_cpus: Some(1),
|
||||
throttling_data: None,
|
||||
});
|
||||
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||
assert_eq!(175.0, cpu_percentage);
|
||||
}
|
||||
}
|
||||
-360
@@ -1,360 +0,0 @@
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
sync::{Arc, atomic::AtomicBool, mpsc::Sender},
|
||||
};
|
||||
|
||||
use bollard::{
|
||||
Docker,
|
||||
exec::{CreateExecOptions, ResizeExecOptions, StartExecOptions, StartExecResults},
|
||||
};
|
||||
use crossterm::terminal::enable_raw_mode;
|
||||
use futures_util::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::layout::Size;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
app_data::{AppData, ContainerId, RunningState, State},
|
||||
app_error::AppError,
|
||||
};
|
||||
|
||||
/// TTY location
|
||||
const TTY: &str = "/dev/tty";
|
||||
|
||||
/// This will be the start of a docker exec message if one is unable to actually exec into the container
|
||||
const OCI_ERROR: &str = "OCI runtime exec failed";
|
||||
|
||||
/// Set the cursor position on the screen to (0,0)
|
||||
const CURSOR_POS: &str = "\x1B[J\x1B[H";
|
||||
|
||||
/// This needs to be written to stdout when exiting the exec mode, else the input handler thread gets confused,
|
||||
/// see https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
|
||||
const KEYBOARD_PROTO: &str = "\x1B[?u\x1B[c";
|
||||
|
||||
mod command {
|
||||
pub const PWD: &str = "pwd";
|
||||
pub const DOCKER: &str = "docker";
|
||||
pub const EXEC: &str = "exec";
|
||||
pub const SH: &str = "sh";
|
||||
pub const IT: &str = "-it";
|
||||
}
|
||||
|
||||
/// Currently known byte output after writing KEYBOARD_PROTO to stdout
|
||||
/// valid arm: [91, 63, 54, 49, 59, 54, 59, 55, 59, 50, 50, 59, 50, 51, 59, 50, 52, 59, 50, 56, 59, 51, 50, 59,52, 50] => [?61;6;7;22;23;24;28;32;2
|
||||
/// valid x86: [91, 63, 49, 59, 50, 99] => [?1;2c
|
||||
/// invalid x86: [91, 63, 49, 59, 48, 99] => [?1;0c
|
||||
enum ByteOutput {
|
||||
Arm,
|
||||
X86,
|
||||
}
|
||||
|
||||
impl ByteOutput {
|
||||
const fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::Arm => 26,
|
||||
Self::X86 => 6,
|
||||
}
|
||||
}
|
||||
const fn last(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Arm => &[50],
|
||||
Self::X86 => &[99],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the output from tty to see if it matches known sequence.
|
||||
/// At the moment we only need to check the length and end digit, as x86 valid and invalid match in these two regards
|
||||
fn byte_sequence_valid(bytes: &[u8]) -> bool {
|
||||
[ByteOutput::Arm, ByteOutput::X86]
|
||||
.iter()
|
||||
.any(|i| i.len() == bytes.len() && bytes.ends_with(i.last()))
|
||||
}
|
||||
|
||||
/// Check if tty is able to be written to, aka not windows
|
||||
pub fn tty_readable() -> bool {
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(false)
|
||||
.open(TTY)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
struct AsyncTTY {
|
||||
rx: std::sync::mpsc::Receiver<u8>,
|
||||
}
|
||||
|
||||
impl AsyncTTY {
|
||||
/// Use an async timeout to read data from the file, and send to the "main" thread
|
||||
async fn read_loop(mut f: File, tx: Sender<u8>) {
|
||||
loop {
|
||||
let mut buf = [0];
|
||||
if tokio::time::timeout(std::time::Duration::from_millis(10), f.read_exact(&mut buf))
|
||||
.await
|
||||
.is_ok()
|
||||
&& tx.send(buf[0]).is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Async tty reading, spawned into its own tokio thread
|
||||
fn get(cancel_token: &CancellationToken) -> Option<Self> {
|
||||
if tty_readable() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let cancel_token = cancel_token.to_owned();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(f) = tokio::fs::File::open(TTY).await {
|
||||
tokio::select! {
|
||||
() = cancel_token.cancelled() => (),
|
||||
() = Self::read_loop(f, tx) => cancel_token.cancel(),
|
||||
}
|
||||
}
|
||||
});
|
||||
Some(Self { rx })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl TryFrom<&Terminal<CrosstermBackend<Stdout>>> for HWU16 {
|
||||
// type Error = None;
|
||||
// fn try_from(terminal: &Terminal<CrosstermBackend<Stdout>>) -> Option<Self> {
|
||||
// terminal.size().map_or(None, |i| {
|
||||
// Some(Self {
|
||||
// width: i.width,
|
||||
// height: i.height,
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// impl TerminalSize {
|
||||
// pub fn new(terminal: &Terminal<CrosstermBackend<Stdout>>) -> Option<Self> {
|
||||
// terminal.size().map_or(None, |i| {
|
||||
// Some(Self {
|
||||
// width: i.width,
|
||||
// height: i.height,
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExecMode {
|
||||
// use Bollard Rust library
|
||||
Internal((Arc<ContainerId>, Arc<Docker>)),
|
||||
// use the external `docker-cli`
|
||||
External(Arc<ContainerId>),
|
||||
}
|
||||
|
||||
impl ExecMode {
|
||||
/// Test if we can exec into the selected container, first via the Internal methods, then by the External
|
||||
/// If the container is oxker, it will always return None
|
||||
pub async fn new(app_data: &Arc<Mutex<AppData>>, docker: &Arc<Docker>) -> Option<Self> {
|
||||
let is_oxker = app_data.lock().is_oxker();
|
||||
if is_oxker {
|
||||
return None;
|
||||
}
|
||||
|
||||
let use_cli = app_data.lock().config.use_cli;
|
||||
let container = app_data.lock().get_selected_container_id_state_name();
|
||||
|
||||
if let Some((id, state, _)) = container
|
||||
&& [
|
||||
State::Running(RunningState::Healthy),
|
||||
State::Running(RunningState::Unhealthy),
|
||||
]
|
||||
.contains(&state)
|
||||
{
|
||||
if tty_readable()
|
||||
&& !use_cli
|
||||
&& let Ok(exec) = docker
|
||||
.create_exec(
|
||||
id.get(),
|
||||
CreateExecOptions {
|
||||
attach_stdout: Some(true),
|
||||
attach_stderr: Some(true),
|
||||
cmd: Some(vec![command::PWD]),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
&& let Ok(StartExecResults::Attached { mut output, .. }) =
|
||||
docker.start_exec(&exec.id, None).await
|
||||
&& let Some(Ok(msg)) = output.next().await
|
||||
&& !msg.to_string().starts_with(OCI_ERROR)
|
||||
{
|
||||
return Some(Self::Internal((Arc::new(id), Arc::clone(docker))));
|
||||
}
|
||||
|
||||
if let Ok(output) = std::process::Command::new(command::DOCKER)
|
||||
.args([command::EXEC, id.get(), command::PWD])
|
||||
.output()
|
||||
&& let Ok(output) = String::from_utf8(output.stdout)
|
||||
&& !output.starts_with(OCI_ERROR)
|
||||
{
|
||||
return Some(Self::External(Arc::new(id)));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// exec into the container using the external docker cli, the result it just piped into oxker
|
||||
fn exec_external(id: &ContainerId) {
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.write_all(CURSOR_POS.as_bytes()).ok();
|
||||
if let Ok(mut child) = std::process::Command::new(command::DOCKER)
|
||||
.args([command::EXEC, command::IT, id.get(), command::SH])
|
||||
.stdin(std::process::Stdio::inherit())
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.spawn()
|
||||
{
|
||||
child.wait().ok();
|
||||
if child.kill().is_err() {
|
||||
std::process::exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exec into the container via the Bollard library, stdout & stdin on different threads
|
||||
/// Have to deal with strange output once dropped, hence the use of internal_cleanup() method
|
||||
async fn exec_internal(
|
||||
&self,
|
||||
id: &ContainerId,
|
||||
docker: &Arc<Docker>,
|
||||
terminal_size: Option<Size>,
|
||||
) -> Result<(), AppError> {
|
||||
let cancel_token = CancellationToken::new();
|
||||
|
||||
if let Ok(exec_result) = docker
|
||||
.create_exec(
|
||||
id.get(),
|
||||
CreateExecOptions {
|
||||
attach_stdout: Some(true),
|
||||
attach_stderr: Some(true),
|
||||
attach_stdin: Some(true),
|
||||
tty: Some(true),
|
||||
cmd: Some(vec![command::SH]),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
match docker
|
||||
.start_exec(
|
||||
&exec_result.id,
|
||||
Some(StartExecOptions {
|
||||
detach: false,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(StartExecResults::Attached {
|
||||
mut output,
|
||||
mut input,
|
||||
}) => {
|
||||
if let Some(tty) = AsyncTTY::get(&cancel_token) {
|
||||
tokio::spawn(async move {
|
||||
enable_raw_mode().ok();
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.write_all(CURSOR_POS.as_bytes()).ok();
|
||||
stdout.flush().ok();
|
||||
while let Some(Ok(x)) = output.next().await {
|
||||
stdout.write_all(&x.into_bytes()).ok();
|
||||
stdout.flush().ok();
|
||||
}
|
||||
cancel_token.cancel();
|
||||
});
|
||||
|
||||
if let Some(terminal_size) = terminal_size {
|
||||
docker
|
||||
.resize_exec(
|
||||
&exec_result.id,
|
||||
ResizeExecOptions {
|
||||
height: terminal_size.height,
|
||||
width: terminal_size.width,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
while let Ok(x) = tty.rx.recv() {
|
||||
input.write_all(&[x]).await.ok();
|
||||
}
|
||||
|
||||
self.internal_cleanup()?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::Terminal);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is the fix for key pressed not being handled correctly on quit
|
||||
/// It writes a special message to the stdout, and then listens out for a valid response
|
||||
/// afterwhich it's assumes that we're completely done with TTY
|
||||
fn internal_cleanup(&self) -> Result<(), AppError> {
|
||||
match self {
|
||||
Self::External(_) => Ok(()),
|
||||
Self::Internal(_) => {
|
||||
let waiting = Arc::new(AtomicBool::new(true));
|
||||
let waiting_thread = Arc::clone(&waiting);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut bytes = Vec::with_capacity(26);
|
||||
while waiting_thread.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
let mut buf = [0];
|
||||
if let Ok(mut f) = std::fs::File::open(TTY) {
|
||||
if f.read_exact(&mut buf).is_err() {
|
||||
waiting_thread.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
bytes.push(buf[0]);
|
||||
if byte_sequence_valid(&bytes) {
|
||||
waiting_thread.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.write_all(KEYBOARD_PROTO.as_bytes()).ok();
|
||||
stdout.flush().ok();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
while waiting.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
if start.elapsed().as_millis() > 1500 {
|
||||
waiting.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
return Err(AppError::Terminal);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&self, tty_size: Option<Size>) -> Result<(), AppError> {
|
||||
match self {
|
||||
Self::External(id) => {
|
||||
Self::exec_external(id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Self::Internal((id, docker)) => self.exec_internal(id, docker, tty_size).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use crossterm::event::{KeyCode, KeyModifiers, MouseEvent};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum InputMessages {
|
||||
ButtonPress((KeyCode, KeyModifiers)),
|
||||
MouseEvent((MouseEvent, KeyModifiers)),
|
||||
}
|
||||
@@ -1,871 +0,0 @@
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{BufWriter, Write},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use bollard::query_parameters::LogsOptions;
|
||||
use cansi::v3::categorise_text;
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
||||
execute,
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::layout::Rect;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod message;
|
||||
use crate::{
|
||||
app_data::{AppData, DockerCommand, Header, ScrollDirection},
|
||||
app_error::AppError,
|
||||
config,
|
||||
docker_data::DockerMessage,
|
||||
exec::{ExecMode, tty_readable},
|
||||
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
|
||||
};
|
||||
pub use message::InputMessages;
|
||||
|
||||
/// Handle all input events
|
||||
#[derive(Debug)]
|
||||
pub struct InputHandler {
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
docker_tx: Sender<DockerMessage>,
|
||||
keymap: config::Keymap,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
mouse_capture: bool,
|
||||
rx: Receiver<InputMessages>,
|
||||
}
|
||||
|
||||
impl InputHandler {
|
||||
/// Initialize self, and running the message handling loop
|
||||
pub async fn start(
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
docker_tx: Sender<DockerMessage>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
rx: Receiver<InputMessages>,
|
||||
) {
|
||||
let keymap = app_data.lock().config.keymap.clone();
|
||||
let mut inner = Self {
|
||||
app_data,
|
||||
docker_tx,
|
||||
gui_state,
|
||||
is_running,
|
||||
keymap,
|
||||
rx,
|
||||
mouse_capture: true,
|
||||
};
|
||||
inner.message_handler().await;
|
||||
}
|
||||
|
||||
/// check for incoming messages
|
||||
async fn message_handler(&mut self) {
|
||||
while let Some(message) = self.rx.recv().await {
|
||||
match message {
|
||||
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
|
||||
InputMessages::MouseEvent((mouse_event, modifider)) => {
|
||||
let status = self.gui_state.lock().get_status();
|
||||
let contains = |s: Status| status.contains(&s);
|
||||
|
||||
if contains(Status::DeleteConfirm) {
|
||||
self.button_intersect(mouse_event).await;
|
||||
} else if !contains(Status::Error)
|
||||
&& !contains(Status::Help)
|
||||
&& !contains(Status::DeleteConfirm)
|
||||
&& !contains(Status::Filter)
|
||||
&& !contains(Status::SearchLogs)
|
||||
{
|
||||
// TODO handle state where you want to scroll log search results with the mouse wheel
|
||||
self.mouse_press(mouse_event, modifider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort the containers by a given header
|
||||
fn sort(&self, selected_header: Header) {
|
||||
self.app_data.lock().set_sort_by_header(selected_header);
|
||||
}
|
||||
|
||||
/// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead
|
||||
/// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
|
||||
fn quit(&self) {
|
||||
let status = self.gui_state.lock().get_status();
|
||||
let contains = |s: Status| status.contains(&s);
|
||||
if !contains(Status::Error) | !contains(Status::Init) {
|
||||
self.is_running
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// This is executed from the Delete Confirm dialog, and will send an internal message to actually remove the given container
|
||||
async fn confirm_delete(&self) {
|
||||
let id = self.gui_state.lock().get_delete_container();
|
||||
if let Some(id) = id {
|
||||
self.docker_tx
|
||||
.send(DockerMessage::Control((DockerCommand::Delete, id)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// This is executed from the Delete Confirm dialog, and will clear the delete_container information (removes id and closes panel)
|
||||
fn clear_delete(&self) {
|
||||
self.gui_state.lock().set_delete_container(None);
|
||||
}
|
||||
|
||||
async fn inspect_key(&self) {
|
||||
self.app_data.lock().clear_inspect_data();
|
||||
let selected = self.app_data.lock().get_selected_container().cloned();
|
||||
if let Some(g) = selected {
|
||||
self.docker_tx.send(DockerMessage::Inspect(g.id)).await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that one can exec into a Docker container
|
||||
async fn exec_key(&self) {
|
||||
let is_oxker = self.app_data.lock().is_oxker();
|
||||
if !is_oxker && tty_readable() {
|
||||
let uuid = Uuid::new_v4();
|
||||
GuiState::start_loading_animation(&self.gui_state, uuid);
|
||||
let (sx, rx) = tokio::sync::oneshot::channel();
|
||||
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
|
||||
|
||||
if let Ok(docker) = rx.await {
|
||||
(ExecMode::new(&self.app_data, &docker).await).map_or_else(
|
||||
|| {
|
||||
self.app_data.lock().set_error(
|
||||
AppError::DockerExec,
|
||||
&self.gui_state,
|
||||
Status::Error,
|
||||
);
|
||||
},
|
||||
|mode| {
|
||||
self.gui_state.lock().set_exec_mode(mode);
|
||||
},
|
||||
);
|
||||
}
|
||||
self.gui_state.lock().stop_loading_animation(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the mouse capture (via input of the 'm' key)
|
||||
fn mouse_capture_key(&mut self) {
|
||||
let err = || {
|
||||
self.app_data.lock().set_error(
|
||||
AppError::MouseCapture(!self.mouse_capture),
|
||||
&self.gui_state,
|
||||
Status::Error,
|
||||
);
|
||||
};
|
||||
if self.mouse_capture {
|
||||
if execute!(std::io::stdout(), DisableMouseCapture).is_ok() {
|
||||
self.gui_state
|
||||
.lock()
|
||||
.set_info_box("✖ mouse capture disabled");
|
||||
} else {
|
||||
err();
|
||||
}
|
||||
} else if Ui::enable_mouse_capture().is_ok() {
|
||||
self.gui_state
|
||||
.lock()
|
||||
.set_info_box("✓ mouse capture enabled");
|
||||
} else {
|
||||
err();
|
||||
}
|
||||
|
||||
self.mouse_capture = !self.mouse_capture;
|
||||
}
|
||||
|
||||
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
|
||||
async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = self.app_data.lock().config.clone();
|
||||
let container = self.app_data.lock().get_selected_container_id_state_name();
|
||||
if let Some((id, _, name)) = container
|
||||
&& let Some(log_path) = args.dir_save
|
||||
{
|
||||
let (sx, rx) = tokio::sync::oneshot::channel();
|
||||
self.docker_tx.send(DockerMessage::Exec(sx)).await?;
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |i| i.as_secs());
|
||||
|
||||
let path = log_path.join(format!("{name}_{now}.log"));
|
||||
|
||||
let options = Some(LogsOptions {
|
||||
stderr: true,
|
||||
stdout: true,
|
||||
timestamps: args.show_timestamp,
|
||||
since: 0,
|
||||
..Default::default()
|
||||
});
|
||||
let mut logs = rx.await?.logs(id.get(), options);
|
||||
let mut output = vec![];
|
||||
|
||||
while let Some(Ok(value)) = logs.next().await {
|
||||
let data = value.to_string();
|
||||
if !data.trim().is_empty() {
|
||||
output.push(
|
||||
categorise_text(&data)
|
||||
.into_iter()
|
||||
.map(|i| i.text)
|
||||
.collect::<String>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if !output.is_empty() {
|
||||
let mut stream = BufWriter::new(
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&path)?,
|
||||
);
|
||||
|
||||
for line in &output {
|
||||
stream.write_all(line.as_bytes())?;
|
||||
}
|
||||
stream.flush()?;
|
||||
|
||||
self.gui_state
|
||||
.lock()
|
||||
.set_info_box(&format!("saved to {}", path.display()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to save the currently selected container logs to a file
|
||||
async fn save_key(&self) {
|
||||
let status = self.gui_state.lock().get_status();
|
||||
let contains = |s: Status| status.contains(&s);
|
||||
|
||||
if !contains(Status::Logs) {
|
||||
self.gui_state.lock().status_push(Status::Logs);
|
||||
let uuid = Uuid::new_v4();
|
||||
GuiState::start_loading_animation(&self.gui_state, uuid);
|
||||
if self.save_logs().await.is_err() {
|
||||
self.app_data.lock().set_error(
|
||||
AppError::DockerLogs,
|
||||
&self.gui_state,
|
||||
Status::Error,
|
||||
);
|
||||
}
|
||||
self.gui_state.lock().status_del(Status::Logs);
|
||||
self.gui_state.lock().stop_loading_animation(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send docker command, if the Commands panel is selected
|
||||
async fn enter_key(&self) {
|
||||
// This isn't great, just means you can't send docker commands before full initialization of the program
|
||||
let panel = self.gui_state.lock().get_selected_panel();
|
||||
if panel == SelectablePanel::Commands {
|
||||
let option_command = self.app_data.lock().selected_docker_controls();
|
||||
|
||||
if let Some(command) = option_command {
|
||||
// Poor way of disallowing commands to be sent to a containerised okxer
|
||||
if self.app_data.lock().is_oxker_in_container() {
|
||||
return;
|
||||
}
|
||||
let option_id = self.app_data.lock().get_selected_container_id();
|
||||
if let Some(id) = option_id {
|
||||
match command {
|
||||
DockerCommand::Delete => self
|
||||
.docker_tx
|
||||
.send(DockerMessage::ConfirmDelete(id))
|
||||
.await
|
||||
.ok(),
|
||||
|
||||
_ => self
|
||||
.docker_tx
|
||||
.send(DockerMessage::Control((command, id)))
|
||||
.await
|
||||
.ok(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If keymap.scroll_modifier is pressed, return 10, else return 1, to speed up scrolling
|
||||
fn get_modifier_total(&self, modifier: KeyModifiers) -> u8 {
|
||||
if modifier == self.keymap.scroll_many {
|
||||
10
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_scroll(&self, modifier: KeyModifiers, sd: &ScrollDirection) {
|
||||
for _ in 0..self.get_modifier_total(modifier) {
|
||||
self.gui_state.lock().set_inspect_offset(sd);
|
||||
}
|
||||
}
|
||||
|
||||
// fn inspect_scroll(&self, modifier: KeyModifiers, sd: &ScrollDirection) {
|
||||
// for _ in 0..self.get_modifier_total(modifier) {
|
||||
// self.gui_state.lock().set_inspect_offset(sd);
|
||||
// }
|
||||
// }
|
||||
|
||||
fn logs_horizontal_scroll(&self, modifier: KeyModifiers, sd: &ScrollDirection) {
|
||||
let panel = self.gui_state.lock().get_selected_panel();
|
||||
if panel == SelectablePanel::Logs {
|
||||
for _ in 0..self.get_modifier_total(modifier) {
|
||||
let width = self.gui_state.lock().get_screen_width();
|
||||
self.app_data.lock().logs_horizontal_scroll(sd, width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the the "next" selectable panel
|
||||
/// If no containers, and on Commands panel, skip to next panel, as Commands panel isn't visible in this state
|
||||
fn next_panel_key(&self) {
|
||||
self.gui_state.lock().selectable_panel_next(&self.app_data);
|
||||
}
|
||||
|
||||
/// Change to previously selected panel
|
||||
/// Need to skip the commands planel if there no are current containers running
|
||||
fn previous_panel_key(&self) {
|
||||
self.gui_state
|
||||
.lock()
|
||||
.selectable_panel_previous(&self.app_data);
|
||||
}
|
||||
|
||||
fn scroll_start_key(&self) {
|
||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => self.app_data.lock().containers_start(),
|
||||
SelectablePanel::Logs => self.app_data.lock().log_start(),
|
||||
SelectablePanel::Commands => self.app_data.lock().docker_controls_start(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to end of the list of the currently selected panel
|
||||
fn scroll_end_key(&self) {
|
||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => self.app_data.lock().containers_end(),
|
||||
SelectablePanel::Logs => self.app_data.lock().log_end(),
|
||||
SelectablePanel::Commands => self.app_data.lock().docker_controls_end(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Actions to take when in Help status active
|
||||
fn handle_help(&mut self, key_code: KeyCode) {
|
||||
if self.keymap.clear.0 == key_code
|
||||
|| self.keymap.clear.1 == Some(key_code)
|
||||
|| self.keymap.toggle_help.0 == key_code
|
||||
|| self.keymap.toggle_help.1 == Some(key_code)
|
||||
{
|
||||
self.gui_state.lock().status_del(Status::Help);
|
||||
}
|
||||
|
||||
if self.keymap.toggle_mouse_capture.0 == key_code
|
||||
|| self.keymap.toggle_mouse_capture.1 == Some(key_code)
|
||||
{
|
||||
self.mouse_capture_key();
|
||||
}
|
||||
}
|
||||
|
||||
/// Actions to take when Error status active
|
||||
fn handle_error(&self, key_code: KeyCode) {
|
||||
if self.keymap.clear.0 == key_code || self.keymap.clear.1 == Some(key_code) {
|
||||
self.app_data.lock().remove_error();
|
||||
self.gui_state.lock().status_del(Status::Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Actions to take when Delete status active
|
||||
async fn handle_delete(&self, key_code: KeyCode) {
|
||||
if self.keymap.delete_confirm.0 == key_code
|
||||
|| self.keymap.delete_confirm.1 == Some(key_code)
|
||||
{
|
||||
self.confirm_delete().await;
|
||||
} else if self.keymap.delete_deny.0 == key_code
|
||||
|| self.keymap.delete_deny.1 == Some(key_code)
|
||||
|| self.keymap.clear.0 == key_code
|
||||
|| self.keymap.clear.1 == Some(key_code)
|
||||
{
|
||||
self.clear_delete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Actions to take when Filter status active
|
||||
fn handle_search_logs(&self, key_code: KeyCode, modifier: KeyModifiers) {
|
||||
match key_code {
|
||||
KeyCode::Esc => {
|
||||
self.app_data.lock().logs_search_clear();
|
||||
self.gui_state.lock().status_del(Status::SearchLogs);
|
||||
}
|
||||
_ if KeyCode::Enter == key_code
|
||||
|| self.keymap.log_search_mode.0 == key_code
|
||||
|| self.keymap.log_search_mode.1 == Some(key_code) =>
|
||||
{
|
||||
self.gui_state.lock().status_del(Status::SearchLogs);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_back.0 == key_code
|
||||
|| self.keymap.scroll_back.1 == Some(key_code) =>
|
||||
{
|
||||
self.logs_horizontal_scroll(modifier, &ScrollDirection::Up);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_forward.0 == key_code
|
||||
|| self.keymap.scroll_forward.1 == Some(key_code) =>
|
||||
{
|
||||
self.logs_horizontal_scroll(modifier, &ScrollDirection::Down);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_down.0 == key_code => {
|
||||
self.app_data
|
||||
.lock()
|
||||
.log_search_scroll(&ScrollDirection::Down);
|
||||
// TODO should only do this is log_search_scroll returns some
|
||||
// Need to wait til app_data and gui_data is combined
|
||||
self.gui_state
|
||||
.lock()
|
||||
.set_logs_panel_selected(&self.app_data);
|
||||
//
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_up.0 == key_code => {
|
||||
self.app_data.lock().log_search_scroll(&ScrollDirection::Up);
|
||||
// TODO should only do this is log_search_scroll returns some
|
||||
// Need to wait til app_data and gui_data is combined
|
||||
self.gui_state
|
||||
.lock()
|
||||
.set_logs_panel_selected(&self.app_data);
|
||||
}
|
||||
|
||||
// handle up and down keys
|
||||
KeyCode::Backspace => {
|
||||
self.app_data.lock().log_search_pop();
|
||||
}
|
||||
KeyCode::Char(x) => {
|
||||
self.app_data.lock().log_search_push(x);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Actions to take when Filter status active
|
||||
fn handle_inspect(&mut self, key_code: KeyCode, modifier: KeyModifiers) {
|
||||
match key_code {
|
||||
_ if self.keymap.inspect.0 == key_code
|
||||
|| self.keymap.inspect.1 == Some(key_code)
|
||||
|| self.keymap.clear.0 == key_code
|
||||
|| self.keymap.clear.1 == Some(key_code) =>
|
||||
{
|
||||
self.app_data.lock().clear_inspect_data();
|
||||
self.gui_state.lock().clear_inspect_offset();
|
||||
self.gui_state.lock().status_del(Status::Inspect);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_down.0 == key_code
|
||||
|| self.keymap.scroll_down.1 == Some(key_code) =>
|
||||
{
|
||||
self.inspect_scroll(modifier, &ScrollDirection::Down);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_up.0 == key_code
|
||||
|| self.keymap.scroll_up.1 == Some(key_code) =>
|
||||
{
|
||||
self.inspect_scroll(modifier, &ScrollDirection::Up);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_forward.0 == key_code
|
||||
|| self.keymap.scroll_forward.1 == Some(key_code) =>
|
||||
{
|
||||
self.inspect_scroll(modifier, &ScrollDirection::Right);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_back.0 == key_code
|
||||
|| self.keymap.scroll_back.1 == Some(key_code) =>
|
||||
{
|
||||
self.inspect_scroll(modifier, &ScrollDirection::Left);
|
||||
}
|
||||
|
||||
_ if self.keymap.toggle_mouse_capture.0 == key_code
|
||||
|| self.keymap.toggle_mouse_capture.1 == Some(key_code) =>
|
||||
{
|
||||
self.mouse_capture_key();
|
||||
}
|
||||
_ if self.keymap.scroll_start.0 == key_code
|
||||
|| self.keymap.scroll_start.1 == Some(key_code) =>
|
||||
{
|
||||
self.gui_state.lock().clear_inspect_offset();
|
||||
}
|
||||
_ if self.keymap.scroll_end.0 == key_code
|
||||
|| self.keymap.scroll_end.1 == Some(key_code) =>
|
||||
{
|
||||
self.gui_state.lock().set_inspect_offset_y_to_max();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Actions to take when Filter status active
|
||||
fn handle_filter(&self, key_code: KeyCode) {
|
||||
match key_code {
|
||||
KeyCode::Esc => {
|
||||
self.app_data.lock().filter_term_clear();
|
||||
self.gui_state.lock().status_del(Status::Filter);
|
||||
}
|
||||
_ if KeyCode::Enter == key_code
|
||||
|| self.keymap.filter_mode.0 == key_code
|
||||
|| self.keymap.filter_mode.1 == Some(key_code) =>
|
||||
{
|
||||
self.gui_state.lock().status_del(Status::Filter);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
self.app_data.lock().filter_term_pop();
|
||||
}
|
||||
KeyCode::Char(x) => {
|
||||
self.app_data.lock().filter_term_push(x);
|
||||
}
|
||||
KeyCode::Right => {
|
||||
self.app_data.lock().filter_by_next();
|
||||
}
|
||||
KeyCode::Left => {
|
||||
self.app_data.lock().filter_by_prev();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle input that refers to the sorting of columns
|
||||
fn handle_sort(&self, key_code: KeyCode) {
|
||||
match key_code {
|
||||
_ if self.keymap.force_redraw.0 == key_code
|
||||
|| self.keymap.force_redraw.1 == Some(key_code) =>
|
||||
{
|
||||
self.gui_state.lock().set_clear();
|
||||
}
|
||||
_ if self.keymap.sort_reset.0 == key_code
|
||||
|| self.keymap.sort_reset.1 == Some(key_code) =>
|
||||
{
|
||||
self.app_data.lock().reset_sorted();
|
||||
}
|
||||
|
||||
_ if self.keymap.sort_by_name.0 == key_code
|
||||
|| self.keymap.sort_by_name.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::Name);
|
||||
}
|
||||
|
||||
_ if self.keymap.sort_by_state.0 == key_code
|
||||
|| self.keymap.sort_by_state.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::State);
|
||||
}
|
||||
|
||||
_ if self.keymap.sort_by_status.0 == key_code
|
||||
|| self.keymap.sort_by_status.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::Status);
|
||||
}
|
||||
|
||||
_ if self.keymap.sort_by_cpu.0 == key_code
|
||||
|| self.keymap.sort_by_cpu.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::Cpu);
|
||||
}
|
||||
_ if self.keymap.sort_by_memory.0 == key_code
|
||||
|| self.keymap.sort_by_memory.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::Memory);
|
||||
}
|
||||
_ if self.keymap.sort_by_id.0 == key_code
|
||||
|| self.keymap.sort_by_id.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::Id);
|
||||
}
|
||||
_ if self.keymap.sort_by_image.0 == key_code
|
||||
|| self.keymap.sort_by_image.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::Image);
|
||||
}
|
||||
|
||||
_ if self.keymap.sort_by_rx.0 == key_code
|
||||
|| self.keymap.sort_by_rx.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::Rx);
|
||||
}
|
||||
|
||||
_ if self.keymap.sort_by_tx.0 == key_code
|
||||
|| self.keymap.sort_by_tx.1 == Some(key_code) =>
|
||||
{
|
||||
self.sort(Header::Tx);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Increase the log panel height
|
||||
fn log_panel_height_increase(&self) {
|
||||
self.gui_state.lock().log_height_increase();
|
||||
}
|
||||
|
||||
// Decrease the log panel height
|
||||
fn log_panel_height_decrease(&self) {
|
||||
self.gui_state.lock().log_height_decrease();
|
||||
}
|
||||
|
||||
// Toggle visibility of the log panel
|
||||
fn log_panel_toggle(&self) {
|
||||
self.gui_state.lock().toggle_show_logs();
|
||||
}
|
||||
|
||||
/// Handle button presses in all other scenarios
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
async fn handle_others(&mut self, key_code: KeyCode, modifier: KeyModifiers) {
|
||||
self.handle_sort(key_code);
|
||||
// shift key plus arrows
|
||||
match key_code {
|
||||
_ if self.keymap.exec.0 == key_code || self.keymap.exec.1 == Some(key_code) => {
|
||||
self.exec_key().await;
|
||||
}
|
||||
|
||||
_ if self.keymap.toggle_help.0 == key_code
|
||||
|| self.keymap.toggle_help.1 == Some(key_code) =>
|
||||
{
|
||||
self.gui_state.lock().status_push(Status::Help);
|
||||
}
|
||||
|
||||
_ if self.keymap.toggle_mouse_capture.0 == key_code
|
||||
|| self.keymap.toggle_mouse_capture.1 == Some(key_code) =>
|
||||
{
|
||||
self.mouse_capture_key();
|
||||
}
|
||||
_ if self.keymap.log_section_height_decrease.0 == key_code
|
||||
|| self.keymap.log_section_height_decrease.1 == Some(key_code) =>
|
||||
{
|
||||
self.log_panel_height_decrease();
|
||||
}
|
||||
|
||||
_ if self.keymap.log_section_height_increase.0 == key_code
|
||||
|| self.keymap.log_section_height_increase.1 == Some(key_code) =>
|
||||
{
|
||||
self.log_panel_height_increase();
|
||||
}
|
||||
|
||||
_ if self.keymap.log_section_toggle.0 == key_code
|
||||
|| self.keymap.log_section_toggle.1 == Some(key_code) =>
|
||||
{
|
||||
self.log_panel_toggle();
|
||||
}
|
||||
|
||||
_ if self.keymap.save_logs.0 == key_code
|
||||
|| self.keymap.save_logs.1 == Some(key_code) =>
|
||||
{
|
||||
self.save_key().await;
|
||||
}
|
||||
|
||||
_ if self.keymap.inspect.0 == key_code || self.keymap.inspect.1 == Some(key_code) => {
|
||||
self.inspect_key().await;
|
||||
}
|
||||
|
||||
_ if self.keymap.select_next_panel.0 == key_code
|
||||
|| self.keymap.select_next_panel.1 == Some(key_code) =>
|
||||
{
|
||||
self.next_panel_key();
|
||||
}
|
||||
|
||||
_ if self.keymap.select_previous_panel.0 == key_code
|
||||
|| self.keymap.select_previous_panel.1 == Some(key_code) =>
|
||||
{
|
||||
self.previous_panel_key();
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_start.0 == key_code
|
||||
|| self.keymap.scroll_start.1 == Some(key_code) =>
|
||||
{
|
||||
self.scroll_start_key();
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_end.0 == key_code
|
||||
|| self.keymap.scroll_end.1 == Some(key_code) =>
|
||||
{
|
||||
self.scroll_end_key();
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_up.0 == key_code
|
||||
|| self.keymap.scroll_up.1 == Some(key_code) =>
|
||||
{
|
||||
self.scroll(modifier, &ScrollDirection::Up);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_down.0 == key_code
|
||||
|| self.keymap.scroll_down.1 == Some(key_code) =>
|
||||
{
|
||||
self.scroll(modifier, &ScrollDirection::Down);
|
||||
}
|
||||
|
||||
_ if self.keymap.filter_mode.0 == key_code
|
||||
|| self.keymap.filter_mode.1 == Some(key_code) =>
|
||||
{
|
||||
self.gui_state.lock().status_push(Status::Filter);
|
||||
self.docker_tx.send(DockerMessage::Update).await.ok();
|
||||
}
|
||||
|
||||
_ if self.keymap.log_search_mode.0 == key_code
|
||||
|| self.keymap.log_search_mode.1 == Some(key_code) =>
|
||||
{
|
||||
if !self.gui_state.lock().get_show_logs() {
|
||||
self.gui_state.lock().toggle_show_logs();
|
||||
}
|
||||
self.gui_state.lock().status_push(Status::SearchLogs);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_back.0 == key_code
|
||||
|| self.keymap.scroll_back.1 == Some(key_code) =>
|
||||
{
|
||||
self.logs_horizontal_scroll(modifier, &ScrollDirection::Up);
|
||||
// self.logs_back(modifier);
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_forward.0 == key_code
|
||||
|| self.keymap.scroll_forward.1 == Some(key_code) =>
|
||||
{
|
||||
self.logs_horizontal_scroll(modifier, &ScrollDirection::Down);
|
||||
}
|
||||
|
||||
KeyCode::Enter => self.enter_key().await,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle keyboard button events
|
||||
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
|
||||
let status = self.gui_state.lock().get_status();
|
||||
let contains = |s: Status| status.contains(&s);
|
||||
|
||||
let contains_error = contains(Status::Error);
|
||||
let contains_help = contains(Status::Help);
|
||||
let contains_exec = contains(Status::Exec);
|
||||
let contains_filter = contains(Status::Filter);
|
||||
let contains_delete = contains(Status::DeleteConfirm);
|
||||
let contains_search_logs = contains(Status::SearchLogs);
|
||||
let contains_inspect = contains(Status::Inspect);
|
||||
|
||||
if !contains_exec {
|
||||
let is_q = || key_code == self.keymap.quit.0 || Some(key_code) == self.keymap.quit.1;
|
||||
if key_modifier == KeyModifiers::CONTROL && key_code == KeyCode::Char('c')
|
||||
|| is_q() && !contains_filter && !contains_search_logs
|
||||
{
|
||||
// Always just quit on Ctrl + c/C or q/Q, unless in filter/search_logs mode, i.e. when user inmput can include the q key
|
||||
self.quit();
|
||||
}
|
||||
|
||||
if contains_error {
|
||||
self.handle_error(key_code);
|
||||
} else if contains_help {
|
||||
self.handle_help(key_code);
|
||||
} else if contains_filter {
|
||||
self.handle_filter(key_code);
|
||||
} else if contains_search_logs {
|
||||
self.handle_search_logs(key_code, key_modifier);
|
||||
} else if contains_delete {
|
||||
self.handle_delete(key_code).await;
|
||||
} else if contains_inspect {
|
||||
self.handle_inspect(key_code, key_modifier);
|
||||
} else {
|
||||
self.handle_others(key_code, key_modifier).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window
|
||||
async fn button_intersect(&self, mouse_event: MouseEvent) {
|
||||
if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
|
||||
let intersect = self.gui_state.lock().get_intersect_button(Rect::new(
|
||||
mouse_event.column,
|
||||
mouse_event.row,
|
||||
1,
|
||||
1,
|
||||
));
|
||||
|
||||
if let Some(button) = intersect {
|
||||
match button {
|
||||
DeleteButton::Confirm => self.confirm_delete().await,
|
||||
DeleteButton::Cancel => self.clear_delete(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle mouse button events
|
||||
fn mouse_press(&self, mouse_event: MouseEvent, modifier: KeyModifiers) {
|
||||
let status = self.gui_state.lock().get_status();
|
||||
|
||||
if status.contains(&Status::Inspect) {
|
||||
match mouse_event.kind {
|
||||
MouseEventKind::ScrollDown => self.inspect_scroll(modifier, &ScrollDirection::Down),
|
||||
MouseEventKind::ScrollUp => self.inspect_scroll(modifier, &ScrollDirection::Up),
|
||||
MouseEventKind::ScrollRight => {
|
||||
self.inspect_scroll(modifier, &ScrollDirection::Right)
|
||||
}
|
||||
MouseEventKind::ScrollLeft => self.inspect_scroll(modifier, &ScrollDirection::Left),
|
||||
_ => (),
|
||||
}
|
||||
} else if status.contains(&Status::Help) {
|
||||
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
|
||||
let help_intersect = self.gui_state.lock().get_intersect_help(mouse_point);
|
||||
if help_intersect {
|
||||
self.gui_state.lock().status_del(Status::Help);
|
||||
}
|
||||
} else {
|
||||
match mouse_event.kind {
|
||||
MouseEventKind::ScrollUp => self.scroll(modifier, &ScrollDirection::Up),
|
||||
MouseEventKind::ScrollDown => self.scroll(modifier, &ScrollDirection::Down),
|
||||
// TODO left and right for log offsets
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
|
||||
let header = self.gui_state.lock().get_intersect_header(mouse_point);
|
||||
if let Some(header) = header {
|
||||
self.sort(header);
|
||||
}
|
||||
let help_intersect = self.gui_state.lock().get_intersect_help(mouse_point);
|
||||
if help_intersect {
|
||||
self.gui_state.lock().status_push(Status::Help);
|
||||
}
|
||||
|
||||
self.gui_state.lock().check_panel_intersect(mouse_point);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change state to next, depending which panel is currently in focus
|
||||
fn scroll(&self, modifier: KeyModifiers, scroll: &ScrollDirection) {
|
||||
let status = self.gui_state.lock().get_status();
|
||||
if status.contains(&Status::SearchLogs) {
|
||||
self.app_data.lock().log_search_scroll(scroll);
|
||||
} else {
|
||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => {
|
||||
for _ in 0..self.get_modifier_total(modifier) {
|
||||
self.app_data.lock().containers_scroll(scroll);
|
||||
}
|
||||
}
|
||||
SelectablePanel::Logs => {
|
||||
for _ in 0..self.get_modifier_total(modifier) {
|
||||
self.app_data.lock().log_scroll(scroll);
|
||||
}
|
||||
}
|
||||
SelectablePanel::Commands => self.app_data.lock().docker_controls_scroll(scroll),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-269
@@ -1,269 +0,0 @@
|
||||
// #![allow(unused)]
|
||||
// Zigbuild is stuck on 1.87.0, which means Mac builds won't work when using collapsible ifs
|
||||
|
||||
use app_data::AppData;
|
||||
use app_error::AppError;
|
||||
use bollard::{API_DEFAULT_VERSION, Docker};
|
||||
use config::Config;
|
||||
use docker_data::DockerData;
|
||||
use input_handler::InputMessages;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
process,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{Level, error, info};
|
||||
|
||||
mod app_data;
|
||||
mod app_error;
|
||||
mod config;
|
||||
mod docker_data;
|
||||
mod exec;
|
||||
mod input_handler;
|
||||
mod ui;
|
||||
|
||||
use ui::{GuiState, Rerender, Status, Ui};
|
||||
|
||||
use crate::docker_data::DockerMessage;
|
||||
|
||||
/// This is the entry point when running as a Docker Container, and is used, in conjunction with the `CONTAINER_ENV` ENV, to check if we are running as a Docker Container
|
||||
const ENTRY_POINT: &str = "/app/oxker";
|
||||
const ENV_KEY: &str = "OXKER_RUNTIME";
|
||||
const ENV_VALUE: &str = "container";
|
||||
const DOCKER_HOST: &str = "DOCKER_HOST";
|
||||
|
||||
/// Enable tracing, only really used in debug mode, for now
|
||||
/// write to file if `-g` is set?
|
||||
fn setup_tracing() {
|
||||
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
|
||||
}
|
||||
|
||||
/// Read the optional docker_host path
|
||||
/// Bollard will use DOCKER_HOST env, so might be pointless here, although it will fix it's priority over any config setting
|
||||
fn read_docker_host(config: &Config) -> Option<String> {
|
||||
if let Some(x) = &config.host {
|
||||
Some(x.to_string())
|
||||
} else if let Ok(env) = std::env::var(DOCKER_HOST)
|
||||
&& !env.trim().is_empty()
|
||||
{
|
||||
Some(env)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Create docker daemon handler, and only spawn up the docker data handler if a ping returns non-error
|
||||
async fn docker_init(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
docker_rx: Receiver<DockerMessage>,
|
||||
docker_tx: Sender<DockerMessage>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let host = read_docker_host(&app_data.lock().config);
|
||||
|
||||
if let Ok(docker) = host
|
||||
.as_ref()
|
||||
.map_or_else(Docker::connect_with_defaults, |host| {
|
||||
Docker::connect_with_socket(host, 120, API_DEFAULT_VERSION)
|
||||
})
|
||||
&& docker.ping().await.is_ok()
|
||||
{
|
||||
tokio::spawn(DockerData::start(
|
||||
Arc::clone(app_data),
|
||||
docker,
|
||||
docker_rx,
|
||||
docker_tx,
|
||||
Arc::clone(gui_state),
|
||||
));
|
||||
} else {
|
||||
app_data.lock().set_error(
|
||||
AppError::DockerConnect,
|
||||
gui_state,
|
||||
Status::DockerConnect(host),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create data for, and then spawn a tokio thread, for the input handler
|
||||
fn handler_init(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
docker_sx: &Sender<DockerMessage>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
input_rx: Receiver<InputMessages>,
|
||||
is_running: &Arc<AtomicBool>,
|
||||
) {
|
||||
tokio::spawn(input_handler::InputHandler::start(
|
||||
Arc::clone(app_data),
|
||||
docker_sx.clone(),
|
||||
Arc::clone(gui_state),
|
||||
Arc::clone(is_running),
|
||||
input_rx,
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
setup_tracing();
|
||||
let config = config::Config::new();
|
||||
let redraw = Arc::new(Rerender::new());
|
||||
|
||||
let app_data = Arc::new(Mutex::new(AppData::new(config.clone(), &redraw)));
|
||||
let gui_state = Arc::new(Mutex::new(GuiState::new(&redraw, config.show_logs)));
|
||||
let is_running = Arc::new(AtomicBool::new(true));
|
||||
let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
|
||||
|
||||
docker_init(&app_data, docker_rx, docker_tx.clone(), &gui_state).await;
|
||||
|
||||
if config.gui {
|
||||
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
|
||||
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
|
||||
Ui::start(app_data, gui_state, input_tx, is_running, redraw).await;
|
||||
} else {
|
||||
info!("in debug mode\n");
|
||||
let mut now = std::time::Instant::now();
|
||||
// Debug mode for testing, less pointless now, will display some basic information
|
||||
while is_running.load(Ordering::SeqCst) {
|
||||
let err = app_data.lock().get_error();
|
||||
if let Some(err) = err {
|
||||
error!("{}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
if let Some(Ok(to_sleep)) = u128::from(config.docker_interval_ms)
|
||||
.checked_sub(now.elapsed().as_millis())
|
||||
.map(u64::try_from)
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_millis(to_sleep)).await;
|
||||
}
|
||||
let containers = app_data
|
||||
.lock()
|
||||
.get_container_items()
|
||||
.iter()
|
||||
.map(|i| format!("{i}"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !containers.is_empty() {
|
||||
for item in containers {
|
||||
info!("{item}");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
now = std::time::Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use bollard::service::{ContainerSummary, PortSummary};
|
||||
|
||||
use crate::{
|
||||
app_data::{
|
||||
AppData, ContainerId, ContainerItem, ContainerPorts, ContainerStatus, Filter,
|
||||
RunningState, State, StatefulList,
|
||||
},
|
||||
config::{AppColors, Config, Keymap},
|
||||
ui::Rerender,
|
||||
};
|
||||
|
||||
/// Default test config, has timestamps turned off
|
||||
pub fn gen_config() -> Config {
|
||||
Config {
|
||||
app_colors: AppColors::new(),
|
||||
color_logs: false,
|
||||
dir_save: None,
|
||||
dir_config: None,
|
||||
docker_interval_ms: 1000,
|
||||
gui: true,
|
||||
host: None,
|
||||
in_container: false,
|
||||
keymap: Keymap::new(),
|
||||
log_search_case_sensitive: true,
|
||||
raw_logs: false,
|
||||
show_logs: true,
|
||||
show_self: false,
|
||||
show_std_err: false,
|
||||
show_timestamp: false,
|
||||
timestamp_format: "HH:MM:SS.NNNNN dd-mm-yyyy".to_owned(),
|
||||
timezone: None,
|
||||
use_cli: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gen_item(id: &ContainerId, index: usize) -> ContainerItem {
|
||||
ContainerItem::new(
|
||||
u64::try_from(index).unwrap(),
|
||||
id.clone(),
|
||||
format!("image_{index}"),
|
||||
false,
|
||||
format!("container_{index}"),
|
||||
vec![ContainerPorts {
|
||||
ip: None,
|
||||
private: u16::try_from(index).unwrap_or(1) + 8000,
|
||||
public: None,
|
||||
}],
|
||||
State::Running(RunningState::Healthy),
|
||||
ContainerStatus::from(format!("Up {index} hour")),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
|
||||
AppData {
|
||||
containers: StatefulList::new(containers.to_vec()),
|
||||
hidden_containers: vec![],
|
||||
current_sorted_id: vec![],
|
||||
inspect_data: None,
|
||||
error: None,
|
||||
sorted_by: None,
|
||||
rerender: Arc::new(Rerender::new()),
|
||||
filter: Filter::new(),
|
||||
config: gen_config(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gen_containers() -> (Vec<ContainerId>, Vec<ContainerItem>) {
|
||||
let ids = (1..=3)
|
||||
.map(|i| ContainerId::from(format!("{i}").as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
let containers = ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, id)| gen_item(id, index + 1))
|
||||
.collect::<Vec<_>>();
|
||||
(ids, containers)
|
||||
}
|
||||
|
||||
pub fn gen_container_summary(index: usize, state: &str) -> ContainerSummary {
|
||||
ContainerSummary {
|
||||
image_manifest_descriptor: None,
|
||||
health: None,
|
||||
id: Some(format!("{index}")),
|
||||
names: Some(vec![format!("container_{}", index)]),
|
||||
image: Some(format!("image_{index}")),
|
||||
image_id: Some(format!("{index}")),
|
||||
command: None,
|
||||
created: Some(i64::try_from(index).unwrap()),
|
||||
ports: Some(vec![PortSummary {
|
||||
ip: None,
|
||||
private_port: u16::try_from(index).unwrap_or(1) + 8000,
|
||||
public_port: None,
|
||||
typ: None,
|
||||
}]),
|
||||
size_rw: None,
|
||||
size_root_fs: None,
|
||||
labels: None,
|
||||
state: Some(bollard::secret::ContainerSummaryStateEnum::from_str(state).unwrap()),
|
||||
status: Some(format!("Up {index} hour")),
|
||||
host_config: None,
|
||||
network_settings: None,
|
||||
mounts: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
pub mod log_sanitizer {
|
||||
|
||||
use cansi::{Color as CansiColor, Intensity, v3::categorise_text};
|
||||
use ratatui::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
};
|
||||
|
||||
/// Attempt to colorize the given string to ratatui standards
|
||||
pub fn colorize_logs<'a>(input: &str) -> Vec<Line<'a>> {
|
||||
vec![Line::from(
|
||||
categorise_text(input)
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut style = Style::default()
|
||||
.bg(color_ansi_to_tui(i.bg.unwrap_or(CansiColor::Black)))
|
||||
.fg(color_ansi_to_tui(i.fg.unwrap_or(CansiColor::White)));
|
||||
if i.blink.is_some() {
|
||||
style = style.add_modifier(Modifier::SLOW_BLINK);
|
||||
}
|
||||
if i.underline.is_some() {
|
||||
style = style.add_modifier(Modifier::UNDERLINED);
|
||||
}
|
||||
if i.reversed.is_some() {
|
||||
style = style.add_modifier(Modifier::REVERSED);
|
||||
}
|
||||
if i.intensity == Some(Intensity::Bold) {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if i.hidden.is_some() {
|
||||
style = style.add_modifier(Modifier::HIDDEN);
|
||||
}
|
||||
if i.strikethrough.is_some() {
|
||||
style = 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 ratatui Lines
|
||||
pub fn remove_ansi<'a>(input: &str) -> Vec<Line<'a>> {
|
||||
vec![Line::from(
|
||||
categorise_text(input)
|
||||
.into_iter()
|
||||
.map(|i| i.text)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_owned(),
|
||||
)]
|
||||
}
|
||||
|
||||
/// create ratatui Lines that exactly match the given strings
|
||||
pub fn raw<'a>(input: &str) -> Vec<Line<'a>> {
|
||||
vec![Line::from(input.escape_debug().collect::<String>())]
|
||||
}
|
||||
|
||||
/// Change from ansi to tui colors
|
||||
const fn color_ansi_to_tui(color: CansiColor) -> Color {
|
||||
match color {
|
||||
CansiColor::Black | CansiColor::BrightBlack => 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 | CansiColor::BrightWhite => Color::Gray,
|
||||
CansiColor::BrightRed => Color::LightRed,
|
||||
CansiColor::BrightGreen => Color::LightGreen,
|
||||
CansiColor::BrightYellow => Color::LightYellow,
|
||||
CansiColor::BrightBlue => Color::LightBlue,
|
||||
CansiColor::BrightMagenta => Color::LightMagenta,
|
||||
CansiColor::BrightCyan => Color::LightCyan,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui::{
|
||||
style::{Color, Style},
|
||||
text::{Line, Span},
|
||||
};
|
||||
|
||||
use super::log_sanitizer;
|
||||
|
||||
// This spells out "oxker", with each char having a foreground and background colour
|
||||
const INPUT: &str = "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m";
|
||||
|
||||
#[test]
|
||||
/// Return test raw, as in show escape codes
|
||||
fn test_color_match_raw() {
|
||||
let result = log_sanitizer::raw(INPUT);
|
||||
let expected = vec![Line {
|
||||
spans: [Span {
|
||||
content: std::borrow::Cow::Borrowed(
|
||||
"\\u{1b}[31;47mo\\u{1b}[32;40mx\\u{1b}[33;41mk\\u{1b}[34;42me\\u{1b}[35;43mr\\u{1b}[0m",
|
||||
),
|
||||
style: Style::default(),
|
||||
}]
|
||||
.to_vec(),
|
||||
alignment: None,
|
||||
style: Style::default(),
|
||||
}];
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Use the escape codes to colorize the text
|
||||
fn test_color_match_colorize() {
|
||||
let result = log_sanitizer::colorize_logs(INPUT);
|
||||
let expected = vec![Line {
|
||||
spans: vec![
|
||||
Span {
|
||||
content: std::borrow::Cow::Borrowed("o"),
|
||||
style: Style::default().fg(Color::Red).bg(Color::Gray),
|
||||
},
|
||||
Span {
|
||||
content: std::borrow::Cow::Borrowed("x"),
|
||||
style: Style::default().fg(Color::Green).bg(Color::Black),
|
||||
},
|
||||
Span {
|
||||
content: std::borrow::Cow::Borrowed("k"),
|
||||
style: Style::default().fg(Color::Yellow).bg(Color::Red),
|
||||
},
|
||||
Span {
|
||||
content: std::borrow::Cow::Borrowed("e"),
|
||||
style: Style::default().fg(Color::Blue).bg(Color::Green),
|
||||
},
|
||||
Span {
|
||||
content: std::borrow::Cow::Borrowed("r"),
|
||||
style: Style::default().fg(Color::Magenta).bg(Color::Yellow),
|
||||
},
|
||||
],
|
||||
alignment: None,
|
||||
style: Style::default(),
|
||||
}];
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Remove all escape ansi codes from given input
|
||||
fn test_color_match_remove_ansi() {
|
||||
let result = log_sanitizer::remove_ansi(INPUT);
|
||||
let expected = vec![Line {
|
||||
spans: vec![Span {
|
||||
content: std::borrow::Cow::Borrowed("oxker"),
|
||||
style: Style::default(),
|
||||
}],
|
||||
style: Style::default(),
|
||||
alignment: None,
|
||||
}];
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
||||
@@ -1,700 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols::{self, Marker},
|
||||
text::{Line, Span},
|
||||
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType},
|
||||
};
|
||||
|
||||
use super::FrameData;
|
||||
use crate::{
|
||||
app_data::{State, Stats},
|
||||
config::AppColors,
|
||||
};
|
||||
|
||||
fn make_chart<'a, T: Stats + Display>(
|
||||
state: State,
|
||||
colors: AppColors,
|
||||
dataset: Vec<Dataset<'a>>,
|
||||
current_rx: &'a T,
|
||||
max_rx: &'a T,
|
||||
current_tx: &'a T,
|
||||
max_tx: &'a T,
|
||||
) -> Chart<'a> {
|
||||
let gen_color = |state: &State, default: Color| {
|
||||
if state.is_healthy() {
|
||||
default
|
||||
} else {
|
||||
state.get_color(colors)
|
||||
}
|
||||
};
|
||||
|
||||
let mut labels = [
|
||||
Span::raw(""),
|
||||
Span::styled(
|
||||
format!("{max_rx}"),
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(gen_color(&state, colors.chart_bandwidth.max_rx)),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{max_tx}"),
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(gen_color(&state, colors.chart_bandwidth.max_tx)),
|
||||
),
|
||||
Span::raw(""),
|
||||
];
|
||||
|
||||
// Set the order of rx/tx on the y axis, based on which is the highest value
|
||||
if max_rx.get_value() > max_tx.get_value() {
|
||||
labels.reverse();
|
||||
}
|
||||
|
||||
Chart::new(dataset)
|
||||
.bg(colors.chart_bandwidth.background)
|
||||
.block(
|
||||
Block::default()
|
||||
.title_alignment(Alignment::Center)
|
||||
.title(Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" rx: {current_rx}"),
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(gen_color(&state, colors.chart_bandwidth.title_rx)),
|
||||
),
|
||||
Span::styled(
|
||||
format!(" tx: {current_tx} "),
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(gen_color(&state, colors.chart_bandwidth.title_tx)),
|
||||
),
|
||||
]))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(colors.chart_bandwidth.border)),
|
||||
)
|
||||
.x_axis(Axis::default().bounds([0.0, 60.0]))
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.labels(labels)
|
||||
.style(Style::default().fg(colors.chart_bandwidth.y_axis))
|
||||
.bounds([0.0, (max_rx.get_value()).max(max_tx.get_value()) + 0.01]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Draw bandwidth chart
|
||||
pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
||||
if let Some(x) = fd.chart_data.as_ref() {
|
||||
let mut dataset = vec![
|
||||
Dataset::default()
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(colors.chart_bandwidth.points_tx))
|
||||
.graph_type(GraphType::Line)
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(colors.chart_bandwidth.points_tx))
|
||||
.data(&x.tx.dataset),
|
||||
];
|
||||
dataset.extend(vec![
|
||||
Dataset::default()
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(colors.chart_bandwidth.points_rx))
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(colors.chart_bandwidth.points_rx))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&x.rx.dataset),
|
||||
]);
|
||||
|
||||
let chart = make_chart(
|
||||
x.state,
|
||||
colors,
|
||||
dataset,
|
||||
&x.rx.current,
|
||||
&x.rx.max,
|
||||
&x.tx.current,
|
||||
&x.tx.max,
|
||||
);
|
||||
|
||||
f.render_widget(chart, area);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::Color;
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerId, NetworkBandwidth, State},
|
||||
config::AppColors,
|
||||
ui::{
|
||||
FrameData,
|
||||
draw_blocks::tests::{COLOR_RX, COLOR_TX, get_result, test_setup},
|
||||
},
|
||||
};
|
||||
|
||||
const TX_DOTS: [(usize, usize); 14] = [
|
||||
(1, 21),
|
||||
(2, 19),
|
||||
(2, 20),
|
||||
(3, 18),
|
||||
(3, 19),
|
||||
(4, 10),
|
||||
(4, 11),
|
||||
(4, 17),
|
||||
(4, 18),
|
||||
(5, 16),
|
||||
(6, 14),
|
||||
(6, 15),
|
||||
(7, 13),
|
||||
(7, 14),
|
||||
];
|
||||
|
||||
const RX_DOTS: [(usize, usize); 15] = [
|
||||
(1, 21),
|
||||
(2, 19),
|
||||
(2, 20),
|
||||
(3, 18),
|
||||
(3, 19),
|
||||
(4, 10),
|
||||
(4, 11),
|
||||
(4, 17),
|
||||
(4, 18),
|
||||
(5, 16),
|
||||
(6, 16),
|
||||
(6, 15),
|
||||
(7, 13),
|
||||
(7, 14),
|
||||
(8, 13),
|
||||
];
|
||||
|
||||
const COMBINED_DOTS_RX: [(usize, usize); 15] = [
|
||||
(1, 21),
|
||||
(2, 19),
|
||||
(2, 20),
|
||||
(3, 18),
|
||||
(3, 19),
|
||||
(4, 10),
|
||||
(4, 11),
|
||||
(4, 17),
|
||||
(4, 18),
|
||||
(5, 16),
|
||||
(6, 15),
|
||||
(6, 16),
|
||||
(7, 13),
|
||||
(7, 14),
|
||||
(8, 13),
|
||||
];
|
||||
|
||||
const COMBINED_DOTS_TX: [(usize, usize); 8] = [
|
||||
(7, 19),
|
||||
(7, 20),
|
||||
(7, 21),
|
||||
(8, 14),
|
||||
(8, 15),
|
||||
(8, 16),
|
||||
(8, 17),
|
||||
(8, 18),
|
||||
];
|
||||
|
||||
#[test]
|
||||
/// When status is Running, but not data, charts drawn without dots etc, colours correct
|
||||
fn test_draw_blocks_charts_running_none() {
|
||||
let mut setup = test_setup(40, 10, true, true);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(9, _) | (1..=9, 0 | 39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Border first row only
|
||||
(0, 0..=4 | 34..=39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Title RX
|
||||
(0, 5..=18) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
// Title TX
|
||||
(0, 19..=33) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// Y axis
|
||||
(1..=8, 10) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// TX max
|
||||
(4, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// RX max
|
||||
(6, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test with TX data
|
||||
fn test_draw_blocks_charts_running_with_data_tx() {
|
||||
let mut setup = test_setup(40, 10, true, true);
|
||||
let mut tx = NetworkBandwidth::new();
|
||||
|
||||
for i in 0..=20 {
|
||||
tx.push(1000 * i * (10 + 5 * i));
|
||||
}
|
||||
|
||||
if let Some(item) = setup
|
||||
.app_data
|
||||
.lock()
|
||||
.get_container_by_id(&ContainerId::from("1"))
|
||||
{
|
||||
item.tx = tx;
|
||||
}
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(9, _) | (1..=9, 0 | 39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Border first row only
|
||||
(0, 0..=3 | 35..=39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Title RX
|
||||
(0, 4..=17) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
// Title TX
|
||||
(0, 18..=34) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// Y axis
|
||||
(1..=8, 12) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// TX max
|
||||
(4, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// RX max
|
||||
(6, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
// TX dots
|
||||
x if TX_DOTS.contains(&(row_index, result_cell_index)) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test with RX data
|
||||
fn test_draw_blocks_charts_running_with_data_rx() {
|
||||
let mut setup = test_setup(40, 10, true, true);
|
||||
let mut rx = NetworkBandwidth::new();
|
||||
|
||||
for i in 0..=20 {
|
||||
rx.push(2000 * i * (10 + 7 * i));
|
||||
}
|
||||
|
||||
if let Some(item) = setup
|
||||
.app_data
|
||||
.lock()
|
||||
.get_container_by_id(&ContainerId::from("1"))
|
||||
{
|
||||
item.rx = rx;
|
||||
}
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(9, _) | (1..=9, 0 | 39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Border first row only
|
||||
(0, 0..=3 | 35..=39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Title RX
|
||||
(0, 4..=19) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
// Title TX
|
||||
(0, 20..=34) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// Y axis
|
||||
(1..=8, 12) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// RX max
|
||||
(4, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
// TX max
|
||||
(6, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// RX dots
|
||||
x if RX_DOTS.contains(&(row_index, result_cell_index)) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test with RX & TX data
|
||||
fn test_draw_blocks_charts_running_with_data_tx_and_rx() {
|
||||
let mut setup = test_setup(40, 10, true, true);
|
||||
let mut rx = NetworkBandwidth::new();
|
||||
let mut tx = NetworkBandwidth::new();
|
||||
for i in 0..=20 {
|
||||
rx.push(2000 * i * (10 + 7 * i));
|
||||
tx.push(200 * i * (10 + 7 * i));
|
||||
}
|
||||
|
||||
if let Some(item) = setup
|
||||
.app_data
|
||||
.lock()
|
||||
.get_container_by_id(&ContainerId::from("1"))
|
||||
{
|
||||
item.rx = rx;
|
||||
item.tx = tx;
|
||||
}
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(9, _) | (1..=9, 0 | 39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Border first row only
|
||||
(0, 0..=3 | 36..=39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Title RX
|
||||
(0, 4..=19) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
// Title TX
|
||||
(0, 20..=35) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// Y axis
|
||||
(1..=8, 12) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// RX max
|
||||
(4, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
// TX max
|
||||
(6, 1..=10) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// TX dots
|
||||
x if COMBINED_DOTS_TX.contains(&(row_index, result_cell_index)) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_TX);
|
||||
}
|
||||
// RX dots
|
||||
x if COMBINED_DOTS_RX.contains(&(row_index, result_cell_index)) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, COLOR_RX);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Whens status paused, some text is now Yellow
|
||||
fn test_draw_blocks_charts_paused() {
|
||||
let mut setup = test_setup(40, 10, true, true);
|
||||
setup.app_data.lock().containers.items[0].state = State::Paused;
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(9, _) | (1..=9, 0 | 39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Border first row only
|
||||
(0, 0..=4 | 34..=39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Title & y-axis max
|
||||
(0, 5..=33) | (4 | 6, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
// Y axis
|
||||
(1..=8, 10) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Whens status dead, some text is now red
|
||||
fn test_draw_blocks_charts_dead() {
|
||||
let mut setup = test_setup(40, 10, true, true);
|
||||
setup.app_data.lock().containers.items[0].state = State::Dead;
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(9, _) | (1..=9, 0 | 39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Border first row only
|
||||
(0, 0..=4 | 34..=39) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// Title & y-axis max
|
||||
(0, 5..=33) | (4 | 6, 1..=9) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// Y axis
|
||||
(1..=8, 10) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colours correctly applied to each part of the charts
|
||||
fn test_draw_blocks_charts_custom_colors() {
|
||||
let mut colors = AppColors::new();
|
||||
|
||||
colors.chart_bandwidth.background = Color::White;
|
||||
colors.chart_bandwidth.border = Color::Red;
|
||||
colors.chart_bandwidth.max_rx = Color::Green;
|
||||
colors.chart_bandwidth.max_tx = Color::Magenta;
|
||||
colors.chart_bandwidth.title_rx = Color::LightGreen;
|
||||
colors.chart_bandwidth.title_tx = Color::LightRed;
|
||||
colors.chart_bandwidth.points_rx = Color::Black;
|
||||
colors.chart_bandwidth.points_tx = Color::Blue;
|
||||
colors.chart_bandwidth.y_axis = Color::Yellow;
|
||||
|
||||
let mut setup = test_setup(40, 10, true, true);
|
||||
|
||||
let mut rx = NetworkBandwidth::new();
|
||||
let mut tx = NetworkBandwidth::new();
|
||||
for i in 0..=20 {
|
||||
rx.push(2000 * i * (10 + 7 * i));
|
||||
tx.push(200 * i * (10 + 7 * i));
|
||||
}
|
||||
|
||||
if let Some(item) = setup
|
||||
.app_data
|
||||
.lock()
|
||||
.get_container_by_id(&ContainerId::from("1"))
|
||||
{
|
||||
item.rx = rx;
|
||||
item.tx = tx;
|
||||
}
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(9, _) | (1..=9, 0 | 39) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// Border first row only
|
||||
(0, 0..=3 | 36..=39) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// Title RX
|
||||
(0, 4..=19) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::LightGreen);
|
||||
}
|
||||
// Title TX
|
||||
(0, 20..=35) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::LightRed);
|
||||
}
|
||||
// Y axis
|
||||
(1..=8, 12) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
// RX max
|
||||
(4, 1..=11) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
}
|
||||
// TX max
|
||||
(6, 1..=10) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
// TX dots
|
||||
x if COMBINED_DOTS_TX.contains(&(row_index, result_cell_index)) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
// RX dots
|
||||
x if COMBINED_DOTS_RX.contains(&(row_index, result_cell_index)) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,476 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType},
|
||||
};
|
||||
|
||||
use super::{CONSTRAINT_50_50, FrameData};
|
||||
use crate::{
|
||||
app_data::{State, Stats},
|
||||
config::AppColors,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ChartVariant {
|
||||
Cpu,
|
||||
Memory,
|
||||
}
|
||||
|
||||
impl ChartVariant {
|
||||
const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Cpu => "cpu",
|
||||
Self::Memory => "memory",
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_title_color(self, colors: AppColors, state: State) -> Color {
|
||||
if state.is_healthy() {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.title,
|
||||
Self::Memory => colors.chart_memory.title,
|
||||
}
|
||||
} else {
|
||||
state.get_color(colors)
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_bg_color(self, colors: AppColors) -> Color {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.background,
|
||||
Self::Memory => colors.chart_memory.background,
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_border_color(self, colors: AppColors) -> Color {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.border,
|
||||
Self::Memory => colors.chart_memory.border,
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_y_axis_color(self, colors: AppColors) -> Color {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.y_axis,
|
||||
Self::Memory => colors.chart_memory.y_axis,
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_max_color(self, colors: AppColors, state: State) -> Color {
|
||||
if state.is_healthy() {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.max,
|
||||
Self::Memory => colors.chart_memory.max,
|
||||
}
|
||||
} else {
|
||||
state.get_color(colors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create charts
|
||||
fn make_chart<'a, T: Stats + Display>(
|
||||
chart_variant: ChartVariant,
|
||||
colors: AppColors,
|
||||
current: &'a T,
|
||||
dataset: Vec<Dataset<'a>>,
|
||||
max: &'a T,
|
||||
state: State,
|
||||
) -> Chart<'a> {
|
||||
let max_color = chart_variant.get_max_color(colors, state);
|
||||
|
||||
Chart::new(dataset)
|
||||
.bg(chart_variant.get_bg_color(colors))
|
||||
.block(
|
||||
Block::default()
|
||||
.style(Style::default().bg(chart_variant.get_bg_color(colors)))
|
||||
.title_alignment(Alignment::Center)
|
||||
.title(Span::styled(
|
||||
format!(" {} {current} ", chart_variant.name()),
|
||||
Style::default()
|
||||
.fg(chart_variant.get_title_color(colors, state))
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(chart_variant.get_border_color(colors))),
|
||||
)
|
||||
.x_axis(Axis::default().bounds([0.00, 60.0]))
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.labels(vec![
|
||||
Span::styled("", Style::default().fg(max_color)),
|
||||
Span::styled(
|
||||
format!("{max}"),
|
||||
Style::default().add_modifier(Modifier::BOLD).fg(max_color),
|
||||
),
|
||||
])
|
||||
.style(Style::new().fg(chart_variant.get_y_axis_color(colors)))
|
||||
// Add 0.01, so that max point is always visible?
|
||||
.bounds([0.0, max.get_value() + 0.01]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Draw the cpu + mem charts
|
||||
pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
||||
if let Some(x) = fd.chart_data.as_ref() {
|
||||
let area = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(CONSTRAINT_50_50)
|
||||
.split(area);
|
||||
|
||||
let cpu_dataset = vec![
|
||||
Dataset::default()
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(colors.chart_cpu.points))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&x.cpu.dataset),
|
||||
];
|
||||
let mem_dataset = vec![
|
||||
Dataset::default()
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(colors.chart_memory.points))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&x.memory.dataset),
|
||||
];
|
||||
|
||||
// let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1));
|
||||
// #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
// let mem_stats = ByteStats::new(mem.0.last().map_or(0, |f| f.1 as u64));
|
||||
let cpu_chart = make_chart(
|
||||
ChartVariant::Cpu,
|
||||
colors,
|
||||
&x.cpu.current,
|
||||
cpu_dataset,
|
||||
&x.cpu.max,
|
||||
x.state,
|
||||
);
|
||||
let mem_chart = make_chart(
|
||||
ChartVariant::Memory,
|
||||
colors,
|
||||
&x.memory.current,
|
||||
mem_dataset,
|
||||
&x.memory.max,
|
||||
x.state,
|
||||
);
|
||||
|
||||
f.render_widget(cpu_chart, area[0]);
|
||||
f.render_widget(mem_chart, area[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::State,
|
||||
config::AppColors,
|
||||
ui::{
|
||||
FrameData,
|
||||
draw_blocks::tests::{COLOR_ORANGE, get_result, insert_all_chart_data, test_setup},
|
||||
},
|
||||
};
|
||||
|
||||
/// CPU and Memory charts used in multiple tests, based on data from above insert_chart_data()
|
||||
const _EXPECTED: [&str; 10] = [
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮",
|
||||
"│10.00%│ • ││100.00 kB│ •• │",
|
||||
"│ │ •• ││ │ •• │",
|
||||
"│ │ ••• ││ │ • • │",
|
||||
"│ │ • • ││ │ • • │",
|
||||
"│ │ • •• ││ │•• •• │",
|
||||
"│ │• • ││ │• • │",
|
||||
"│ │• • ││ │• • │",
|
||||
"│ │ ││ │ │",
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯",
|
||||
];
|
||||
|
||||
// co-ordinates of the dots from the cpu chart
|
||||
const CPU_XY: [(usize, usize); 16] = [
|
||||
(1, 13),
|
||||
(2, 12),
|
||||
(2, 13),
|
||||
(3, 11),
|
||||
(3, 13),
|
||||
(4, 11),
|
||||
(4, 13),
|
||||
(5, 10),
|
||||
(5, 13),
|
||||
(6, 9),
|
||||
(6, 13),
|
||||
(6, 14),
|
||||
(7, 8),
|
||||
(7, 9),
|
||||
(7, 13),
|
||||
(7, 14),
|
||||
];
|
||||
|
||||
// co-ordinates of the dots from the memory chart
|
||||
const MEM_XY: [(usize, usize); 14] = [
|
||||
(1, 55),
|
||||
(2, 54),
|
||||
(2, 55),
|
||||
(3, 54),
|
||||
(3, 55),
|
||||
(4, 53),
|
||||
(4, 55),
|
||||
(5, 52),
|
||||
(5, 53),
|
||||
(5, 56),
|
||||
(6, 52),
|
||||
(6, 56),
|
||||
(7, 51),
|
||||
(7, 56),
|
||||
];
|
||||
|
||||
#[test]
|
||||
/// When status is Running, but not data, charts drawn without dots etc, colours correct
|
||||
fn test_draw_blocks_charts_running_none() {
|
||||
let mut setup = test_setup(80, 10, true, true);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0, 14..=25 | 52..=67) => {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
(1, 1..=6 | 41..=47) => {
|
||||
assert_eq!(result_cell.fg, COLOR_ORANGE);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
(2..=8, 1..=6 | 8..=38 | 49..=78 | 41..=47) | (1, 8..=38 | 49..=78) => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// When status is Running, charts correctly drawn
|
||||
fn test_draw_blocks_charts_running_some() {
|
||||
let mut setup = test_setup(80, 10, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0, 14..=25 | 51..=67) => {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
(1, 1..=6 | 41..=49) => {
|
||||
assert_eq!(result_cell.fg, COLOR_ORANGE);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
xy if CPU_XY.contains(&xy) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
xy if MEM_XY.contains(&xy) => {
|
||||
assert_eq!(result_cell.fg, Color::Cyan);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Whens status paused, some text is now Yellow
|
||||
fn test_draw_blocks_charts_paused() {
|
||||
let mut setup = test_setup(80, 10, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
setup.app_data.lock().containers.items[0].state = State::Paused;
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
//
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0, 14..=25 | 51..=67) | (1, 1..=6 | 41..=49) => {
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
xy if CPU_XY.contains(&xy) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
xy if MEM_XY.contains(&xy) => {
|
||||
assert_eq!(result_cell.fg, Color::Cyan);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// When dead, text is red
|
||||
fn test_draw_blocks_charts_dead() {
|
||||
let mut setup = test_setup(80, 10, true, true);
|
||||
insert_all_chart_data(&setup);
|
||||
setup.app_data.lock().containers.items[0].state = State::Dead;
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0, 14..=25 | 51..=67) | (1, 1..=6 | 41..=49) => {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
xy if CPU_XY.contains(&xy) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
xy if MEM_XY.contains(&xy) => {
|
||||
assert_eq!(result_cell.fg, Color::Cyan);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colos correctly applied to each part of the charts
|
||||
fn test_draw_blocks_charts_custom_colors() {
|
||||
let mut colors = AppColors::new();
|
||||
|
||||
colors.chart_cpu.background = Color::White;
|
||||
colors.chart_cpu.border = Color::Red;
|
||||
colors.chart_cpu.title = Color::Green;
|
||||
colors.chart_cpu.max = Color::Magenta;
|
||||
colors.chart_cpu.points = Color::Black;
|
||||
colors.chart_cpu.y_axis = Color::Blue;
|
||||
|
||||
colors.chart_memory.background = Color::White;
|
||||
colors.chart_memory.border = Color::Red;
|
||||
colors.chart_memory.title = Color::Green;
|
||||
colors.chart_memory.max = Color::Magenta;
|
||||
colors.chart_memory.points = Color::Black;
|
||||
colors.chart_memory.y_axis = Color::Blue;
|
||||
|
||||
let mut setup = test_setup(80, 10, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(0, 0..=13 | 26..=50 | 68..=79) | (9, _) | (1..=8, 0 | 39 | 40 | 79) => {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// title
|
||||
(0, 14..=25 | 51..=67) => {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
}
|
||||
// max label
|
||||
(1, 1..=6 | 41..=49) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
// data points
|
||||
xy if CPU_XY.contains(&xy) | MEM_XY.contains(&xy) => {
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
// y axis
|
||||
(1..=8, 7 | 50) => {
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::SELECT_ARROW;
|
||||
use crate::{
|
||||
app_data::AppData,
|
||||
config::AppColors,
|
||||
ui::{FrameData, GuiState, SelectablePanel},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{List, ListItem, Paragraph},
|
||||
};
|
||||
|
||||
use super::generate_block;
|
||||
|
||||
/// Draw the command panel
|
||||
pub fn draw(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let block = generate_block(area, colors, fd, gui_state, SelectablePanel::Commands)
|
||||
.bg(colors.commands.background);
|
||||
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
|
||||
i.iter()
|
||||
.map(|c| {
|
||||
let lines = Line::from(vec![Span::styled(
|
||||
c.to_string(),
|
||||
Style::default().fg(c.get_color(colors)),
|
||||
)]);
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
if let Some(i) = app_data.lock().get_control_state() {
|
||||
let items = List::new(items)
|
||||
.block(block)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol(SELECT_ARROW);
|
||||
f.render_stateful_widget(items, area, i);
|
||||
} else {
|
||||
let paragraph = Paragraph::new("").block(block).alignment(Alignment::Center);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::ScrollDirection,
|
||||
config::AppColors,
|
||||
tests::gen_container_summary,
|
||||
ui::{
|
||||
FrameData,
|
||||
draw_blocks::tests::{BORDER_CHARS, get_result, test_setup},
|
||||
},
|
||||
};
|
||||
|
||||
// cusomt border colors
|
||||
#[test]
|
||||
/// Test that when DockerCommands are available, they are drawn correctly, dependant on container state
|
||||
/// In this case, no commands are drawn
|
||||
fn test_draw_blocks_commands_none() {
|
||||
let mut setup = test_setup(12, 6, false, false);
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test that when DockerCommands are available, they are drawn correctly, dependant on container state
|
||||
/// In this test, container is running
|
||||
fn test_draw_blocks_commands_some() {
|
||||
let mut setup = test_setup(12, 6, true, true);
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
match (row_index, result_cell_index) {
|
||||
// Borders & delete
|
||||
(0 | 5, _) | (1..=4, 0 | 11) | (4, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
// pause
|
||||
(1, 3..=7) => {
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
// restart
|
||||
(2, 3..=9) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
// stop
|
||||
(3, 3..=6) => {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test that when DockerCommands are available, they are drawn correctly, dependant on container state
|
||||
/// In this test, container is paused
|
||||
fn test_draw_blocks_commands_some_paused() {
|
||||
let mut setup = test_setup(12, 6, true, true);
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Set the container state to paused
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.update_containers(vec![gen_container_summary(1, "paused")]);
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.docker_controls_scroll(&ScrollDirection::Down);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
match (row_index, result_cell_index) {
|
||||
// resume
|
||||
(1, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
// stop
|
||||
(2, 3..=6) => {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// delete
|
||||
(0 | 5, _) | (1..=4, 0 | 11) | (3, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// When control panel is selected, the border is blue, if not then white, selected text is highlighted
|
||||
fn test_draw_blocks_commands_panel_selected_color() {
|
||||
let mut setup = test_setup(12, 6, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
// Unselected, has a grey border
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for result_cell in result_row {
|
||||
if BORDER_CHARS.contains(&result_cell.symbol()) {
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Control panel now selected, should have a blue border
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.selectable_panel_next(&setup.app_data);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
if row_index == 0
|
||||
|| row_index == 5
|
||||
|| result_cell_index == 0
|
||||
|| result_cell_index == 11
|
||||
{
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
if row_index == 1 && result_cell_index > 0 && result_cell_index < 11 {
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
} else {
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colors are rendered correctly
|
||||
fn test_draw_blocks_commands_custom_colors_running() {
|
||||
let mut setup = test_setup(12, 6, true, true);
|
||||
let mut colors = AppColors::new();
|
||||
colors.commands.background = Color::White;
|
||||
colors.commands.pause = Color::Black;
|
||||
colors.commands.restart = Color::Green;
|
||||
colors.commands.stop = Color::Blue;
|
||||
colors.commands.delete = Color::Magenta;
|
||||
colors.commands.resume = Color::Yellow;
|
||||
colors.commands.start = Color::Cyan;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
match (row_index, result_cell_index) {
|
||||
// pause
|
||||
(1, 3..=7) => {
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
// restart
|
||||
(2, 3..=9) => {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
}
|
||||
// stop
|
||||
(3, 3..=6) => {
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
// delete
|
||||
(4, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
/// Custom colors are rendered correctly
|
||||
fn test_draw_blocks_commands_custom_colors_paused() {
|
||||
let mut setup = test_setup(12, 6, true, true);
|
||||
let mut colors = AppColors::new();
|
||||
colors.commands.background = Color::White;
|
||||
colors.commands.pause = Color::Black;
|
||||
colors.commands.restart = Color::Green;
|
||||
colors.commands.stop = Color::Blue;
|
||||
colors.commands.delete = Color::Magenta;
|
||||
colors.commands.resume = Color::Yellow;
|
||||
colors.commands.start = Color::Cyan;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Set the controls state
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.update_containers(vec![gen_container_summary(1, "paused")]);
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.docker_controls_scroll(&ScrollDirection::Down);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
// resume
|
||||
(1, 3..=7) => {
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
// stop
|
||||
(2, 3..=6) => {
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
// delete
|
||||
(3, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,329 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Direction, Layout},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
};
|
||||
|
||||
use super::{CONSTRAINT_BUTTONS, CONSTRAINT_POPUP};
|
||||
use crate::{
|
||||
app_data::ContainerName,
|
||||
config::{AppColors, Keymap},
|
||||
ui::{
|
||||
DeleteButton, GuiState,
|
||||
gui_state::{BoxLocation, Region},
|
||||
},
|
||||
};
|
||||
|
||||
use super::popup;
|
||||
|
||||
/// Draw the delete confirm box in the centre of the screen
|
||||
/// take in container id and container name here?
|
||||
pub fn draw(
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
keymap: &Keymap,
|
||||
name: &ContainerName,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(" Confirm Delete ")
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_delete.background)
|
||||
.fg(colors.popup_delete.text),
|
||||
)
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let confirm = Line::from(vec![
|
||||
Span::from("Are you sure you want to delete container: "),
|
||||
Span::styled(
|
||||
name.get(),
|
||||
Style::default()
|
||||
.fg(colors.popup_delete.text_highlight)
|
||||
.bg(colors.popup_delete.background)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]);
|
||||
|
||||
let yes_text = if keymap.delete_confirm == Keymap::new().delete_confirm {
|
||||
"( y ) yes".to_owned()
|
||||
} else if let Some(secondary) = keymap.delete_confirm.1 {
|
||||
format!("( {} | {} ) yes", keymap.delete_confirm.0, secondary)
|
||||
} else {
|
||||
format!("( {} ) yes", keymap.delete_confirm.0)
|
||||
};
|
||||
|
||||
let no_text = if keymap.delete_deny == Keymap::new().delete_deny {
|
||||
"( n ) no".to_owned()
|
||||
} else if let Some(secondary) = keymap.delete_deny.1 {
|
||||
format!("( {} | {} ) no", keymap.delete_deny.0, secondary)
|
||||
} else {
|
||||
format!("( {} ) no", keymap.delete_deny.0)
|
||||
};
|
||||
|
||||
// Find the maximum line width & height, and add some padding
|
||||
let max_line_width = u16::try_from(confirm.width()).unwrap_or(64) + 12;
|
||||
let lines = 8;
|
||||
|
||||
let confirm_para = Paragraph::new(confirm).alignment(Alignment::Center);
|
||||
|
||||
let button_block = || {
|
||||
Block::default()
|
||||
.border_type(BorderType::Rounded)
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(colors.popup_delete.background))
|
||||
};
|
||||
|
||||
let yes_para = Paragraph::new(yes_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(button_block());
|
||||
|
||||
let no_para = Paragraph::new(no_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(button_block());
|
||||
|
||||
let area = popup::draw(
|
||||
lines,
|
||||
max_line_width.into(),
|
||||
f.area(),
|
||||
BoxLocation::MiddleCentre,
|
||||
);
|
||||
|
||||
let split_popup = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(CONSTRAINT_POPUP)
|
||||
.split(area);
|
||||
|
||||
let split_buttons = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(CONSTRAINT_BUTTONS)
|
||||
.split(split_popup[3]);
|
||||
|
||||
let no_area = split_buttons[1];
|
||||
let yes_area = split_buttons[3];
|
||||
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(block, area);
|
||||
f.render_widget(confirm_para, split_popup[1]);
|
||||
f.render_widget(no_para, no_area);
|
||||
f.render_widget(yes_para, yes_area);
|
||||
// Insert button areas into region map, so can interact with them on click
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Delete(DeleteButton::Cancel), no_area);
|
||||
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Delete(DeleteButton::Confirm), yes_area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crossterm::event::KeyCode;
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::ContainerName,
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::tests::{get_result, test_setup},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Delete container popup is drawn correctly
|
||||
fn test_draw_blocks_delete() {
|
||||
let mut setup = test_setup(82, 10, true, true);
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
keymap,
|
||||
&ContainerName::from("container_1"),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 9, _) | (1..=8, 0..=7 | 74..=81) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
(3, 57..=67) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Delete container popup is drawn correctly
|
||||
fn test_draw_blocks_delete_long_name() {
|
||||
let mut setup = test_setup(106, 10, true, true);
|
||||
let name = ContainerName::from("container_1_container_1_container_1");
|
||||
setup.app_data.lock().containers.items[0].name = name.clone();
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(colors, f, &setup.gui_state, keymap, &name);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 9, _) | (1..=8, 0..=7 | 98..=106) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
(3, 57..=91) => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colors applied correctly to delete popup
|
||||
fn test_draw_blocks_delete_custom_colors() {
|
||||
let mut setup = test_setup(82, 10, true, true);
|
||||
let mut colors = AppColors::new();
|
||||
colors.popup_delete.background = Color::Black;
|
||||
colors.popup_delete.text = Color::Yellow;
|
||||
colors.popup_delete.text_highlight = Color::Green;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
&ContainerName::from("container_1"),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 9, _) | (1..=8, 0..=7 | 74..=81) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
(3, 57..=67) => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap, with multiple definitions for each button, applied correctly to delete popup
|
||||
fn test_draw_blocks_delete_custom_keymap_one_definition() {
|
||||
let mut setup = test_setup(82, 10, true, true);
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.delete_confirm = (KeyCode::F(10), None);
|
||||
keymap.delete_deny = (KeyCode::End, None);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
&ContainerName::from("container_1"),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
#[test]
|
||||
/// Custom keymap, with multiple definitions for each button, applied correctly to delete popup
|
||||
fn test_draw_blocks_delete_custom_keymap_two_definition() {
|
||||
let mut setup = test_setup(82, 10, true, true);
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.delete_confirm = (KeyCode::F(10), Some(KeyCode::Char('L')));
|
||||
keymap.delete_deny = (KeyCode::End, Some(KeyCode::Up));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
&ContainerName::from("container_1"),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
#[test]
|
||||
/// Custom keymap, with multiple definitions for each button, applied correctly to delete popup
|
||||
fn test_draw_blocks_delete_custom_keymap_one_two_definition() {
|
||||
let mut setup = test_setup(82, 10, true, true);
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.delete_confirm = (KeyCode::F(10), None);
|
||||
keymap.delete_deny = (KeyCode::End, Some(KeyCode::Up));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
&ContainerName::from("container_1"),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Alignment,
|
||||
style::Style,
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
};
|
||||
|
||||
use super::{NAME, VERSION, max_line_width};
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
config::{AppColors, Keymap},
|
||||
ui::gui_state::BoxLocation,
|
||||
};
|
||||
|
||||
use super::popup;
|
||||
|
||||
const SUFFIX_CLEAR: &str = "clear error";
|
||||
const SUFFIX_QUIT: &str = "quit oxker";
|
||||
|
||||
/// Draw an error popup over whole screen
|
||||
pub fn draw(
|
||||
colors: AppColors,
|
||||
error: &AppError,
|
||||
f: &mut Frame,
|
||||
host: Option<String>,
|
||||
keymap: &Keymap,
|
||||
seconds: Option<u8>,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(" Error ")
|
||||
.border_type(BorderType::Rounded)
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let mut text = format!("\n{error}");
|
||||
|
||||
if error == &AppError::DockerConnect {
|
||||
let s = if let Some(host) = host {
|
||||
format!(" @ \"{host}\"")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
text.push_str(&format!(
|
||||
"{}\n\n {}::v{} closing in {:02} seconds",
|
||||
s,
|
||||
NAME,
|
||||
VERSION,
|
||||
seconds.unwrap_or(5),
|
||||
))
|
||||
} else {
|
||||
let clear_text = if keymap.clear == Keymap::new().clear {
|
||||
format!("( {} ) {SUFFIX_CLEAR}", keymap.clear.0)
|
||||
} else if let Some(secondary) = keymap.clear.1 {
|
||||
format!(" ( {} | {secondary} ) {SUFFIX_CLEAR}", keymap.clear.0)
|
||||
} else {
|
||||
format!(" ( {} ) {SUFFIX_CLEAR}", keymap.clear.0)
|
||||
};
|
||||
text.push_str(&format!("\n\n{clear_text}"));
|
||||
}
|
||||
|
||||
let quit_text = if keymap.quit == Keymap::new().quit {
|
||||
format!("( {} ) {SUFFIX_QUIT}", keymap.quit.0)
|
||||
} else if let Some(secondary) = keymap.quit.1 {
|
||||
format!(" ( {} | {secondary} ) {SUFFIX_QUIT}", keymap.quit.0)
|
||||
} else {
|
||||
format!(" ( {} ) {SUFFIX_QUIT}", keymap.quit.0)
|
||||
};
|
||||
text.push_str(&format!("\n\n{quit_text}"));
|
||||
|
||||
// Find the maximum line width & height
|
||||
let padded_width = max_line_width(&text) + 8;
|
||||
|
||||
let line_count = text.lines().count();
|
||||
let padded_height = if line_count % 2 == 0 {
|
||||
line_count + 3
|
||||
} else {
|
||||
line_count + 2
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_error.background)
|
||||
.fg(colors.popup_error.text),
|
||||
)
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let area = popup::draw(
|
||||
padded_height,
|
||||
padded_width,
|
||||
f.area(),
|
||||
BoxLocation::MiddleCentre,
|
||||
);
|
||||
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::tests::{get_result, test_setup},
|
||||
};
|
||||
use crossterm::event::KeyCode;
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::Color;
|
||||
|
||||
#[test]
|
||||
/// Test that the error popup is centered, red background, white border, white text, and displays the correct text
|
||||
fn test_draw_blocks_error_docker_connect_error() {
|
||||
let mut setup = test_setup(50, 11, true, true);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
&AppError::DockerConnect,
|
||||
f,
|
||||
None,
|
||||
&Keymap::new(),
|
||||
Some(4),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 10, _) | (_, 0 | 49) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Red);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test that the error popup is centered, red background, white border, white text, and displays the correct text with the custom docker host address
|
||||
fn test_draw_blocks_error_docker_connect_error_custom_host() {
|
||||
let mut setup = test_setup(60, 11, true, true);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
&AppError::DockerConnect,
|
||||
f,
|
||||
Some("/test/host.sock".to_owned()),
|
||||
&Keymap::new(),
|
||||
Some(4),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 10, _) | (_, 0 | 59) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Red);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test that the clearable error popup is centered, red background, white border, white text, and displays the correct text
|
||||
fn test_draw_blocks_error_clearable_error() {
|
||||
let mut setup = test_setup(39, 11, true, true);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
&AppError::DockerExec,
|
||||
f,
|
||||
None,
|
||||
&Keymap::new(),
|
||||
Some(4),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 10, _) | (1..=9, 0 | 38) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Red);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colors applied to the error popup correctly
|
||||
fn test_draw_blocks_error_custom_colors() {
|
||||
let mut setup = test_setup(39, 11, true, true);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.popup_error.background = Color::Yellow;
|
||||
colors.popup_error.text = Color::Black;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
&AppError::DockerExec,
|
||||
f,
|
||||
None,
|
||||
&Keymap::new(),
|
||||
Some(4),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 10, _) | (1..=9, 0 | 38) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Yellow);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap applied correctly
|
||||
fn test_draw_blocks_error_custom_keymap() {
|
||||
let mut setup = test_setup(39, 11, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.clear = (KeyCode::BackTab, None);
|
||||
keymap.quit = (KeyCode::F(4), None);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
&AppError::DockerExec,
|
||||
f,
|
||||
None,
|
||||
&keymap,
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
#[test]
|
||||
/// Custom keymap applied with two definitions for each option
|
||||
fn test_draw_blocks_error_custom_keymap_two_definitions() {
|
||||
let mut setup = test_setup(39, 11, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.clear = (KeyCode::BackTab, Some(KeyCode::Char('m')));
|
||||
keymap.quit = (KeyCode::F(4), Some(KeyCode::End));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
&AppError::DockerExec,
|
||||
f,
|
||||
None,
|
||||
&keymap,
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap applied correctly, with 1 definition for the first option, and 2 definitions for the other
|
||||
fn test_draw_blocks_error_custom_keymap_one_two_definitions() {
|
||||
let mut setup = test_setup(39, 11, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.quit = (KeyCode::F(4), Some(KeyCode::End));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
&AppError::DockerExec,
|
||||
f,
|
||||
None,
|
||||
&keymap,
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
style::{Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_data::FilterBy,
|
||||
config::AppColors,
|
||||
ui::{
|
||||
FrameData,
|
||||
draw_blocks::{LEFT_ARROW, RIGHT_ARROW},
|
||||
},
|
||||
};
|
||||
|
||||
/// Create the filter_by by spans, coloured dependant on which one is selected
|
||||
fn filter_by_spans(colors: AppColors, fd: &'_ FrameData) -> [Span<'_>; 4] {
|
||||
let selected = Style::default()
|
||||
.bg(colors.filter.selected_filter_background)
|
||||
.fg(colors.filter.selected_filter_text);
|
||||
let not_selected = Style::default()
|
||||
.bg(colors.filter.background)
|
||||
.fg(colors.filter.text);
|
||||
|
||||
let name = [" Name ", " Image ", " Status ", " All "];
|
||||
|
||||
let mut filter_spans = [
|
||||
Span::styled(name[0], not_selected),
|
||||
Span::styled(name[1], not_selected),
|
||||
Span::styled(name[2], not_selected),
|
||||
Span::styled(name[3], not_selected),
|
||||
];
|
||||
|
||||
match fd.filter_by {
|
||||
FilterBy::Name => filter_spans[0] = Span::styled(name[0], selected),
|
||||
FilterBy::Image => filter_spans[1] = Span::styled(name[1], selected),
|
||||
FilterBy::Status => filter_spans[2] = Span::styled(name[2], selected),
|
||||
FilterBy::All => filter_spans[3] = Span::styled(name[3], selected),
|
||||
}
|
||||
filter_spans
|
||||
}
|
||||
|
||||
/// Draw the filter bar
|
||||
pub fn draw(area: Rect, colors: AppColors, frame: &mut Frame, fd: &FrameData) {
|
||||
let style_but = Style::default()
|
||||
.fg(colors.filter.selected_filter_text)
|
||||
.bg(colors.filter.highlight);
|
||||
let style_desc = Style::default()
|
||||
.fg(colors.filter.text)
|
||||
.bg(colors.filter.background);
|
||||
|
||||
let mut line = vec![
|
||||
Span::styled(" Esc ", style_but),
|
||||
Span::styled(" clear ", style_desc),
|
||||
Span::styled(format!(" {LEFT_ARROW} by {RIGHT_ARROW} "), style_but),
|
||||
Span::from(" "),
|
||||
];
|
||||
line.extend_from_slice(&filter_by_spans(colors, fd));
|
||||
line.extend_from_slice(&[
|
||||
Span::styled(
|
||||
" filter term: ",
|
||||
Style::default()
|
||||
.fg(colors.filter.highlight)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
fd.filter_term
|
||||
.as_ref()
|
||||
.map_or(String::new(), std::clone::Clone::clone),
|
||||
Style::default().fg(colors.filter.text),
|
||||
),
|
||||
]);
|
||||
frame.render_widget(Line::from(line).bg(colors.filter.background), area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
ui::{
|
||||
FrameData,
|
||||
draw_blocks::tests::{get_result, test_setup},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Filter row is drawn correctly & colors are correct
|
||||
/// Colours change when filter_by option is changed
|
||||
fn test_draw_blocks_filter_row() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::Filter);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &setup.fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 | 12..=19 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 27..=46 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
21..=26 => {
|
||||
assert_eq!(result_cell.bg, Color::Gray);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
47..=60 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
/// Colours change when filter_by option is changed
|
||||
fn test_draw_blocks_filter_row_text() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::Filter);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &setup.fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Test when char added to search term
|
||||
setup.app_data.lock().filter_term_push('c');
|
||||
setup.app_data.lock().filter_term_push('d');
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 | 12..=19 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 27..=46 | 61..=62 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
21..=26 => {
|
||||
assert_eq!(result_cell.bg, Color::Gray);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
47..=60 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Colours change when filter_by option is changed
|
||||
fn test_draw_blocks_filter_row_filter_by() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::Filter);
|
||||
setup.app_data.lock().filter_by_next();
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &setup.fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 | 12..=19 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 27..=46 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
21..=26 => {
|
||||
assert_eq!(result_cell.bg, Color::Gray);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
47..=60 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Make sure custom colors are applied
|
||||
fn test_draw_blocks_filter_row_custom_colors() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::Filter);
|
||||
|
||||
setup.app_data.lock().filter_term_push('c');
|
||||
setup.app_data.lock().filter_term_push('d');
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.filter.background = Color::White;
|
||||
colors.filter.highlight = Color::Blue;
|
||||
colors.filter.selected_filter_background = Color::Red;
|
||||
colors.filter.selected_filter_text = Color::Yellow;
|
||||
colors.filter.text = Color::Magenta;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 | 12..=19 => {
|
||||
assert_eq!(result_cell.bg, Color::Blue);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
5..=11 | 27..=46 | 61..=62 => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
21..=26 => {
|
||||
assert_eq!(result_cell.bg, Color::Red);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
47..=60 => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,673 +0,0 @@
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Paragraph},
|
||||
};
|
||||
|
||||
use super::{CONSTRAINT_100, MARGIN};
|
||||
use crate::{
|
||||
app_data::{Header, SortedOrder},
|
||||
config::{AppColors, Keymap},
|
||||
ui::{FrameData, GuiState, Status, gui_state::Region},
|
||||
};
|
||||
|
||||
/// Generate a header paragraph with it's width
|
||||
fn gen_header<'a>(
|
||||
colors: AppColors,
|
||||
fd: &FrameData,
|
||||
header: Header,
|
||||
width: usize,
|
||||
) -> (Paragraph<'a>, u16) {
|
||||
let block = gen_header_block(colors, fd, header);
|
||||
|
||||
let text = format!(
|
||||
"{x:<width$}{MARGIN}",
|
||||
x = format!("{header}{ic}", ic = block.1),
|
||||
);
|
||||
let count = u16::try_from(text.chars().count()).unwrap_or_default();
|
||||
let status = Paragraph::new(text)
|
||||
.style(gen_style(None, block.0))
|
||||
.alignment(Alignment::Left);
|
||||
(status, count)
|
||||
}
|
||||
|
||||
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
|
||||
fn gen_header_block<'a>(colors: AppColors, fd: &FrameData, header: Header) -> (Color, &'a str) {
|
||||
let mut color = colors.headers_bar.text;
|
||||
let mut suffix = "";
|
||||
if let Some((a, b)) = &fd.sorted_by
|
||||
&& &header == a
|
||||
{
|
||||
match b {
|
||||
SortedOrder::Asc => suffix = " ▲",
|
||||
SortedOrder::Desc => suffix = " ▼",
|
||||
}
|
||||
color = colors.headers_bar.text_selected;
|
||||
}
|
||||
|
||||
(color, suffix)
|
||||
}
|
||||
|
||||
fn gen_style(bg: Option<Color>, fg: Color) -> Style {
|
||||
bg.map_or_else(
|
||||
|| Style::default().fg(fg),
|
||||
|bg| Style::default().bg(bg).fg(fg),
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate the text to display on the show help section, as can change with a custom keymap
|
||||
fn gen_help_text(fd: &FrameData, keymap: &Keymap) -> String {
|
||||
let suffix = if fd.status.contains(&Status::Help) {
|
||||
"exit"
|
||||
} else {
|
||||
"show"
|
||||
};
|
||||
|
||||
if keymap.toggle_help == Keymap::new().toggle_help {
|
||||
format!("( h ) {suffix} help{MARGIN}")
|
||||
} else if let Some(secondary) = keymap.toggle_help.1 {
|
||||
format!(
|
||||
" ( {} | {secondary} ) {suffix} help{MARGIN}",
|
||||
keymap.toggle_help.0
|
||||
)
|
||||
} else {
|
||||
format!(" ( {} ) {suffix} help{MARGIN}", keymap.toggle_help.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the show/hide help section
|
||||
fn draw_help(
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
help_text: String,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
split_bar: &Rc<[Rect]>,
|
||||
) {
|
||||
let help_text_color = if fd.status.contains(&Status::Help) {
|
||||
colors.headers_bar.text
|
||||
} else {
|
||||
colors.headers_bar.text_selected
|
||||
};
|
||||
|
||||
let help_paragraph = Paragraph::new(help_text)
|
||||
.style(gen_style(None, help_text_color))
|
||||
.alignment(Alignment::Right);
|
||||
|
||||
// If no containers, don't display the headers, could maybe do this first?
|
||||
let help_index = if fd.has_containers { 2 } else { 0 };
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::HelpPanel, split_bar[help_index]);
|
||||
f.render_widget(help_paragraph, split_bar[help_index]);
|
||||
}
|
||||
|
||||
// Draw loading icon, or not, and a prefix with a single space
|
||||
fn draw_loading_spinner(colors: AppColors, f: &mut Frame, fd: &FrameData, rect: Rect) {
|
||||
let loading_paragraph = Paragraph::new(format!("{:>2}", fd.loading_icon))
|
||||
.style(gen_style(None, colors.headers_bar.loading_spinner))
|
||||
.alignment(Alignment::Left);
|
||||
f.render_widget(loading_paragraph, rect);
|
||||
}
|
||||
|
||||
/// Draw the sortable column headers (name/state/status etc)
|
||||
fn draw_columns(
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
split_bar: &Rc<[Rect]>,
|
||||
) {
|
||||
if fd.has_containers {
|
||||
let header_section_width = split_bar[1].width;
|
||||
|
||||
let mut counter = 0;
|
||||
|
||||
// Meta data to iterate over to create blocks with correct widths
|
||||
let header_meta = [
|
||||
(Header::Name, fd.columns.name.1),
|
||||
(Header::State, fd.columns.state.1),
|
||||
(Header::Status, fd.columns.status.1),
|
||||
(Header::Cpu, fd.columns.cpu.1),
|
||||
(Header::Memory, fd.columns.mem.1 + fd.columns.mem.2 + 3),
|
||||
(Header::Id, fd.columns.id.1),
|
||||
(Header::Image, fd.columns.image.1),
|
||||
(Header::Rx, fd.columns.net_rx.1),
|
||||
(Header::Tx, fd.columns.net_tx.1),
|
||||
];
|
||||
|
||||
// Only show a header if the header cumulative header width is less than the header section width
|
||||
let header_data = header_meta
|
||||
.into_iter()
|
||||
.filter_map(|(header, width)| {
|
||||
let header_block = gen_header(colors, fd, header, usize::from(width));
|
||||
counter += header_block.1;
|
||||
if counter <= header_section_width {
|
||||
Some((header_block.0, header, Constraint::Max(header_block.1)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let headers_section = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(header_data.iter().map(|i| i.2))
|
||||
.split(split_bar[1]);
|
||||
|
||||
for (index, (paragraph, header, _)) in header_data.into_iter().enumerate() {
|
||||
let rect = headers_section[index];
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Header(header), rect);
|
||||
f.render_widget(paragraph, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw heading bar at top of program, always visible
|
||||
pub fn draw(
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
keymap: &Keymap,
|
||||
) {
|
||||
let gen_style = |bg: Option<Color>, fg: Color| {
|
||||
bg.map_or_else(
|
||||
|| Style::default().fg(fg),
|
||||
|bg| Style::default().bg(bg).fg(fg),
|
||||
)
|
||||
};
|
||||
|
||||
f.render_widget(
|
||||
Block::default().style(gen_style(Some(colors.headers_bar.background), Color::Reset)),
|
||||
area,
|
||||
);
|
||||
|
||||
let help_text = gen_help_text(fd, keymap);
|
||||
let help_width = help_text.chars().count();
|
||||
|
||||
let column_width = usize::from(area.width).saturating_sub(help_width);
|
||||
let column_width = if column_width > 0 { column_width } else { 1 };
|
||||
|
||||
let split_bar = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(if fd.has_containers {
|
||||
vec![
|
||||
Constraint::Max(4),
|
||||
Constraint::Max(column_width.try_into().unwrap_or_default()),
|
||||
Constraint::Max(help_width.try_into().unwrap_or_default()),
|
||||
]
|
||||
} else {
|
||||
CONSTRAINT_100.to_vec()
|
||||
})
|
||||
.split(area);
|
||||
|
||||
draw_loading_spinner(colors, f, fd, split_bar[0]);
|
||||
draw_columns(colors, f, fd, gui_state, &split_bar);
|
||||
draw_help(colors, f, fd, help_text, gui_state, &split_bar);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::Color;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_data::{Header, SortedOrder, StatefulList},
|
||||
config::{AppColors, Keymap},
|
||||
ui::{
|
||||
FrameData, Status,
|
||||
draw_blocks::tests::{TuiTestSetup, get_result, test_setup},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Heading back only has show/exit help when no containers, correctly coloured
|
||||
fn test_draw_blocks_headers_no_containers_show_help() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
setup.app_data.lock().containers = StatefulList::new(vec![]);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for result_cell in result_row {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Gray,);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Heading back only has show/exit help when no containers, correctly coloured
|
||||
fn test_draw_blocks_headers_no_containers_exit_help() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
setup.app_data.lock().containers = StatefulList::new(vec![]);
|
||||
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
fd.status.insert(Status::Help);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for result_cell in result_row {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Show all headings when containers present, colors valid
|
||||
fn test_draw_blocks_headers_some_containers() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(
|
||||
result_cell.fg,
|
||||
match result_cell_index {
|
||||
0..=3 => Color::White,
|
||||
4..=111 => Color::Black,
|
||||
112..=121 => Color::Reset,
|
||||
_ => Color::Gray,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Only show the headings that fit the reduced-in-size header section
|
||||
fn test_draw_blocks_headers_some_containers_reduced_width() {
|
||||
let mut setup = test_setup(80, 1, true, true);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(
|
||||
result_cell.fg,
|
||||
match result_cell_index {
|
||||
0..=3 => Color::White,
|
||||
4..=50 => Color::Black,
|
||||
51..=61 => Color::Reset,
|
||||
_ => Color::Gray,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Show animation
|
||||
fn test_draw_blocks_headers_animation() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
let uuid = Uuid::new_v4();
|
||||
setup.gui_state.lock().next_loading(uuid);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(
|
||||
result_cell.fg,
|
||||
match result_cell_index {
|
||||
0..=3 => Color::White,
|
||||
4..=111 => Color::Black,
|
||||
122..=140 => Color::Gray,
|
||||
_ => Color::Reset,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colors are applied correctly
|
||||
fn test_draw_blocks_headers_custom_colors() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
let uuid = Uuid::new_v4();
|
||||
setup.gui_state.lock().next_loading(uuid);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.headers_bar.background = Color::Black;
|
||||
colors.headers_bar.loading_spinner = Color::Green;
|
||||
colors.headers_bar.text = Color::Blue;
|
||||
colors.headers_bar.text_selected = Color::Yellow;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd, &setup.gui_state, keymap);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(
|
||||
result_cell.fg,
|
||||
match result_cell_index {
|
||||
0..=3 => Color::Green,
|
||||
4..=111 => Color::Blue,
|
||||
122..=140 => Color::Yellow,
|
||||
_ => Color::Reset,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap for help panel is correctly display, with one definitions
|
||||
fn test_draw_blocks_headers_custom_keymap_one_definition() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.toggle_help = (KeyCode::Char('T'), None);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap for help panel is correctly display, two definitions
|
||||
fn test_draw_blocks_headers_custom_keymap_two_definitions() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.toggle_help = (KeyCode::Char('T'), Some(KeyCode::Tab));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
fn check_color(setup: &TuiTestSetup, range: RangeInclusive<usize>) {
|
||||
for (_, result_row) in get_result(setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(
|
||||
result_cell.fg,
|
||||
match result_cell_index {
|
||||
0..=3 => Color::White,
|
||||
122..=139 => Color::Gray,
|
||||
// given range | help section
|
||||
x if range.contains(&x) => Color::Gray,
|
||||
112..=121 => Color::Reset,
|
||||
_ => Color::Black,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// As a macro - headers test, check for asc/desc icon and colors
|
||||
macro_rules! test_draw_blocks_headers_sort {
|
||||
($name:ident, $header:expr, $order:expr, $color_range:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
fd.sorted_by = Some(($header, $order));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
check_color(&setup, $color_range);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_name_asc,
|
||||
Header::Name,
|
||||
SortedOrder::Asc,
|
||||
1..=17
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_name_desc,
|
||||
Header::Name,
|
||||
SortedOrder::Desc,
|
||||
1..=17
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_state_asc,
|
||||
Header::State,
|
||||
SortedOrder::Asc,
|
||||
18..=29
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_state_desc,
|
||||
Header::State,
|
||||
SortedOrder::Desc,
|
||||
18..=29
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_status_asc,
|
||||
Header::Status,
|
||||
SortedOrder::Asc,
|
||||
30..=41
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_status_desc,
|
||||
Header::Status,
|
||||
SortedOrder::Desc,
|
||||
30..=41
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_cpu_asc,
|
||||
Header::Cpu,
|
||||
SortedOrder::Asc,
|
||||
42..=50
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_cpu_desc,
|
||||
Header::Cpu,
|
||||
SortedOrder::Desc,
|
||||
42..=50
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_memory_asc,
|
||||
Header::Memory,
|
||||
SortedOrder::Asc,
|
||||
51..=70
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_memory_desc,
|
||||
Header::Memory,
|
||||
SortedOrder::Desc,
|
||||
51..=70
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_id_asc,
|
||||
Header::Id,
|
||||
SortedOrder::Asc,
|
||||
71..=81
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_id_desc,
|
||||
Header::Id,
|
||||
SortedOrder::Desc,
|
||||
71..=81
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_image_asc,
|
||||
Header::Image,
|
||||
SortedOrder::Asc,
|
||||
82..=91
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_image_desc,
|
||||
Header::Image,
|
||||
SortedOrder::Desc,
|
||||
82..=91
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_rx_asc,
|
||||
Header::Rx,
|
||||
SortedOrder::Asc,
|
||||
92..=101
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_rx_desc,
|
||||
Header::Rx,
|
||||
SortedOrder::Desc,
|
||||
92..=101
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_tx_asc,
|
||||
Header::Tx,
|
||||
SortedOrder::Asc,
|
||||
102..=111
|
||||
);
|
||||
|
||||
test_draw_blocks_headers_sort!(
|
||||
test_draw_blocks_headers_sort_containers_tx_desc,
|
||||
Header::Tx,
|
||||
SortedOrder::Desc,
|
||||
102..=111
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,136 +0,0 @@
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Alignment,
|
||||
style::Style,
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
ui::{GuiState, gui_state::BoxLocation},
|
||||
};
|
||||
|
||||
use super::{max_line_width, popup};
|
||||
|
||||
/// Draw info box in one of the 9 BoxLocations
|
||||
// TODO is this broken - I don't think so
|
||||
pub fn draw(
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
instant: &Instant,
|
||||
msg: String,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title("")
|
||||
.title_alignment(Alignment::Center)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_info.background)
|
||||
.fg(colors.popup_info.text),
|
||||
)
|
||||
.borders(Borders::NONE);
|
||||
|
||||
let max_line_width = max_line_width(&msg) + 8;
|
||||
let lines = msg.lines().count() + 2;
|
||||
|
||||
let paragraph = Paragraph::new(msg)
|
||||
.block(block)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_info.background)
|
||||
.fg(colors.popup_info.text),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let area = popup::draw(lines, max_line_width, f.area(), BoxLocation::BottomRight);
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(paragraph, area);
|
||||
if instant.elapsed().as_millis() > 4000 {
|
||||
gui_state.lock().reset_info_box();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::Color;
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
ui::draw_blocks::tests::{get_result, test_setup},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Info box drawn in bottom right
|
||||
fn test_draw_blocks_info() {
|
||||
let mut setup = test_setup(45, 9, true, true);
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&std::time::Instant::now(),
|
||||
"test".to_owned(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
let (bg, fg) = match (row_index, result_cell_index) {
|
||||
(6..=8, 32..=44) => (Color::Blue, Color::White),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
assert_eq!(result_cell.bg, bg);
|
||||
assert_eq!(result_cell.fg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Info box drawn in bottom right with custom colors applied
|
||||
fn test_draw_blocks_info_custom_color() {
|
||||
let mut setup = test_setup(45, 9, true, true);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.popup_info.background = Color::Red;
|
||||
colors.popup_info.text = Color::Black;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&std::time::Instant::now(),
|
||||
"test".to_owned(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
let (bg, fg) = match (row_index, result_cell_index) {
|
||||
(6..=8, 32..=44) => (Color::Red, Color::Black),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
assert_eq!(result_cell.bg, bg);
|
||||
assert_eq!(result_cell.fg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,777 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_data::InspectData,
|
||||
config::{AppColors, Keymap},
|
||||
ui::{
|
||||
GuiState,
|
||||
draw_blocks::{DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW},
|
||||
gui_state::ScrollOffset,
|
||||
},
|
||||
};
|
||||
|
||||
/// Create a bordered block with a title.
|
||||
fn title_block<'a>(upper_title: &'a str, lower_title: &'a str, colors: &AppColors) -> Block<'a> {
|
||||
Block::default()
|
||||
.borders(Borders::all())
|
||||
.border_type(ratatui::widgets::BorderType::Rounded)
|
||||
.border_style(Style::default().fg(colors.borders.selected))
|
||||
.title(upper_title.bold().into_centered_line())
|
||||
.title_bottom(lower_title.bold().into_centered_line())
|
||||
}
|
||||
|
||||
/// Create the upper title, with container name, id, and keymap to clear
|
||||
fn generate_upper_title(data: &InspectData, keymap: &Keymap) -> String {
|
||||
let mut output = String::from(" inspecting: ");
|
||||
let name = if data.name.starts_with("/") {
|
||||
data.name.replacen('/', "", 1)
|
||||
} else {
|
||||
data.name.clone()
|
||||
};
|
||||
|
||||
output.push_str(&format!("{} {} ", name, data.id.get_short()));
|
||||
let mut inspect_key = keymap.inspect.0.to_string();
|
||||
if let Some(x) = keymap.inspect.1 {
|
||||
inspect_key.push_str(&format!(" or {x}"));
|
||||
}
|
||||
let mut clear_key = keymap.clear.0.to_string();
|
||||
if let Some(x) = keymap.clear.1 {
|
||||
clear_key.push_str(&format!(" or {x}"));
|
||||
}
|
||||
output.push_str(&format!(" - {clear_key} or {inspect_key} to exit"));
|
||||
output.push(' ');
|
||||
output
|
||||
}
|
||||
|
||||
/// Generate the lower title, with the current scroll and the scrolling limits
|
||||
fn generate_lower_title(length: usize, width: usize, offset: ScrollOffset) -> String {
|
||||
let length_width = length
|
||||
.to_string()
|
||||
.chars()
|
||||
.count()
|
||||
.max(offset.y.to_string().chars().count());
|
||||
let width_width = width
|
||||
.to_string()
|
||||
.chars()
|
||||
.count()
|
||||
.max(offset.x.to_string().chars().count());
|
||||
|
||||
let left_arrow = if offset.x == 0 { " " } else { LEFT_ARROW };
|
||||
let right_arrow = if offset.x == width { " " } else { RIGHT_ARROW };
|
||||
let up_arrow = if offset.y == 0 { " " } else { UP_ARROW };
|
||||
let down_arrow = if offset.y == length { " " } else { DOWN_ARROW };
|
||||
|
||||
format!(
|
||||
" {up_arrow} {:>length_width$}/{:>length_width$} {down_arrow} {left_arrow} {:>width_width$}/{:>width_width$} {right_arrow} ",
|
||||
offset.y, length, offset.x, width
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate the Lines, remove lines & chars based on the offset and viewport
|
||||
fn gen_lines<'a>(data_as_str: &'a str, offset: &ScrollOffset, rect: &Rect) -> Vec<Line<'a>> {
|
||||
let first_line_index = offset.y.max(0);
|
||||
let first_char_index = offset.x.max(0);
|
||||
let last_char_index = usize::from(rect.width.saturating_sub(2));
|
||||
let take_lines = usize::from(rect.height);
|
||||
//todo see if log scrolling does this - What?
|
||||
|
||||
data_as_str
|
||||
.lines()
|
||||
.skip(first_line_index)
|
||||
.take(take_lines)
|
||||
.map(|line| {
|
||||
Line::from(
|
||||
line.chars()
|
||||
.skip(first_char_index)
|
||||
.take(last_char_index)
|
||||
.collect::<String>(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Draw the InspectContainer widget to the entire screen
|
||||
pub fn draw(
|
||||
f: &mut Frame,
|
||||
colors: AppColors,
|
||||
data: InspectData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
keymap: &Keymap,
|
||||
) {
|
||||
let rect = f.area();
|
||||
let offset = gui_state.lock().get_inspect_offset();
|
||||
// +2 to account for the border
|
||||
let height = data
|
||||
.height
|
||||
.saturating_sub(usize::from(rect.height))
|
||||
.saturating_add(2);
|
||||
let width = data
|
||||
.width
|
||||
.saturating_sub(usize::from(rect.width))
|
||||
.saturating_add(2);
|
||||
let upper_title = generate_upper_title(&data, keymap);
|
||||
let lower_title = generate_lower_title(height, width, offset);
|
||||
|
||||
gui_state.lock().set_inspect_offset_max(ScrollOffset {
|
||||
x: width,
|
||||
y: height,
|
||||
});
|
||||
|
||||
let paragraph = Paragraph::new(gen_lines(&data.as_string, &offset, &rect))
|
||||
.block(title_block(&upper_title, &lower_title, &colors))
|
||||
.gray()
|
||||
.left_aligned()
|
||||
.wrap(Wrap { trim: false });
|
||||
f.render_widget(paragraph, rect);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use std::{collections::HashMap, sync::LazyLock};
|
||||
|
||||
use crate::{
|
||||
app_data::InspectData,
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::tests::{get_result, test_setup},
|
||||
};
|
||||
use bollard::secret::{
|
||||
ContainerConfig, ContainerInspectResponse, ContainerState, ContainerStateStatusEnum,
|
||||
DriverData, EndpointSettings, HostConfig, HostConfigLogConfig, MountPoint,
|
||||
MountPointTypeEnum, NetworkSettings, RestartPolicy, RestartPolicyNameEnum,
|
||||
};
|
||||
use crossterm::event::KeyCode;
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::Color;
|
||||
|
||||
static INSPECT_DATA: LazyLock<InspectData> =
|
||||
LazyLock::new(|| InspectData::from(gen_container_inspect_response()));
|
||||
|
||||
#[test]
|
||||
/// Test a inspect container with default settings, keymap, and position
|
||||
fn test_draw_blocks_inspect_default_valid() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
// Assert border colors
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test a inspect container with custom colors
|
||||
fn test_draw_blocks_inspect_custom_color() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.borders.selected = Color::Red;
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
colors,
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
// Assert custom border colors
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test a inspect container with custom keymap for one clear key
|
||||
fn test_draw_blocks_inspect_custom_keymap_clear_one() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.clear.0 = KeyCode::Char('F');
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
// Assert border colors
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test a inspect container with custom keymap for both clear keys
|
||||
fn test_draw_blocks_inspect_custom_keymap_clear_two() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.clear.0 = KeyCode::Char('F');
|
||||
keymap.clear.1 = Some(KeyCode::Char('Z'));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
// Assert border colors
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test a inspect container with custom keymap for one inspect key
|
||||
fn test_draw_blocks_inspect_custom_keymap_inspect_one() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.inspect.0 = KeyCode::Char('4');
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
// Assert border colors
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test a inspect container with custom keymap for both inspect keys
|
||||
fn test_draw_blocks_inspect_custom_keymap_inspect_two() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.inspect.0 = KeyCode::Char('4');
|
||||
keymap.inspect.1 = Some(KeyCode::Char('5'));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
// Assert border colors
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test a inspect container with all custom keymaps
|
||||
fn test_draw_blocks_inspect_custom_keymap_all() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.clear.0 = KeyCode::Char('F');
|
||||
keymap.clear.1 = Some(KeyCode::Char('Z'));
|
||||
keymap.inspect.0 = KeyCode::Char('4');
|
||||
keymap.inspect.1 = Some(KeyCode::Char('5'));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
// Assert border colors
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Inspect details are offset 10 in x and y axis
|
||||
fn test_draw_blocks_inspect_offset() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
|
||||
// Why does one need to draw first, although it *should* be impossible to scroll before an initial drawing
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let mut gui_state = setup.gui_state.lock();
|
||||
for _ in 0..=9 {
|
||||
gui_state.set_inspect_offset(&crate::app_data::ScrollDirection::Down);
|
||||
gui_state.set_inspect_offset(&crate::app_data::ScrollDirection::Right);
|
||||
}
|
||||
}
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Inspect details are offset to the maximum allowed
|
||||
fn test_draw_blocks_inspect_offset_max() {
|
||||
let mut setup = test_setup(100, 50, true, true);
|
||||
|
||||
// Why does one need to draw first, although it *should* be impossible to scroll before an initial drawing
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Lazy way of getting the max offset
|
||||
{
|
||||
let mut gui_state = setup.gui_state.lock();
|
||||
for _ in 0..=1000 {
|
||||
gui_state.set_inspect_offset(&crate::app_data::ScrollDirection::Down);
|
||||
gui_state.set_inspect_offset(&crate::app_data::ScrollDirection::Right);
|
||||
}
|
||||
}
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
f,
|
||||
AppColors::new(),
|
||||
INSPECT_DATA.clone(),
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 49, _) | (_, 0 | 99) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_container_inspect_response() -> ContainerInspectResponse {
|
||||
ContainerInspectResponse {
|
||||
id: Some("0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7".to_owned()),
|
||||
created: Some("2026-01-23T22:20:19.927967311Z".to_owned()),
|
||||
path: Some("docker-entrypoint.sh".to_owned()),
|
||||
args: Some(vec!["postgres".to_owned()]),
|
||||
state: Some(ContainerState {
|
||||
status: Some(ContainerStateStatusEnum::RUNNING),
|
||||
running: Some(true),
|
||||
paused: Some(false),
|
||||
restarting: Some(false),
|
||||
oom_killed: Some(false),
|
||||
dead: Some(false),
|
||||
pid: Some(782),
|
||||
exit_code: Some(0),
|
||||
error: Some("".to_owned()),
|
||||
started_at: Some("2026-01-30T08:09:01.574885915Z".to_owned()),
|
||||
finished_at: Some("2026-01-30T08:09:01.180567927Z".to_owned()),
|
||||
health: None,
|
||||
}),
|
||||
image: Some("sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352".to_owned()),
|
||||
resolv_conf_path: Some("/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/resolv.conf".to_owned()),
|
||||
hostname_path: Some("/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/hostname".to_owned()),
|
||||
hosts_path: Some("/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/hosts".to_owned()),
|
||||
log_path: Some("/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7-json.log".to_owned()),
|
||||
name: Some("/postgres".to_owned()),
|
||||
restart_count: Some(0),
|
||||
driver: Some("overlay2".to_owned()),
|
||||
platform: Some("linux".to_owned()),
|
||||
image_manifest_descriptor: None,
|
||||
mount_label: Some("".to_owned()),
|
||||
process_label: Some("".to_owned()),
|
||||
app_armor_profile: Some("".to_owned()),
|
||||
exec_ids: None,
|
||||
host_config: Some(HostConfig {
|
||||
cpu_shares: Some(0),
|
||||
memory: Some(1073741824),
|
||||
cgroup_parent: Some("".to_owned()),
|
||||
blkio_weight: Some(0),
|
||||
blkio_weight_device: None,
|
||||
blkio_device_read_bps: None,
|
||||
blkio_device_write_bps: None,
|
||||
blkio_device_read_iops: None,
|
||||
blkio_device_write_iops: None,
|
||||
cpu_period: Some(0),
|
||||
cpu_quota: Some(0),
|
||||
cpu_realtime_period: Some(0),
|
||||
cpu_realtime_runtime: Some(0),
|
||||
cpuset_cpus: Some("".to_owned()),
|
||||
cpuset_mems: Some("".to_owned()),
|
||||
devices: None,
|
||||
device_cgroup_rules: None,
|
||||
device_requests: None,
|
||||
memory_reservation: Some(0),
|
||||
memory_swap: Some(2147483648),
|
||||
memory_swappiness: None,
|
||||
nano_cpus: Some(0),
|
||||
oom_kill_disable: Some(false),
|
||||
init: None,
|
||||
pids_limit: None,
|
||||
ulimits: None,
|
||||
cpu_count: Some(0),
|
||||
cpu_percent: Some(0),
|
||||
io_maximum_iops: Some(0),
|
||||
io_maximum_bandwidth: Some(0),
|
||||
binds: None,
|
||||
container_id_file: Some("".to_owned()),
|
||||
log_config: Some(HostConfigLogConfig {
|
||||
typ: Some("json-file".to_owned()),
|
||||
config: Some(HashMap::new()),
|
||||
}),
|
||||
network_mode: Some("oxker-examaple-net".to_owned()),
|
||||
port_bindings: Some(HashMap::new()),
|
||||
restart_policy: Some(RestartPolicy {
|
||||
name: Some(RestartPolicyNameEnum::ALWAYS),
|
||||
maximum_retry_count: Some(0),
|
||||
}),
|
||||
auto_remove: Some(false),
|
||||
volume_driver: Some("".to_owned()),
|
||||
volumes_from: None,
|
||||
mounts: None,
|
||||
console_size: Some(vec![0, 0]),
|
||||
annotations: None,
|
||||
cap_add: None,
|
||||
cap_drop: None,
|
||||
cgroupns_mode: Some(bollard::secret::HostConfigCgroupnsModeEnum::HOST),
|
||||
dns: Some(vec![]),
|
||||
dns_options: Some(vec![]),
|
||||
dns_search: Some(vec![]),
|
||||
extra_hosts: Some(vec![]),
|
||||
group_add: None,
|
||||
ipc_mode: Some("private".to_owned()),
|
||||
cgroup: Some("".to_owned()),
|
||||
links: None,
|
||||
oom_score_adj: Some(0),
|
||||
pid_mode: Some("".to_owned()),
|
||||
privileged: Some(false),
|
||||
publish_all_ports: Some(false),
|
||||
readonly_rootfs: Some(false),
|
||||
security_opt: None,
|
||||
storage_opt: None,
|
||||
tmpfs: None,
|
||||
uts_mode: Some("".to_owned()),
|
||||
userns_mode: Some("".to_owned()),
|
||||
shm_size: Some(268435456),
|
||||
sysctls: None,
|
||||
runtime: Some("runc".to_owned()),
|
||||
isolation: Some(bollard::secret::HostConfigIsolationEnum::EMPTY),
|
||||
masked_paths: Some(vec![
|
||||
"/proc/acpi".to_owned(),
|
||||
"/proc/asound".to_owned(),
|
||||
"/proc/interrupts".to_owned(),
|
||||
"/proc/kcore".to_owned(),
|
||||
"/proc/keys".to_owned(),
|
||||
"/proc/latency_stats".to_owned(),
|
||||
"/proc/sched_debug".to_owned(),
|
||||
"/proc/scsi".to_owned(),
|
||||
"/proc/timer_list".to_owned(),
|
||||
"/proc/timer_stats".to_owned(),
|
||||
"/sys/devices/virtual/powercap".to_owned(),
|
||||
"/sys/firmware".to_owned(),
|
||||
]),
|
||||
readonly_paths: Some(vec![
|
||||
"/proc/bus".to_owned(),
|
||||
"/proc/fs".to_owned(),
|
||||
"/proc/irq".to_owned(),
|
||||
"/proc/sys".to_owned(),
|
||||
"/proc/sysrq-trigger".to_owned(),
|
||||
]),
|
||||
}),
|
||||
graph_driver: Some(DriverData {
|
||||
name: "overlay2".to_owned(),
|
||||
data: HashMap::from([
|
||||
("LowerDir".to_owned(), "/var/lib/docker/overlay2/b8dae7c82251b8dadc084dbcaceec47b3d48a5ba9d055a59934a8b88d18569ea-init/diff:/var/lib/docker/overlay2/51b93846f7ba3e00cb1ed86564e3e1d7c30df2bb1cd5a8469d54625f1e5a2eca/diff:/var/lib/docker/overlay2/c1364ead843d3af87ce286013b6301329d3089422b22b001e156e45d29b5b4dd/diff:/var/lib/docker/overlay2/0e6dc322cad77b1db3906a3a4e5e6d6b80fbffd138437e550d8849fcf4f4c1f2/diff:/var/lib/docker/overlay2/cc0f967a7471cf06e0c9ad3d474650c668a4cf0c02efe20e9c250c436f93033b/diff:/var/lib/docker/overlay2/5c59e0919969987c96a5d0e0a512a0a1a0f67ea747596af9a9c14a9566198d91/diff:/var/lib/docker/overlay2/d7709b7685c9704e1e392c515b6155517270541f6ccde426ef784403e1681fca/diff:/var/lib/docker/overlay2/c891528563fff91bffaf07416e77bcd3bdebb03e5d32ed0e3d4ee1ec5e80e880/diff:/var/lib/docker/overlay2/2b25c179a432c35cc599a082cd709c8c9a1523f8d1959f72fda21fc76e50ad00/diff:/var/lib/docker/overlay2/3b409d2f7a2455578148892302823a7f03c7c36482d08bb68fd6c1aeeec05f05/diff:/var/lib/docker/overlay2/55dbb2fab0ae8bb3bfe8183093cdd576686f7333e2b2c41e6e4178a7b6407554/diff".to_owned()),
|
||||
("MergedDir".to_owned(), "/var/lib/docker/overlay2/b8dae7c82251b8dadc084dbcaceec47b3d48a5ba9d055a59934a8b88d18569ea/merged".to_owned()),
|
||||
("WorkDir".to_owned(), "/var/lib/docker/overlay2/b8dae7c82251b8dadc084dbcaceec47b3d48a5ba9d055a59934a8b88d18569ea/work".to_owned()),
|
||||
("ID".to_owned(), "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7".to_owned()),
|
||||
("UpperDir".to_owned(), "/var/lib/docker/overlay2/b8dae7c82251b8dadc084dbcaceec47b3d48a5ba9d055a59934a8b88d18569ea/diff".to_owned()),
|
||||
]),
|
||||
}),
|
||||
storage: None,
|
||||
size_rw: None,
|
||||
size_root_fs: None,
|
||||
mounts: Some(vec![MountPoint {
|
||||
typ: Some(MountPointTypeEnum::VOLUME),
|
||||
name: Some("93bc4e4c8d3823964b58105a99a7b3a7e02c801d5560338bdaf7589966a1b02d".to_owned()),
|
||||
source: Some("/var/lib/docker/volumes/93bc4e4c8d3823964b58105a99a7b3a7e02c801d5560338bdaf7589966a1b02d/_data".to_owned()),
|
||||
destination: Some("/var/lib/postgresql/data".to_owned()),
|
||||
driver: Some("local".to_owned()),
|
||||
mode: Some("".to_owned()),
|
||||
rw: Some(true),
|
||||
propagation: Some("".to_owned()),
|
||||
}]),
|
||||
config: Some(ContainerConfig {
|
||||
hostname: Some("0bdea64212f9".to_owned()),
|
||||
domainname: Some("".to_owned()),
|
||||
user: Some("".to_owned()),
|
||||
attach_stdin: Some(false),
|
||||
attach_stdout: Some(true),
|
||||
attach_stderr: Some(true),
|
||||
exposed_ports: Some(vec!["5432/tcp".to_owned()]),
|
||||
tty: Some(false),
|
||||
open_stdin: Some(false),
|
||||
stdin_once: Some(false),
|
||||
env: Some(vec![
|
||||
"POSTGRES_PASSWORD=never_use_this_password_in_production".to_owned(),
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_owned(),
|
||||
"GOSU_VERSION=1.19".to_owned(),
|
||||
"LANG=en_US.utf8".to_owned(),
|
||||
"PG_MAJOR=17".to_owned(),
|
||||
"PG_VERSION=17.7".to_owned(),
|
||||
"PG_SHA256=ef9e343302eccd33112f1b2f0247be493cb5768313adeb558b02de8797a2e9b5".to_owned(),
|
||||
"DOCKER_PG_LLVM_DEPS=llvm19-dev \t\tclang19".to_owned(),
|
||||
"PGDATA=/var/lib/postgresql/data".to_owned(),
|
||||
]),
|
||||
cmd: Some(vec!["postgres".to_owned()]),
|
||||
healthcheck: None,
|
||||
args_escaped: None,
|
||||
image: Some("postgres:17-alpine".to_owned()),
|
||||
volumes: Some(vec!["/var/lib/postgresql/data".to_owned()]),
|
||||
working_dir: Some("/".to_owned()),
|
||||
entrypoint: Some(vec!["docker-entrypoint.sh".to_owned()]),
|
||||
network_disabled: None,
|
||||
on_build: None,
|
||||
labels: Some(HashMap::from([
|
||||
("com.docker.compose.oneoff".to_owned(), "False".to_owned()),
|
||||
("com.docker.compose.project.config_files".to_owned(), "/workspaces/oxker/docker/docker-compose.yml".to_owned()),
|
||||
("com.docker.compose.image".to_owned(), "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352".to_owned()),
|
||||
("com.docker.compose.project.working_dir".to_owned(), "/workspaces/oxker/docker".to_owned()),
|
||||
("com.docker.compose.service".to_owned(), "postgres".to_owned()),
|
||||
("com.docker.compose.config-hash".to_owned(), "e06d69ffb3f9b69dd51b356b60c2297df57caf0da16792ccafaabffdb920e443".to_owned()),
|
||||
("com.docker.compose.depends_on".to_owned(), "".to_owned()),
|
||||
("com.docker.compose.container-number".to_owned(), "1".to_owned()),
|
||||
("com.docker.compose.version".to_owned(), "2.40.3".to_owned()),
|
||||
("com.docker.compose.project".to_owned(), "docker".to_owned()),
|
||||
])),
|
||||
stop_signal: Some("SIGINT".to_owned()),
|
||||
stop_timeout: None,
|
||||
shell: None,
|
||||
}),
|
||||
network_settings: Some(NetworkSettings {
|
||||
sandbox_id: Some("dab64a66594dd8d06478184e2928c81acdcd9c931f643bd5ca62b7edb6345f8d".to_owned()),
|
||||
sandbox_key: Some("/var/run/docker/netns/dab64a66594d".to_owned()),
|
||||
ports: Some(HashMap::from([("5432/tcp".to_owned(), None)])),
|
||||
networks: Some(HashMap::from([(
|
||||
"oxker-examaple-net".to_owned(),
|
||||
EndpointSettings {
|
||||
ipam_config: None,
|
||||
links: None,
|
||||
mac_address: Some("a2:bd:4e:61:25:c7".to_owned()),
|
||||
aliases: Some(vec!["postgres".to_owned(), "postgres".to_owned()]),
|
||||
driver_opts: None,
|
||||
gw_priority: Some(0),
|
||||
network_id: Some("3cbeb475d81676f89a7aa205d8749ec2ad78d685e45d77b638992956f6dc569a".to_owned()),
|
||||
endpoint_id: Some("31718069b2a3ea77487f3ece36b014d5d1329bc3294568e2621e5c0999071bed".to_owned()),
|
||||
gateway: Some("172.19.0.1".to_owned()),
|
||||
ip_address: Some("172.19.0.4".to_owned()),
|
||||
ip_prefix_len: Some(16),
|
||||
ipv6_gateway: Some("".to_owned()),
|
||||
global_ipv6_address: Some("".to_owned()),
|
||||
global_ipv6_prefix_len: Some(0),
|
||||
dns_names: Some(vec!["postgres".to_owned(), "0bdea64212f9".to_owned()]),
|
||||
},
|
||||
)])),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,571 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Modifier, Style, Stylize},
|
||||
widgets::{List, Paragraph},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_data::AppData,
|
||||
config::AppColors,
|
||||
ui::{FrameData, GuiState, SelectablePanel, Status},
|
||||
};
|
||||
|
||||
use super::{SELECT_ARROW, generate_block};
|
||||
|
||||
/// Draw the logs panel
|
||||
pub fn draw(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let mut block = generate_block(area, colors, fd, gui_state, SelectablePanel::Logs);
|
||||
if !fd.color_logs {
|
||||
block = block.bg(colors.logs.background);
|
||||
}
|
||||
|
||||
if fd.status.contains(&Status::Init) {
|
||||
let mut paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
if !fd.color_logs {
|
||||
paragraph = paragraph.fg(colors.logs.text);
|
||||
}
|
||||
f.render_widget(paragraph, area);
|
||||
} else {
|
||||
let padding = usize::from(area.height / 5);
|
||||
let logs = app_data.lock().get_logs(area.as_size(), padding);
|
||||
if logs.is_empty() {
|
||||
let mut paragraph = Paragraph::new("no logs found")
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
if !fd.color_logs {
|
||||
paragraph = paragraph.fg(colors.logs.text);
|
||||
}
|
||||
f.render_widget(paragraph, area);
|
||||
} else if fd.color_logs {
|
||||
let items = List::new(logs)
|
||||
.block(block)
|
||||
.highlight_symbol(SELECT_ARROW)
|
||||
.scroll_padding(padding)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||
// This should always return Some, as logs is not empty
|
||||
if let Some(log_state) = app_data.lock().get_log_state() {
|
||||
f.render_stateful_widget(items, area, log_state);
|
||||
}
|
||||
} else {
|
||||
let items = List::new(logs)
|
||||
.fg(colors.logs.text)
|
||||
.block(block)
|
||||
.highlight_symbol(SELECT_ARROW)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||
// This should always return Some, as logs is not empty
|
||||
if let Some(log_state) = app_data.lock().get_log_state() {
|
||||
f.render_stateful_widget(items, area, log_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerImage, ContainerName, ScrollDirection},
|
||||
config::AppColors,
|
||||
ui::{
|
||||
FrameData, Status,
|
||||
draw_blocks::tests::{BORDER_CHARS, get_result, insert_logs, test_setup},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// No logs, panel unselected, then selected, border color changes correctly
|
||||
fn test_draw_blocks_logs_none() {
|
||||
let mut setup = test_setup(35, 6, true, true);
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 5, 0..=34) | (1..=4, 0) | (1..=5, 34) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.selectable_panel_next(&setup.app_data);
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.selectable_panel_next(&setup.app_data);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
// When selected, has a blue border
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for result_cell in result_row {
|
||||
if BORDER_CHARS.contains(&result_cell.symbol()) {
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Parsing logs, first frame spinner visible
|
||||
fn test_draw_blocks_logs_parsing_frame_one() {
|
||||
let mut setup = test_setup(32, 6, true, true);
|
||||
let uuid = Uuid::new_v4();
|
||||
setup.gui_state.lock().next_loading(uuid);
|
||||
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
fd.status.insert(Status::Init);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0, 0..=31) | (1..=4, 0) | (1..=5, 31) | (5, 0..=30) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
/// Parsing logs, second frame spinner visible
|
||||
fn test_draw_blocks_logs_parsing_frame_two() {
|
||||
let mut setup = test_setup(32, 6, true, true);
|
||||
let uuid = Uuid::new_v4();
|
||||
setup.gui_state.lock().next_loading(uuid);
|
||||
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
fd.status.insert(Status::Init);
|
||||
|
||||
// animation moved by one frame
|
||||
setup.gui_state.lock().next_loading(uuid);
|
||||
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
fd.status.insert(Status::Init);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0, 0..=31) | (1..=4, 0) | (1..=5, 31) | (5, 0..=30) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Logs correct displayed, changing log state also draws correctly
|
||||
fn test_draw_blocks_logs_some_line_three() {
|
||||
let mut setup = test_setup(36, 6, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
// let expected_row = expected_to_vec(&expected, row_index);
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
// assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (1..=4, 1..=34) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
} else {
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
if row_index == 3 && (1..=34).contains(&result_cell_index) {
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
} else {
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
/// Logs correct displayed, changing log state also draws correctly
|
||||
fn test_draw_blocks_logs_some_line_two() {
|
||||
let mut setup = test_setup(36, 6, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
setup.app_data.lock().log_scroll(&ScrollDirection::Up);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (1..=4, 1..=34) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
} else {
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
if row_index == 2 && (1..=34).contains(&result_cell_index) {
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
} else {
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Full (long) name displayed in logs border
|
||||
fn test_draw_blocks_logs_long_name() {
|
||||
let mut setup = test_setup(80, 6, true, true);
|
||||
setup.app_data.lock().containers.items[0].name =
|
||||
ContainerName::from("a_long_container_name_for_the_purposes_of_this_test");
|
||||
setup.app_data.lock().containers.items[0].image =
|
||||
ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test");
|
||||
insert_logs(&setup);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_draw_blocks_logs_custom_colors_parsing() {
|
||||
let mut setup = test_setup(32, 6, true, true);
|
||||
let uuid = Uuid::new_v4();
|
||||
setup.gui_state.lock().next_loading(uuid);
|
||||
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
fd.status.insert(Status::Init);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.logs.background = Color::Green;
|
||||
colors.logs.text = Color::Black;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Green);
|
||||
if let (1..=4, 1..=29) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fd.color_logs = true;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (1..=4, 1..=29) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_draw_blocks_logs_custom_colors_no_logs() {
|
||||
let mut setup = test_setup(35, 6, true, true);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.logs.background = Color::Green;
|
||||
colors.logs.text = Color::Black;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Green);
|
||||
if let (1..=4, 1..=29) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setup.fd.color_logs = true;
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (1..=4, 1..=29) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Logs correct displayed with custom colors
|
||||
fn test_draw_blocks_logs_custom_colors_logs() {
|
||||
let mut setup = test_setup(36, 6, true, true);
|
||||
insert_logs(&setup);
|
||||
|
||||
let mut colors = setup.app_data.lock().config.app_colors;
|
||||
colors.logs.background = Color::Green;
|
||||
colors.logs.text = Color::Black;
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
fd.color_logs = true;
|
||||
|
||||
// Standard colors when color_logs is true
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (1..=4, 1..=34) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
if row_index == 3 && (1..=34).contains(&result_cell_index) {
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
} else {
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fd.color_logs = false;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Green);
|
||||
if let (1..=4, 1..=34) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
if row_index == 3 && (1..=34).contains(&result_cell_index) {
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
} else {
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,556 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Rect},
|
||||
style::Style,
|
||||
widgets::{Block, BorderType, Borders},
|
||||
};
|
||||
|
||||
use crate::config::AppColors;
|
||||
|
||||
use super::{FrameData, GuiState, SelectablePanel, Status, gui_state::Region};
|
||||
|
||||
pub mod chart_bandwidth;
|
||||
pub mod chart_cpu_mem;
|
||||
pub mod commands;
|
||||
pub mod containers;
|
||||
pub mod delete_confirm;
|
||||
pub mod error;
|
||||
pub mod filter;
|
||||
pub mod headers;
|
||||
pub mod help;
|
||||
pub mod info;
|
||||
pub mod inspect;
|
||||
pub mod logs;
|
||||
pub mod popup;
|
||||
pub mod ports;
|
||||
pub mod search_logs;
|
||||
|
||||
pub const NAME_TEXT: &str = r#" 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 "#;
|
||||
|
||||
pub const NAME: &str = env!("CARGO_PKG_NAME");
|
||||
pub const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||
pub const MARGIN: &str = " ";
|
||||
pub const SELECT_ARROW: &str = "▶ ";
|
||||
pub const LEFT_ARROW: &str = "←";
|
||||
pub const RIGHT_ARROW: &str = "→";
|
||||
pub const DOWN_ARROW: &str = "↓";
|
||||
pub const UP_ARROW: &str = "↑";
|
||||
pub const CIRCLE: &str = "⚪ ";
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
#[cfg(test)]
|
||||
pub const VERSION: &str = "0.00.000";
|
||||
|
||||
pub const CONSTRAINT_50_50: [Constraint; 2] =
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)];
|
||||
pub const CONSTRAINT_100: [Constraint; 1] = [Constraint::Percentage(100)];
|
||||
pub const CONSTRAINT_POPUP: [Constraint; 5] = [
|
||||
Constraint::Min(2),
|
||||
Constraint::Max(1),
|
||||
Constraint::Max(1),
|
||||
Constraint::Max(3),
|
||||
Constraint::Min(1),
|
||||
];
|
||||
|
||||
pub const CONSTRAINT_BUTTONS: [Constraint; 5] = [
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(35),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(35),
|
||||
Constraint::Percentage(10),
|
||||
];
|
||||
|
||||
/// From a given &str, return the maximum number of chars on a single line
|
||||
pub fn max_line_width(text: &str) -> usize {
|
||||
text.lines()
|
||||
.map(|i| i.chars().count())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
/// Generate block, add a border if is the selected panel,
|
||||
/// add custom title based on state of each panel
|
||||
fn generate_block<'a>(
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
panel: SelectablePanel,
|
||||
) -> Block<'a> {
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Panel(panel), area);
|
||||
|
||||
let mut title = match panel {
|
||||
SelectablePanel::Containers => {
|
||||
format!("{}{}", panel.title(), fd.container_title)
|
||||
}
|
||||
SelectablePanel::Logs => {
|
||||
format!("{}{}", panel.title(), fd.log_title)
|
||||
}
|
||||
SelectablePanel::Commands => String::new(),
|
||||
};
|
||||
if !title.is_empty() {
|
||||
title = format!(" {title} ");
|
||||
}
|
||||
let mut block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(ratatui::text::Line::from(title).left_aligned());
|
||||
|
||||
if panel == SelectablePanel::Logs
|
||||
&& let Some(x) = fd.scroll_title.as_ref()
|
||||
{
|
||||
block = block
|
||||
.title_bottom(x.to_owned())
|
||||
.title_alignment(ratatui::layout::Alignment::Right);
|
||||
}
|
||||
if !fd.status.contains(&Status::Filter) {
|
||||
if fd.selected_panel == panel {
|
||||
block = block.border_style(Style::default().fg(colors.borders.selected));
|
||||
} else {
|
||||
block = block.border_style(Style::default().fg(colors.borders.unselected));
|
||||
}
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub mod tests {
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{Terminal, backend::TestBackend, layout::Rect, style::Color};
|
||||
|
||||
use crate::{
|
||||
app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts},
|
||||
app_error::AppError,
|
||||
tests::{gen_appdata, gen_containers},
|
||||
ui::{GuiState, Rerender, Status, draw_frame},
|
||||
};
|
||||
|
||||
use super::FrameData;
|
||||
|
||||
pub struct TuiTestSetup {
|
||||
pub app_data: Arc<Mutex<AppData>>,
|
||||
pub gui_state: Arc<Mutex<GuiState>>,
|
||||
pub fd: FrameData,
|
||||
pub area: Rect,
|
||||
pub terminal: Terminal<TestBackend>,
|
||||
pub ids: Vec<ContainerId>,
|
||||
}
|
||||
|
||||
pub const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"];
|
||||
pub const COLOR_RX: Color = Color::Rgb(255, 233, 193);
|
||||
pub const COLOR_TX: Color = Color::Rgb(205, 140, 140);
|
||||
pub const COLOR_ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||
|
||||
/// Create a FrameData struct from two Arc<mutex>'s, instead of from UI
|
||||
impl From<(&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)> for FrameData {
|
||||
fn from(data: (&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)) -> Self {
|
||||
let (mut app_data, gui_data) = (data.0.lock(), data.1.lock());
|
||||
|
||||
let (filter_by, filter_term) = app_data.get_filter();
|
||||
Self {
|
||||
chart_data: app_data.get_chart_data(),
|
||||
color_logs: app_data.config.color_logs,
|
||||
columns: app_data.get_width(),
|
||||
container_title: app_data.get_container_title(),
|
||||
delete_confirm: gui_data.get_delete_container(),
|
||||
filter_by,
|
||||
filter_term: filter_term.cloned(),
|
||||
has_containers: app_data.get_container_len() > 0,
|
||||
has_error: app_data.get_error(),
|
||||
show_logs: gui_data.get_show_logs(),
|
||||
info_text: gui_data.info_box_text.clone(),
|
||||
is_loading: gui_data.is_loading(),
|
||||
log_search: app_data.gen_log_search(),
|
||||
loading_icon: gui_data.get_loading().to_string(),
|
||||
log_height: gui_data.get_log_height(),
|
||||
log_title: app_data.get_log_title(),
|
||||
scroll_title: app_data.get_scroll_title(gui_data.get_screen_width()),
|
||||
port_max_lens: app_data.get_longest_port(),
|
||||
ports: app_data.get_selected_ports(),
|
||||
selected_panel: gui_data.get_selected_panel(),
|
||||
sorted_by: app_data.get_sorted(),
|
||||
status: gui_data.get_status(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate state to be used in *most* gui tests
|
||||
pub fn test_setup(w: u16, h: u16, control_start: bool, container_start: bool) -> TuiTestSetup {
|
||||
let backend = TestBackend::new(w, h);
|
||||
let terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let (ids, containers) = gen_containers();
|
||||
let mut app_data = gen_appdata(&containers);
|
||||
if control_start {
|
||||
app_data.docker_controls_start();
|
||||
}
|
||||
if container_start {
|
||||
app_data.containers_start();
|
||||
}
|
||||
|
||||
let redraw = Arc::new(Rerender::new());
|
||||
let gui_state = GuiState::new(&redraw, app_data.config.show_logs);
|
||||
|
||||
let app_data = Arc::new(Mutex::new(app_data));
|
||||
let gui_state = Arc::new(Mutex::new(gui_state));
|
||||
let fd = FrameData::from((&app_data, &gui_state));
|
||||
let area = Rect::new(0, 0, w, h);
|
||||
gui_state.lock().set_screen_width(w);
|
||||
TuiTestSetup {
|
||||
app_data,
|
||||
gui_state,
|
||||
fd,
|
||||
area,
|
||||
terminal,
|
||||
ids,
|
||||
}
|
||||
}
|
||||
|
||||
/// Just a shorthand for when enumerating over result cells
|
||||
pub fn get_result(
|
||||
setup: &'_ TuiTestSetup,
|
||||
) -> std::iter::Enumerate<std::slice::Chunks<'_, ratatui::buffer::Cell>> {
|
||||
setup
|
||||
.terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.chunks(usize::from(setup.area.width))
|
||||
.enumerate()
|
||||
}
|
||||
|
||||
/// Insert some logs into the first container
|
||||
pub fn insert_logs(setup: &TuiTestSetup) {
|
||||
let logs = (1..=3).map(|i| format!("{i} line {i}")).collect::<Vec<_>>();
|
||||
setup.app_data.lock().update_log_by_id(logs, &setup.ids[0]);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
// Add fixed data to the cpu & mem vecdeques
|
||||
pub fn insert_all_chart_data(setup: &TuiTestSetup) {
|
||||
for i in 1..=10 {
|
||||
setup.app_data.lock().update_stats_by_id(
|
||||
&setup.ids[0],
|
||||
Some(i as f64),
|
||||
Some(i * 10000),
|
||||
i * 10000,
|
||||
i,
|
||||
i,
|
||||
);
|
||||
}
|
||||
for i in 1..=3 {
|
||||
setup.app_data.lock().update_stats_by_id(
|
||||
&setup.ids[0],
|
||||
Some(i as f64),
|
||||
Some(i * 10000),
|
||||
i * 10000,
|
||||
i,
|
||||
i,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// *************** //
|
||||
// The whole layout //
|
||||
// **************** //
|
||||
#[test]
|
||||
/// Check that the whole layout is drawn correctly
|
||||
fn test_draw_blocks_whole_layout() {
|
||||
let mut setup = test_setup(160, 30, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
/// Check that the whole layout is drawn correctly
|
||||
fn test_draw_blocks_whole_layout_with_filter_bar() {
|
||||
let mut setup = test_setup(160, 30, true, true);
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
|
||||
setup.app_data.lock().containers.items[1]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::Filter);
|
||||
setup.app_data.lock().filter_term_push('r');
|
||||
setup.app_data.lock().filter_term_push('_');
|
||||
setup.app_data.lock().filter_term_push('1');
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Check that the whole layout is drawn correctly when have long container name and long image name
|
||||
fn test_draw_blocks_whole_layout_long_name() {
|
||||
let mut setup = test_setup(190, 30, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
|
||||
setup.app_data.lock().containers.items[0].name =
|
||||
ContainerName::from("a_long_container_name_for_the_purposes_of_this_test");
|
||||
setup.app_data.lock().containers.items[0].image =
|
||||
ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test");
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Check that the whole layout is drawn correctly when the logs panel is removed
|
||||
fn test_draw_blocks_whole_layout_no_logs() {
|
||||
let mut setup = test_setup(160, 30, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
setup.gui_state.lock().log_height_zero();
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Check that the whole layout is drawn correctly when the logs panel height is ~4
|
||||
fn test_draw_blocks_whole_layout_short_height_logs() {
|
||||
let mut setup = test_setup(160, 30, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
setup.gui_state.lock().log_height_zero();
|
||||
|
||||
for _ in 0..=3 {
|
||||
setup.gui_state.lock().log_height_increase();
|
||||
}
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Check that the whole layout is drawn with the help panel visible
|
||||
fn test_draw_blocks_whole_layout_help_panel() {
|
||||
let mut setup = test_setup(160, 40, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
|
||||
setup.gui_state.lock().status_push(Status::Help);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Check that the whole layout is drawn with the error box is visible
|
||||
fn test_draw_blocks_whole_layout_error() {
|
||||
let mut setup = test_setup(160, 40, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
|
||||
setup.app_data.lock().set_error(
|
||||
AppError::DockerCommand(crate::app_data::DockerCommand::Pause),
|
||||
&setup.gui_state,
|
||||
Status::Error,
|
||||
);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Check that the whole layout is drawn with the delete box is visible
|
||||
fn test_draw_blocks_whole_layout_delete() {
|
||||
let mut setup = test_setup(160, 40, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.set_delete_container(setup.app_data.lock().get_selected_container_id());
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Check that the whole layout is drawn with the info box is visible
|
||||
fn test_draw_blocks_whole_layout_info_box() {
|
||||
let mut setup = test_setup(160, 40, true, true);
|
||||
|
||||
insert_all_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = setup.app_data.lock().config.keymap.clone();
|
||||
setup.gui_state.lock().set_info_box("This is a test");
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
use ratatui::layout::{Direction, Layout, Rect};
|
||||
|
||||
use crate::ui::gui_state::BoxLocation;
|
||||
|
||||
/// draw a box in the one of the BoxLocations, based on max line width + number of lines
|
||||
pub fn draw(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocation) -> Rect {
|
||||
// Make sure blank_space can't be an negative, as will crash
|
||||
let calc = |x: u16, y: usize| usize::from(x).saturating_sub(y).saturating_div(2);
|
||||
|
||||
let blank_vertical = calc(r.height, text_lines);
|
||||
let blank_horizontal = calc(r.width, text_width);
|
||||
|
||||
let (h_constraints, v_constraints) = box_location.get_constraints(
|
||||
blank_horizontal.try_into().unwrap_or_default(),
|
||||
blank_vertical.try_into().unwrap_or_default(),
|
||||
text_lines.try_into().unwrap_or_default(),
|
||||
text_width.try_into().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let indexes = box_location.get_indexes();
|
||||
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(v_constraints)
|
||||
.split(r);
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(h_constraints)
|
||||
.split(popup_layout[indexes.0])[indexes.1]
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Paragraph},
|
||||
};
|
||||
|
||||
use crate::{app_data::State, config::AppColors, ui::FrameData};
|
||||
|
||||
/// Get the port title color, at the moment the color is only customizable if the container is alive
|
||||
const fn get_port_title_color(colors: AppColors, state: State) -> Color {
|
||||
if state.is_alive() {
|
||||
colors.chart_ports.title
|
||||
} else {
|
||||
state.get_color(colors)
|
||||
}
|
||||
}
|
||||
|
||||
/// Display the ports in a formatted list
|
||||
pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
||||
if let Some(ports) = fd.ports.as_ref() {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::new().fg(colors.chart_ports.border))
|
||||
.title_alignment(Alignment::Center)
|
||||
.title(Span::styled(
|
||||
" ports ",
|
||||
Style::default()
|
||||
.fg(get_port_title_color(colors, ports.1))
|
||||
.bg(colors.chart_ports.background)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
|
||||
let (ip, private, public) = fd.port_max_lens;
|
||||
|
||||
if ports.0.is_empty() {
|
||||
let text = match ports.1 {
|
||||
State::Running(_) | State::Paused | State::Restarting => "no ports",
|
||||
_ => "",
|
||||
};
|
||||
let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD))
|
||||
.alignment(Alignment::Center)
|
||||
.block(block)
|
||||
.bg(colors.chart_ports.background);
|
||||
f.render_widget(paragraph, area);
|
||||
} else {
|
||||
let mut output = vec![Line::from(
|
||||
Span::from(format!(
|
||||
"{:>ip$}{:>private$}{:>public$}",
|
||||
"ip", "private", "public"
|
||||
))
|
||||
.fg(colors.chart_ports.headings),
|
||||
)];
|
||||
for item in &ports.0 {
|
||||
let strings = item.get_all();
|
||||
|
||||
let line = vec![
|
||||
Span::from(format!("{:>ip$}", strings.0)).fg(colors.chart_ports.text),
|
||||
Span::from(format!("{:>private$}", strings.1)).fg(colors.chart_ports.text),
|
||||
Span::from(format!("{:>public$}", strings.2)).fg(colors.chart_ports.text),
|
||||
];
|
||||
output.push(Line::from(line));
|
||||
}
|
||||
let paragraph = Paragraph::new(output)
|
||||
.block(block)
|
||||
.bg(colors.chart_ports.background);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerPorts, RunningState, State},
|
||||
config::AppColors,
|
||||
ui::{
|
||||
FrameData,
|
||||
draw_blocks::tests::{COLOR_ORANGE, COLOR_RX, COLOR_TX, get_result, test_setup},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Port section when container has no ports
|
||||
fn test_draw_blocks_ports_no_ports() {
|
||||
let mut setup = test_setup(30, 8, true, true);
|
||||
setup.app_data.lock().containers.items[0].ports = vec![];
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match (row_index, result_cell_index) {
|
||||
(0, 11..=17) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
(1, 11..=18) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Port section when container has no ports
|
||||
// When state is "State::Running | State::Paused | State::Restarting, won't show "no ports"
|
||||
fn test_draw_blocks_ports_no_ports_dead() {
|
||||
let mut setup = test_setup(30, 8, true, true);
|
||||
setup.app_data.lock().containers.items[0].ports = vec![];
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
// split
|
||||
|
||||
setup.app_data.lock().containers.items[0].state = State::Dead;
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (0, 11..=17) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
} else {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Port section when container has multiple ports
|
||||
fn test_draw_blocks_ports_multiple_ports() {
|
||||
let mut setup = test_setup(32, 8, true, true);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: None,
|
||||
private: 8002,
|
||||
public: None,
|
||||
});
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
(0, 12..=18) => {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
(1, 1..=28) => {
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
(2..=4, 1..=28) | (0 | 2..=9, 0..=31) | (1, 0 | 29..=31) => {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Port section title color correct dependant on state
|
||||
fn test_draw_blocks_ports_container_state() {
|
||||
let mut setup = test_setup(32, 8, true, true);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (0, 12..=18) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setup.app_data.lock().containers.items[0].state = State::Paused;
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (0, 12..=18) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setup.app_data.lock().containers.items[0].state = State::Exited;
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
if let (0, 12..=18) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colors applied to ports panel
|
||||
fn test_draw_blocks_ports_custom_colors() {
|
||||
let mut setup = test_setup(32, 8, true, true);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.chart_ports.background = Color::Black;
|
||||
colors.chart_ports.border = Color::Yellow;
|
||||
colors.chart_ports.headings = Color::Red;
|
||||
colors.chart_ports.text = Color::Green;
|
||||
colors.chart_ports.title = Color::Magenta;
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
// title => {
|
||||
(0, 12..=18) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
// title
|
||||
(1, 1..=24) => {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// text
|
||||
(2, 1..=24) => {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
}
|
||||
// border & everything else
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Custom state color applied to ports panel title
|
||||
fn test_draw_blocks_ports_custom_colors_state() {
|
||||
let mut setup = test_setup(32, 8, true, true);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.container_state.dead = Color::Green;
|
||||
colors.container_state.exited = Color::Magenta;
|
||||
colors.container_state.paused = Color::Gray;
|
||||
colors.container_state.removing = COLOR_ORANGE;
|
||||
colors.container_state.restarting = COLOR_RX;
|
||||
colors.container_state.running_healthy = COLOR_TX;
|
||||
colors.container_state.running_unhealthy = Color::Cyan;
|
||||
colors.container_state.unknown = Color::LightMagenta;
|
||||
|
||||
colors.chart_ports.title = Color::DarkGray;
|
||||
|
||||
for i in [
|
||||
(State::Dead, Color::Green),
|
||||
(State::Exited, Color::Magenta),
|
||||
(State::Paused, Color::Gray),
|
||||
(State::Removing, COLOR_ORANGE),
|
||||
(State::Restarting, COLOR_RX),
|
||||
(State::Unknown, Color::LightMagenta),
|
||||
(State::Running(RunningState::Healthy), Color::DarkGray),
|
||||
(State::Running(RunningState::Unhealthy), Color::DarkGray),
|
||||
] {
|
||||
setup.app_data.lock().containers.items[0].state = i.0;
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (row_index, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
if row_index == 0 && (12..=18).contains(&result_cell_index) {
|
||||
assert_eq!(result_cell.fg, i.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,479 +0,0 @@
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::Paragraph,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_data::LogsButton,
|
||||
config::{AppColors, Keymap},
|
||||
ui::FrameData,
|
||||
};
|
||||
|
||||
// background, text, selected_text, highlight;
|
||||
/// Draw the filter bar
|
||||
pub fn draw(area: Rect, colors: AppColors, frame: &mut Frame, fd: &FrameData, keymap: &Keymap) {
|
||||
let style_but = Style::default()
|
||||
.fg(colors.log_search.button_text)
|
||||
.bg(colors.log_search.highlight);
|
||||
let style_desc = Style::default()
|
||||
.fg(colors.log_search.text)
|
||||
.bg(colors.log_search.background);
|
||||
let space = || Span::from(" ");
|
||||
|
||||
let mut line = vec![
|
||||
Span::styled(" Esc ", style_but),
|
||||
Span::styled(" clear ", style_desc),
|
||||
space(),
|
||||
];
|
||||
line.extend([Span::styled(
|
||||
" search term: ",
|
||||
Style::default()
|
||||
.fg(colors.log_search.highlight)
|
||||
.bg(colors.log_search.background)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)]);
|
||||
|
||||
if let Some(log_search) = fd.log_search.as_ref() {
|
||||
line.extend([
|
||||
Span::styled(
|
||||
log_search
|
||||
.term
|
||||
.as_ref()
|
||||
.map_or(String::new(), std::clone::Clone::clone),
|
||||
Style::default()
|
||||
.fg(colors.log_search.text)
|
||||
.bg(colors.log_search.background),
|
||||
),
|
||||
space(),
|
||||
]);
|
||||
}
|
||||
|
||||
let left_text = Paragraph::new(Line::from(line))
|
||||
.alignment(ratatui::layout::Alignment::Left)
|
||||
.style(Style::default().bg(colors.log_search.background));
|
||||
let mut line = vec![];
|
||||
if let Some(log_search) = fd.log_search.as_ref() {
|
||||
if let Some(buttons) = log_search.buttons.as_ref() {
|
||||
let down = if keymap.scroll_down.0 == KeyCode::Down {
|
||||
"↑".to_owned()
|
||||
} else {
|
||||
keymap.scroll_down.0.to_string()
|
||||
};
|
||||
let up = if keymap.scroll_up.0 == KeyCode::Up {
|
||||
"↓".to_owned()
|
||||
} else {
|
||||
keymap.scroll_up.0.to_string()
|
||||
};
|
||||
let next = [
|
||||
space(),
|
||||
Span::styled(format!(" {up} "), style_but),
|
||||
Span::styled(" next ", style_desc),
|
||||
];
|
||||
let previous = [
|
||||
space(),
|
||||
Span::styled(format!(" {down} "), style_but),
|
||||
Span::styled(" previous ", style_desc),
|
||||
];
|
||||
|
||||
match buttons {
|
||||
LogsButton::Both => line.extend(previous.into_iter().chain(next)),
|
||||
LogsButton::Next => line.extend(next),
|
||||
LogsButton::Previous => line.extend(previous),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(results) = log_search.result.as_ref() {
|
||||
line.extend([
|
||||
Span::styled(
|
||||
" matches: ",
|
||||
Style::default()
|
||||
.fg(colors.log_search.highlight)
|
||||
.bg(colors.log_search.background)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
results,
|
||||
Style::default()
|
||||
.fg(colors.log_search.text)
|
||||
.bg(colors.log_search.background),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
let right_text = Paragraph::new(Line::from(line))
|
||||
.alignment(ratatui::layout::Alignment::Right)
|
||||
.style(Style::default().bg(colors.log_search.background));
|
||||
|
||||
let line_split = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(area);
|
||||
frame.render_widget(left_text, line_split[0]);
|
||||
frame.render_widget(right_text, line_split[1]);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use insta::assert_snapshot;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
config::{AppColors, Keymap},
|
||||
ui::{
|
||||
FrameData,
|
||||
draw_blocks::tests::{get_result, insert_logs, test_setup},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Filter row is drawn correctly & colors are correct
|
||||
/// Colours change when filter_by option is changed
|
||||
fn test_draw_blocks_log_search_row() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::SearchLogs);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &setup.fd, &Keymap::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
13..=26 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Log item found, previous button visible
|
||||
fn test_draw_blocks_log_search_match_previous() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::SearchLogs);
|
||||
|
||||
setup.app_data.lock().log_search_push('e');
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &fd, &Keymap::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 | 114..=116 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 27 | 117..=126 | 137..=139 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
13..=26 | 127..=136 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Log item found, next button visible
|
||||
fn test_draw_blocks_log_search_match_next() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::SearchLogs);
|
||||
|
||||
setup.app_data.lock().log_search_push('e');
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.log_scroll(&crate::app_data::ScrollDirection::Up);
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.log_scroll(&crate::app_data::ScrollDirection::Up);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &fd, &Keymap::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 | 118..=120 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 27 | 121..=126 | 137..=139 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
13..=26 | 127..=136 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Log item found, next & previous button visible
|
||||
fn test_draw_blocks_log_search_match_both_next_previous() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::SearchLogs);
|
||||
|
||||
setup.app_data.lock().log_search_push('e');
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.log_scroll(&crate::app_data::ScrollDirection::Up);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &fd, &Keymap::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 | 104..=106 | 118..=120 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 27 | 107..=116 | 121..=126 | 137..=139 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
13..=26 | 127..=136 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// No log item found
|
||||
fn test_draw_blocks_log_search_match_none() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::SearchLogs);
|
||||
|
||||
setup.app_data.lock().log_search_push('z');
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &fd, &Keymap::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 27 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
13..=26 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap for scroll buttons
|
||||
fn test_draw_blocks_log_search_keymap() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
|
||||
let mut keymap = setup.app_data.lock().config.keymap.clone();
|
||||
keymap.scroll_up = (KeyCode::Char('a'), None);
|
||||
keymap.scroll_down = (KeyCode::Char('b'), None);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::SearchLogs);
|
||||
|
||||
setup.app_data.lock().log_search_push('e');
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.log_scroll(&crate::app_data::ScrollDirection::Up);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, AppColors::new(), f, &fd, &keymap);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colours applied
|
||||
fn test_draw_blocks_log_search_colors() {
|
||||
let mut setup = test_setup(140, 1, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::SearchLogs);
|
||||
|
||||
setup.app_data.lock().log_search_push('e');
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.log_scroll(&crate::app_data::ScrollDirection::Up);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
|
||||
colors.log_search.background = Color::White;
|
||||
colors.log_search.highlight = Color::Blue;
|
||||
colors.log_search.button_text = Color::Yellow;
|
||||
colors.log_search.text = Color::Magenta;
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd, &Keymap::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_snapshot!(setup.terminal.backend());
|
||||
|
||||
for (_, result_row) in get_result(&setup) {
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
match result_cell_index {
|
||||
0..=4 | 104..=106 | 118..=120 => {
|
||||
assert_eq!(result_cell.bg, Color::Blue);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
5..=11 | 27 | 107..=116 | 121..=126 | 137..=139 => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
13..=26 | 127..=136 => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::White);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_bandwidth.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭─── rx: 566.00 kb/s tx: 56.60 kb/s ───╮"
|
||||
"│ │ • │"
|
||||
"│ │ •• │"
|
||||
"│ │ •• │"
|
||||
"│566.00 kb/s│ •• │"
|
||||
"│ │ • │"
|
||||
"│56.60 kb/s │ •• │"
|
||||
"│ │•• ••• │"
|
||||
"│ │•••••• │"
|
||||
"╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_bandwidth.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──── rx: 0.00 kb/s tx: 0.00 kb/s ─────╮"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"│0.00 kb/s│ │"
|
||||
"│ │ │"
|
||||
"│0.00 kb/s│ │"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_bandwidth.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──── rx: 0.00 kb/s tx: 0.00 kb/s ─────╮"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"│0.00 kb/s│ │"
|
||||
"│ │ │"
|
||||
"│0.00 kb/s│ │"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_bandwidth.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──── rx: 0.00 kb/s tx: 0.00 kb/s ─────╮"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"│0.00 kb/s│ │"
|
||||
"│ │ │"
|
||||
"│0.00 kb/s│ │"
|
||||
"│ │ │"
|
||||
"│ │ │"
|
||||
"╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_bandwidth.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭─── rx: 0.00 kb/s tx: 205.00 kb/s ────╮"
|
||||
"│ │ • │"
|
||||
"│ │ •• │"
|
||||
"│ │ •• │"
|
||||
"│205.00 kb/s│ •• │"
|
||||
"│ │ • │"
|
||||
"│0.00 kb/s │ •• │"
|
||||
"│ │•• │"
|
||||
"│ │ │"
|
||||
"╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_bandwidth.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭─── rx: 566.00 kb/s tx: 0.00 kb/s ────╮"
|
||||
"│ │ • │"
|
||||
"│ │ •• │"
|
||||
"│ │ •• │"
|
||||
"│566.00 kb/s│ •• │"
|
||||
"│ │ • │"
|
||||
"│0.00 kb/s │ •• │"
|
||||
"│ │•• │"
|
||||
"│ │• │"
|
||||
"╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_bandwidth.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭─── rx: 0.00 kb/s tx: 205.00 kb/s ────╮"
|
||||
"│ │ • │"
|
||||
"│ │ •• │"
|
||||
"│ │ •• │"
|
||||
"│205.00 kb/s│ •• │"
|
||||
"│ │ • │"
|
||||
"│0.00 kb/s │ •• │"
|
||||
"│ │•• │"
|
||||
"│ │ │"
|
||||
"╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_bandwidth.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭─── rx: 566.00 kb/s tx: 56.60 kb/s ───╮"
|
||||
"│ │ • │"
|
||||
"│ │ •• │"
|
||||
"│ │ •• │"
|
||||
"│566.00 kb/s│ •• │"
|
||||
"│ │ • │"
|
||||
"│56.60 kb/s │ •• │"
|
||||
"│ │•• ••• │"
|
||||
"│ │•••••• │"
|
||||
"╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_cpu_mem.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮"
|
||||
"│10.00%│ • ││100.00 kB│ • │"
|
||||
"│ │ •• ││ │ •• │"
|
||||
"│ │ • • ││ │ •• │"
|
||||
"│ │ • • ││ │ • • │"
|
||||
"│ │ • • ││ │ •• • │"
|
||||
"│ │ • •• ││ │ • • │"
|
||||
"│ │•• •• ││ │• • │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_cpu_mem.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮"
|
||||
"│10.00%│ • ││100.00 kB│ • │"
|
||||
"│ │ •• ││ │ •• │"
|
||||
"│ │ • • ││ │ •• │"
|
||||
"│ │ • • ││ │ • • │"
|
||||
"│ │ • • ││ │ •• • │"
|
||||
"│ │ • •• ││ │ • • │"
|
||||
"│ │•• •• ││ │• • │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_cpu_mem.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮"
|
||||
"│10.00%│ • ││100.00 kB│ • │"
|
||||
"│ │ •• ││ │ •• │"
|
||||
"│ │ • • ││ │ •• │"
|
||||
"│ │ • • ││ │ • • │"
|
||||
"│ │ • • ││ │ •• • │"
|
||||
"│ │ • •• ││ │ • • │"
|
||||
"│ │•• •• ││ │• • │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_cpu_mem.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 00.00% ─────────────╮╭─────────── memory 0.00 kB ───────────╮"
|
||||
"│00.00%│ ││0.00 kB│ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/chart_cpu_mem.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮"
|
||||
"│10.00%│ • ││100.00 kB│ • │"
|
||||
"│ │ •• ││ │ •• │"
|
||||
"│ │ • • ││ │ •• │"
|
||||
"│ │ • • ││ │ • • │"
|
||||
"│ │ • • ││ │ •• • │"
|
||||
"│ │ • •• ││ │ • • │"
|
||||
"│ │•• •• ││ │• • │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/charts.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮"
|
||||
"│10.00%│ • ││100.00 kB│ • │"
|
||||
"│ │ •• ││ │ •• │"
|
||||
"│ │ • • ││ │ •• │"
|
||||
"│ │ • • ││ │ • • │"
|
||||
"│ │ • • ││ │ •• • │"
|
||||
"│ │ • •• ││ │ • • │"
|
||||
"│ │•• •• ││ │• • │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/charts.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮"
|
||||
"│10.00%│ • ││100.00 kB│ • │"
|
||||
"│ │ •• ││ │ •• │"
|
||||
"│ │ • • ││ │ •• │"
|
||||
"│ │ • • ││ │ • • │"
|
||||
"│ │ • • ││ │ •• • │"
|
||||
"│ │ • •• ││ │ • • │"
|
||||
"│ │•• •• ││ │• • │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/charts.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮"
|
||||
"│10.00%│ • ││100.00 kB│ • │"
|
||||
"│ │ •• ││ │ •• │"
|
||||
"│ │ • • ││ │ •• │"
|
||||
"│ │ • • ││ │ • • │"
|
||||
"│ │ • • ││ │ •• • │"
|
||||
"│ │ • •• ││ │ • • │"
|
||||
"│ │•• •• ││ │• • │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/charts.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 00.00% ─────────────╮╭─────────── memory 0.00 kB ───────────╮"
|
||||
"│00.00%│ ││0.00 kB│ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/charts.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮"
|
||||
"│10.00%│ • ││100.00 kB│ • │"
|
||||
"│ │ •• ││ │ •• │"
|
||||
"│ │ • • ││ │ •• │"
|
||||
"│ │ • • ││ │ • • │"
|
||||
"│ │ • • ││ │ •• • │"
|
||||
"│ │ • •• ││ │ • • │"
|
||||
"│ │•• •• ││ │• • │"
|
||||
"│ │ ││ │ │"
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯"
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/commands.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──────────╮"
|
||||
"│ resume │"
|
||||
"│▶ stop │"
|
||||
"│ delete │"
|
||||
"│ │"
|
||||
"╰──────────╯"
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/commands.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──────────╮"
|
||||
"│▶ pause │"
|
||||
"│ restart │"
|
||||
"│ stop │"
|
||||
"│ delete │"
|
||||
"╰──────────╯"
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/commands.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──────────╮"
|
||||
"│ │"
|
||||
"│ │"
|
||||
"│ │"
|
||||
"│ │"
|
||||
"╰──────────╯"
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/commands.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──────────╮"
|
||||
"│▶ pause │"
|
||||
"│ restart │"
|
||||
"│ stop │"
|
||||
"│ delete │"
|
||||
"╰──────────╯"
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/commands.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──────────╮"
|
||||
"│▶ pause │"
|
||||
"│ restart │"
|
||||
"│ stop │"
|
||||
"│ delete │"
|
||||
"╰──────────╯"
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/commands.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭──────────╮"
|
||||
"│ resume │"
|
||||
"│▶ stop │"
|
||||
"│ delete │"
|
||||
"│ │"
|
||||
"╰──────────╯"
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: src/ui/draw_blocks/containers.rs
|
||||
expression: setup.terminal.backend()
|
||||
---
|
||||
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮"
|
||||
"│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │" Hidden by multi-width symbols: [(2, " ")]
|
||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │"
|
||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │"
|
||||
"│ │"
|
||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user