diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 18c6da1..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index fb2d38a..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -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" - } - } -} \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index 57f1613..0000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -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. diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md deleted file mode 100644 index 82ee01a..0000000 --- a/.github/ISSUE_TEMPLATE/feature.md +++ /dev/null @@ -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. diff --git a/.github/ISSUE_TEMPLATE/refactor.md b/.github/ISSUE_TEMPLATE/refactor.md deleted file mode 100644 index 5a279bd..0000000 --- a/.github/ISSUE_TEMPLATE/refactor.md +++ /dev/null @@ -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. diff --git a/.github/demo_01.webp b/.github/demo_01.webp deleted file mode 100644 index a159342..0000000 Binary files a/.github/demo_01.webp and /dev/null differ diff --git a/.github/logo.svg b/.github/logo.svg deleted file mode 100644 index 9f30c44..0000000 --- a/.github/logo.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/.github/release-body.md b/.github/release-body.md deleted file mode 100644 index 4bf4712..0000000 --- a/.github/release-body.md +++ /dev/null @@ -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 CHANGELOG.md for more details diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png deleted file mode 100644 index 27b91c9..0000000 Binary files a/.github/screenshot_01.png and /dev/null differ diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml deleted file mode 100644 index f8d0b56..0000000 --- a/.github/workflows/create_release_and_build.yml +++ /dev/null @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 660f195..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -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}" - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index f6c05f8..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,805 +0,0 @@ -# v0.13.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, [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) - -# v0.12.0 -### 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. - -# v0.11.1 -### 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 - -# v0.11.0 -### 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) - -# v0.10.5 -### 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) - - # v0.10.4 -### 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) - -# v0.10.3 -### 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) - -# v0.10.2 -### 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) - -# v0.10.1 -### 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) - -# v0.10.0 -### 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) - - # v0.9.0 -### 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) - -# v0.8.0 -### 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) - -# v0.7.2 -### 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) - -# v0.7.1 -### 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) - -# v0.7.0 -### 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) - -# v0.6.4 -### 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) - -# v0.6.3 -### 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) - -# v0.6.2 -### 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) - -# v0.6.1 -### 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, [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) - -# v0.6.0 -### 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) - -# v0.5.0 -### 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) - -# v0.4.0 -### 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) - -# v0.3.3 -### 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) - -# v0.3.2 -### 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) - -# v0.3.1 -### 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) - -# v0.3.0 -### 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) - -# v0.2.5 -### 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) - -# v0.2.4 -### 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) - -# v0.2.3 -### 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) - -# v0.2.2 -### 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) - -# v0.2.1 -### 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) - -# v0.2.0 -### 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) - -# v0.1.11 -### 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) - -# v0.1.10 -### 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) - -# v0.1.9 -### 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) - -# v0.1.8 -### 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) - -# v0.1.7 -### 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) - -# v0.1.6 -### 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), - -# v0.1.5 -### 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), - -# v0.1.4 -### 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), - -# v0.1.3 -### 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), - -# v0.1.2 -### 2022-07-23 - -### Fixes -+ remove reqwest dependency, [10ff8bab](https://github.com/mrjackwills/oxker/commit/10ff8bab5f01f097fd6cdec60b2be947f238197b), - -# v0.1.1 -### 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), - -# v0.1.0 -### 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), - -# v0.0.6 -### 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), - - -# v0.0.5 -### 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), - -# v0.0.4 -### 2022-05-08 - -### Fixes -+ Help menu logo corrected, [2f545202](https://github.com/mrjackwills/oxker/commit/2f5452027e86f714729b804d4bf65306e755df7f), - -# v0.0.3 -### 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), - - -# v0.0.2 -### 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), - -# v0.0.1 -### 2022-04-25 - -+ init commit diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ca07a43 --- /dev/null +++ b/CLAUDE.md @@ -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 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 1005181..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -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) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 3912ed1..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,2989 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bollard" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227aa051deec8d16bd9c34605e7aaf153f240e35483dd42f6f78903847934738" -dependencies = [ - "base64", - "bollard-stubs", - "bytes", - "futures-core", - "futures-util", - "hex", - "http", - "http-body-util", - "hyper", - "hyper-named-pipe", - "hyper-util", - "hyperlocal", - "log", - "pin-project-lite", - "serde", - "serde_derive", - "serde_json", - "serde_urlencoded", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tower-service", - "url", - "winapi", -] - -[[package]] -name = "bollard-stubs" -version = "1.52.1-rc.29.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" -dependencies = [ - "serde", - "serde_json", - "serde_repr", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cansi" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bdcae87153686017415ce77e48c53e6818a0a058f0e21b56640d1e944967ef8" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "clap" -version = "4.5.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", - "unicase", - "unicode-width", -] - -[[package]] -name = "clap_derive" -version = "4.5.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "clap_lex" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "lab", - "phf", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "deltae" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "directories" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "euclid" -version = "0.22.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set", - "regex", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - -[[package]] -name = "finl_unicode" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-macro", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-named-pipe" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" -dependencies = [ - "hex", - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", - "winapi", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "libc", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "hyperlocal" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" -dependencies = [ - "hex", - "http-body-util", - "hyper", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "insta" -version = "1.46.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" -dependencies = [ - "console", - "once_cell", - "similar", - "tempfile", -] - -[[package]] -name = "instability" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jiff" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e3d65f018c6ae946ab16e80944b97096ed73c35b221d1c478a6c81d8f57940" -dependencies = [ - "jiff-static", - "jiff-tzdb", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-sys 0.61.2", -] - -[[package]] -name = "jiff-static" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17c2b211d863c7fde02cbea8a3c1a439b98e109286554f2860bdded7ff83818" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "js-sys" -version = "0.3.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "kasuari" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" -dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.18", -] - -[[package]] -name = "lab" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.182" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags 2.11.0", - "libc", -] - -[[package]] -name = "line-clipping" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix", - "winapi", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - -[[package]] -name = "oxker" -version = "0.13.0" -dependencies = [ - "anyhow", - "bollard", - "cansi", - "clap", - "crossterm", - "directories", - "futures-util", - "insta", - "jiff", - "parking_lot", - "ratatui", - "serde", - "serde_json", - "serde_jsonc", - "tokio", - "tokio-util", - "toml", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "ratatui" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-macros", - "ratatui-termwiz", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" -dependencies = [ - "bitflags 2.11.0", - "compact_str", - "hashbrown 0.16.1", - "indoc", - "itertools", - "kasuari", - "lru", - "strum", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" -dependencies = [ - "ratatui-core", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-termwiz" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" -dependencies = [ - "ratatui-core", - "termwiz", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools", - "line-clipping", - "ratatui-core", - "strum", - "time", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_jsonc" -version = "1.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a58154381df481a41b7536101c0daccdaf2426f244334074c4c77b89b6253a7" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tempfile" -version = "3.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" -dependencies = [ - "fastrand", - "getrandom 0.4.1", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom", - "phf", - "phf_codegen", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - -[[package]] -name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" -dependencies = [ - "anyhow", - "base64", - "bitflags 2.11.0", - "fancy-regex", - "filedescriptor", - "finl_unicode", - "fixedbitset", - "hex", - "lazy_static", - "libc", - "log", - "memmem", - "nix", - "num-derive", - "num-traits", - "ordered-float", - "pest", - "pest_derive", - "phf", - "sha2", - "signal-hook", - "siphasher", - "terminfo", - "termios", - "thiserror 1.0.69", - "ucd-trie", - "unicode-segmentation", - "vtparse", - "wezterm-bidi", - "wezterm-blob-leases", - "wezterm-color-types", - "wezterm-dynamic", - "wezterm-input-types", - "winapi", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "1.0.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" -dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.0.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.0.9+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" -dependencies = [ - "winnow", -] - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" -dependencies = [ - "atomic", - "getrandom 0.4.1", - "js-sys", - "rand 0.9.2", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vtparse" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wezterm-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" -dependencies = [ - "log", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-blob-leases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" -dependencies = [ - "getrandom 0.3.4", - "mac_address", - "sha2", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "wezterm-color-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" -dependencies = [ - "csscolorparser", - "deltae", - "lazy_static", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-dynamic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" -dependencies = [ - "log", - "ordered-float", - "strsim", - "thiserror 1.0.69", - "wezterm-dynamic-derive", -] - -[[package]] -name = "wezterm-dynamic-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "wezterm-input-types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" -dependencies = [ - "bitflags 1.3.2", - "euclid", - "lazy_static", - "serde", - "wezterm-dynamic", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 1125a77..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "oxker" -version = "0.13.0" -edition = "2024" -authors = ["Jack Wills "] -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 - - diff --git a/README.md b/README.md index 394121f..9d4b53c 100644 --- a/README.md +++ b/README.md @@ -1,238 +1,47 @@ -

- -
-

oxker

-
A simple tui to view & control docker containers
-

+# Oxker - Go Version -

- Built in Rust, making heavy use of ratatui & Bollard -

+A simple TUI to view & control Docker containers, built with Bubbletea. -

- - - An animated demo of oxker in operation - - - - - link to alternative screenshot - - -

+## 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 crates.io, 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 ghcr.io and Docker Hub, -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 pre-built binaries - -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 script content 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. -
-
-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. -
-
-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). -
-
-Command line arguments will take priority over values from the config file. -
-
-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 & cross-rs - -#### 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 https://forums.raspberrypi.com/viewtopic.php?t=203128 and https://github.com/docker/for-linux/issues/1112 - - -### 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 \ No newline at end of file diff --git a/REVIEW_FINDINGS.md b/REVIEW_FINDINGS.md new file mode 100644 index 0000000..63de67a --- /dev/null +++ b/REVIEW_FINDINGS.md @@ -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 diff --git a/_typos.toml b/_typos.toml deleted file mode 100644 index 372dc95..0000000 --- a/_typos.toml +++ /dev/null @@ -1,2 +0,0 @@ -[default.extend-words] -ratatui = "ratatui" diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..4d7645a --- /dev/null +++ b/build.sh @@ -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 \ No newline at end of file diff --git a/cmd/oxker/cli.go b/cmd/oxker/cli.go new file mode 100644 index 0000000..b961844 --- /dev/null +++ b/cmd/oxker/cli.go @@ -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 +} diff --git a/cmd/oxker/main.go b/cmd/oxker/main.go new file mode 100644 index 0000000..749ec31 --- /dev/null +++ b/cmd/oxker/main.go @@ -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) + } +} diff --git a/cmd/oxker/main_test.go b/cmd/oxker/main_test.go new file mode 100644 index 0000000..fcd9d1f --- /dev/null +++ b/cmd/oxker/main_test.go @@ -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") + } +} \ No newline at end of file diff --git a/containerised/DOCKERHUB_README.md b/containerised/DOCKERHUB_README.md deleted file mode 100644 index 4a0b238..0000000 --- a/containerised/DOCKERHUB_README.md +++ /dev/null @@ -1,30 +0,0 @@ - -

- -

oxker

-
- A simple tui to view & control docker containers -
-

- -

- - - -
- - - link to alternative screenshot - - -

- -## 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 Github repo \ No newline at end of file diff --git a/containerised/Dockerfile b/containerised/Dockerfile deleted file mode 100644 index 0f93266..0000000 --- a/containerised/Dockerfile +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev deleted file mode 100644 index f8315da..0000000 --- a/containerised/Dockerfile_dev +++ /dev/null @@ -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 \ No newline at end of file diff --git a/containerised/target.sh b/containerised/target.sh deleted file mode 100644 index 40ba3d6..0000000 --- a/containerised/target.sh +++ /dev/null @@ -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 diff --git a/create_release.sh b/create_release.sh deleted file mode 100755 index 393ccbf..0000000 --- a/create_release.sh +++ /dev/null @@ -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 '/# CHANGELOG.md for more details" >.github/release-body.md - - # Add subheading with release version and date of release - echo -e "# ${NEW_TAG_WITH_V}\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 diff --git a/docker/Dockerfile.unhealthy b/docker/Dockerfile.unhealthy deleted file mode 100644 index 4a6d10a..0000000 --- a/docker/Dockerfile.unhealthy +++ /dev/null @@ -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 \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 2ef1418..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -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 - - - diff --git a/example_config/example.config.jsonc b/example_config/example.config.jsonc deleted file mode 100644 index db1251f..0000000 --- a/example_config/example.config.jsonc +++ /dev/null @@ -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" - } - } -} \ No newline at end of file diff --git a/example_config/example.config.toml b/example_config/example.config.toml deleted file mode 100644 index 1b1a0c1..0000000 --- a/example_config/example.config.toml +++ /dev/null @@ -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" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0f757aa --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..23c937f --- /dev/null +++ b/go.sum @@ -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= diff --git a/install.sh b/install.sh deleted file mode 100755 index 394c113..0000000 --- a/install.sh +++ /dev/null @@ -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 diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..8033a97 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,3255 @@ +package app + +import ( + "bufio" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + tea "charm.land/bubbletea/v2" + "github.com/charmbracelet/lipgloss" + "github.com/creack/pty" + "github.com/docker/docker/api/types/container" + "github.com/hinshun/vt10x" + "github.com/oxker/oxker/internal/config" + "github.com/oxker/oxker/internal/docker" +) + +// ============================================================ +// TYPES +// ============================================================ + +type AppState int + +const ( + StateLoading AppState = iota + StateReady + StateError +) + +type Panel int + +const ( + PanelContainers Panel = iota + PanelLogs + PanelCommands +) + +type ContainerState int + +const ( + RunningHealthy ContainerState = iota + RunningUnhealthy + Paused + Exited + Dead + Restarting + Removing + Created + Unknown +) + +type SortOrder int + +const ( + SortNone SortOrder = iota + SortAsc + SortDesc +) + +type SortColumn int + +const ( + SortName SortColumn = iota + SortState + SortStatus + SortCPU + SortMemory + SortID + SortImage + SortRX + SortTX +) + +type FilterBy int + +const ( + FilterByName FilterBy = iota + FilterByImage + FilterByStatus + FilterByAll +) + +func (f FilterBy) String() string { + return [...]string{"Name", "Image", "Status", "All"}[f] +} + +type ContainerPorts struct { + IP string + Private int + Public int +} + +type Container struct { + ID string + Name string + Image string + State ContainerState + StateStr string + Status string + Ports []ContainerPorts + CreatedAt int64 + CPUPercent float64 + MemUsage uint64 + MemLimit uint64 + RxBytes uint64 + TxBytes uint64 + CPUHist []float64 + MemHist []uint64 + RxHist []uint64 + TxHist []uint64 + // Per-container log storage + Logs []string + LogScroll int + LogHScroll int + LogsSince string // timestamp for incremental fetching +} + +// ============================================================ +// MESSAGES +// ============================================================ + +type ContainersLoadedMsg struct{ Containers []Container } +type StatsUpdatedMsg struct { + ID string + CPU float64 + MemUsage uint64 + MemLimit uint64 + Rx, Tx uint64 +} +type ContainerActionMsg struct{ Action, ID string; Err error } +type ErrorMsg struct{ Err error } +type TickMsg struct{} +type LogsLoadedMsg struct{ ID string; Lines []string; Since string } +type InspectLoadedMsg struct{ Data string } +type InfoDismissMsg struct{} +type ExecDoneMsg struct{ Err error } +type ConnectCountdownMsg struct{ SecsLeft int } +type ExecOutputMsg struct{} // tick to refresh VTE display +type ExecExitMsg struct{ Err error } + +// ============================================================ +// APP MODEL +// ============================================================ + +type App struct { + // State + State AppState + CmdMgr *docker.Client + Config *config.Config + Width int + Height int + Error error + + // Containers + Containers []Container + SelectedIdx int + SelectedID string // persistent selected container ID + ScrollOffset int + PrevSelectedID string // track selection changes for log save/restore + + // Panels + ActivePanel Panel + + // Logs + LogLines []string + LogScroll int + LogHScroll int + LogHeight int // percentage 5-80 + ShowLogs bool + + // Sorting + SortCol SortColumn + SortOrd SortOrder + + // Filtering + FilterMode bool + FilterText string + FilterBy FilterBy + + // Log Search + SearchMode bool + SearchText string + SearchMatches []int + SearchIdx int + + // Inspect + InspectMode bool + InspectData string + InspectScrollY int + InspectScrollX int + + // Delete confirm + DeleteConfirm bool + DeleteTarget string + + // Help + ShowHelp bool + + // Command selection + CmdSelectedIdx int + + // Stats dedup — prevent duplicate in-flight stats requests + PendingStats map[string]bool + + // Info box (bottom-right, auto-dismiss) + InfoText string + InfoTimeout *time.Timer + + // Loading animation + LoadingIdx int + + // Mouse capture + MouseEnabled bool + + // Docker connect error countdown (auto-exit) + ConnectCountdown int + + // Embedded exec terminal (PTY + VTE) + ExecMode bool + ExecPTY *os.File // PTY master fd + ExecCmd *exec.Cmd // running docker exec process + ExecVT vt10x.Terminal // virtual terminal emulator + ExecMu sync.Mutex // protects ExecVT + ExecDone chan struct{} // closed when exec process exits + ExecContainer string // container name + ExecImage string // container image + ExecID string // container ID short + + // Exec mouse selection + ExecSelecting bool // drag in progress + ExecSelActive bool // selection exists (show highlight) + ExecSelStartR int // start row (in visible area, 0-based) + ExecSelStartC int // start col + ExecSelEndR int // end row + ExecSelEndC int // end col +} + +func New(cfg ...*config.Config) *App { + var c *config.Config + if len(cfg) > 0 && cfg[0] != nil { + c = cfg[0] + } else { + c = config.NewConfig() + } + cli, err := docker.New(c.Host) + + app := &App{ + State: StateLoading, + Config: c, + LogHeight: 75, + ShowLogs: c.ShowLogs, + MouseEnabled: true, + } + + if err != nil { + app.Error = fmt.Errorf("Docker connect failed: %v", err) + app.ConnectCountdown = 5 + } else if pingErr := cli.Ping(); pingErr != nil { + host := c.Host + if host == "" { host = "default socket" } + app.Error = fmt.Errorf("Cannot connect to Docker at %s: %v", host, pingErr) + app.ConnectCountdown = 5 + } else { + app.CmdMgr = cli + } + + return app +} + +func (a *App) Init() tea.Cmd { + if a.ConnectCountdown > 0 { + return tea.Tick(time.Second, func(t time.Time) tea.Msg { + return ConnectCountdownMsg{SecsLeft: a.ConnectCountdown} + }) + } + return tea.Batch(a.loadContainers(), a.tickCmd()) +} + +func (a *App) tickCmd() tea.Cmd { + return tea.Tick(time.Duration(a.Config.DockerIntervalMs)*time.Millisecond, func(t time.Time) tea.Msg { + return TickMsg{} + }) +} + +// quit cancels in-flight Docker calls and returns tea.Quit. +func (a *App) quit() (*App, tea.Cmd) { + if a.CmdMgr != nil { + a.CmdMgr.Cancel() + } + return a, tea.Quit +} + +// ============================================================ +// UPDATE +// ============================================================ + +func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Exec mode: route most messages to exec handler + if a.ExecMode { + return a.updateExecMode(msg) + } + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + a.Width = msg.Width + a.Height = msg.Height + return a, nil + + case tea.KeyPressMsg: + return a.handleKey(msg) + + case tea.MouseClickMsg: + return a.handleMouseClick(msg) + + case tea.MouseWheelMsg: + return a.handleMouseWheel(msg) + + case tea.MouseMotionMsg: + // No-op in normal mode + return a, nil + + case tea.MouseReleaseMsg: + // No-op in normal mode + return a, nil + + case ContainersLoadedMsg: + return a.handleContainersLoaded(msg) + + case StatsUpdatedMsg: + return a.handleStatsUpdated(msg) + + case LogsLoadedMsg: + for i := range a.Containers { + if a.Containers[i].ID == msg.ID { + c := &a.Containers[i] + if msg.Since != "" && len(c.Logs) > 0 { + // Incremental: append new lines, dedup + existing := make(map[string]bool, len(c.Logs)) + for _, l := range c.Logs { + existing[l] = true + } + for _, l := range msg.Lines { + if !existing[l] { + c.Logs = append(c.Logs, l) + } + } + } else { + c.Logs = msg.Lines + } + c.LogsSince = time.Now().UTC().Format(time.RFC3339Nano) + break + } + } + // Sync to active view if this is the selected container + if sel := a.selected(); sel != nil && sel.ID == msg.ID { + // Auto-follow: if we were at/near the bottom, scroll to new bottom + wasAtBottom := a.LogScroll >= len(a.LogLines) - a.logsVisibleRows() - 2 + a.LogLines = sel.Logs + if wasAtBottom || len(a.LogLines) <= a.logsVisibleRows() { + max := len(a.LogLines) - a.logsVisibleRows() + if max < 0 { max = 0 } + a.LogScroll = max + } + } + return a, nil + + case InspectLoadedMsg: + a.InspectData = msg.Data + a.InspectMode = true + a.InspectScrollX = 0 + a.InspectScrollY = 0 + return a, nil + + case ContainerActionMsg: + if msg.Err != nil { + a.Error = msg.Err + } else { + dismissCmd := a.setInfo(fmt.Sprintf("%s: success", msg.Action)) + return a, tea.Batch(a.loadContainers(), dismissCmd) + } + return a, a.loadContainers() + + case ErrorMsg: + a.Error = msg.Err + a.State = StateError + return a, nil + + case TickMsg: + a.LoadingIdx = (a.LoadingIdx + 1) % 10 + cmds := []tea.Cmd{a.tickCmd(), a.loadContainers()} + // Update stats for all running containers + cmds = append(cmds, a.updateAllStats()...) + return a, tea.Batch(cmds...) + + case InfoDismissMsg: + a.InfoText = "" + return a, nil + + case ExecDoneMsg: + if msg.Err != nil { + a.Error = msg.Err + } + return a, a.loadContainers() + + case ConnectCountdownMsg: + a.ConnectCountdown = msg.SecsLeft - 1 + if a.ConnectCountdown <= 0 { + return a.quit() + } + return a, tea.Tick(time.Second, func(t time.Time) tea.Msg { + return ConnectCountdownMsg{SecsLeft: a.ConnectCountdown} + }) + } + + return a, nil +} + +func (a *App) handleContainersLoaded(msg ContainersLoadedMsg) (*App, tea.Cmd) { + // Merge stats from existing containers + oldMap := make(map[string]*Container) + for i := range a.Containers { + oldMap[a.Containers[i].ID] = &a.Containers[i] + } + for i := range msg.Containers { + if old, ok := oldMap[msg.Containers[i].ID]; ok { + msg.Containers[i].CPUPercent = old.CPUPercent + msg.Containers[i].MemUsage = old.MemUsage + msg.Containers[i].MemLimit = old.MemLimit + msg.Containers[i].RxBytes = old.RxBytes + msg.Containers[i].TxBytes = old.TxBytes + msg.Containers[i].CPUHist = old.CPUHist + msg.Containers[i].MemHist = old.MemHist + msg.Containers[i].RxHist = old.RxHist + msg.Containers[i].TxHist = old.TxHist + // Preserve per-container logs + msg.Containers[i].Logs = old.Logs + msg.Containers[i].LogScroll = old.LogScroll + msg.Containers[i].LogHScroll = old.LogHScroll + msg.Containers[i].LogsSince = old.LogsSince + } + } + a.Containers = msg.Containers + a.State = StateReady + // Use persistent SelectedID to restore selection after sort + a.sortContainersWithID(a.SelectedID) + return a, a.loadLogsForSelected() +} + +func (a *App) handleStatsUpdated(msg StatsUpdatedMsg) (*App, tea.Cmd) { + delete(a.PendingStats, msg.ID) + for i := range a.Containers { + if a.Containers[i].ID == msg.ID { + c := &a.Containers[i] + c.CPUPercent = msg.CPU + c.MemUsage = msg.MemUsage + c.MemLimit = msg.MemLimit + c.RxBytes = msg.Rx + c.TxBytes = msg.Tx + // History (max 60 samples) + c.CPUHist = appendMax(c.CPUHist, msg.CPU, 60) + c.MemHist = appendMaxU(c.MemHist, msg.MemUsage, 60) + c.RxHist = appendMaxU(c.RxHist, msg.Rx, 60) + c.TxHist = appendMaxU(c.TxHist, msg.Tx, 60) + break + } + } + return a, nil +} + +// ============================================================ +// KEY HANDLING +// ============================================================ + +func (a *App) handleKey(msg tea.KeyPressMsg) (*App, tea.Cmd) { + // Ctrl+C always quits regardless of mode + if msg.String() == "ctrl+c" { + return a.quit() + } + + // Inspect mode - separate handling + if a.InspectMode { + return a.handleInspectKey(msg) + } + + // Delete confirm + if a.DeleteConfirm { + return a.handleDeleteConfirmKey(msg) + } + + // Error mode - only allow clear/quit + if a.Error != nil { + k := msg.String() + if keyMatch(k, a.Config.Keymap.Clear) || k == "c" || k == "esc" { + a.Error = nil + a.ConnectCountdown = 0 + if a.State == StateError { + a.State = StateReady + } + } else if keyMatch(k, a.Config.Keymap.Quit) || k == "q" { + return a.quit() + } + return a, nil + } + + // Filter mode + if a.FilterMode { + return a.handleFilterKey(msg) + } + + // Search mode + if a.SearchMode { + return a.handleSearchKey(msg) + } + + // Help overlay + if a.ShowHelp { + switch msg.String() { + case "h", "esc", "q": + a.ShowHelp = false + } + return a, nil + } + + key := msg.String() + km := &a.Config.Keymap + + // Keymap-aware dispatch: check config keybindings first, fall back to defaults + switch { + case keyMatch(key, km.Quit) || key == "q": + return a.quit() + + // Panel navigation + case keyMatch(key, km.SelectNextPanel) || key == "tab": + a.nextPanel() + case keyMatch(key, km.SelectPreviousPanel) || key == "shift+tab": + a.prevPanel() + + // Scroll + case keyMatch(key, km.ScrollDown) || key == "j" || key == "down": + a.scrollDown(1) + return a, a.loadLogsForSelected() + case keyMatch(key, km.ScrollUp) || key == "k" || key == "up": + a.scrollUp(1) + return a, a.loadLogsForSelected() + case key == "J": + a.scrollDown(10) + return a, a.loadLogsForSelected() + case key == "K": + a.scrollUp(10) + return a, a.loadLogsForSelected() + case keyMatch(key, km.ScrollStart) || key == "g" || key == "home": + a.scrollHome() + return a, a.loadLogsForSelected() + case keyMatch(key, km.ScrollEnd) || key == "G" || key == "end": + a.scrollEnd() + return a, a.loadLogsForSelected() + case keyMatch(key, km.ScrollForward) || key == "pgdown": + a.scrollDown(a.visibleRows()) + return a, a.loadLogsForSelected() + case keyMatch(key, km.ScrollBack) || key == "pgup": + a.scrollUp(a.visibleRows()) + return a, a.loadLogsForSelected() + + // Sorting + case keyMatch(key, km.SortByName) || key == "1": + a.toggleSort(SortName) + case keyMatch(key, km.SortByState) || key == "2": + a.toggleSort(SortState) + case keyMatch(key, km.SortByStatus) || key == "3": + a.toggleSort(SortStatus) + case keyMatch(key, km.SortByCPU) || key == "4": + a.toggleSort(SortCPU) + case keyMatch(key, km.SortByMemory) || key == "5": + a.toggleSort(SortMemory) + case keyMatch(key, km.SortByID) || key == "6": + a.toggleSort(SortID) + case keyMatch(key, km.SortByImage) || key == "7": + a.toggleSort(SortImage) + case keyMatch(key, km.SortByRX) || key == "8": + a.toggleSort(SortRX) + case keyMatch(key, km.SortByTX) || key == "9": + a.toggleSort(SortTX) + case keyMatch(key, km.SortReset) || key == "0": + a.SortOrd = SortNone + a.sortContainers() + + // Filter + case keyMatch(key, km.FilterMode) || key == "/": + a.FilterMode = true + a.FilterText = "" + + // Log search + case keyMatch(key, km.LogSearchMode) || key == "#": + a.SearchMode = true + a.SearchText = "" + a.SearchMatches = nil + + // Log panel controls + case keyMatch(key, km.LogSectionToggle) || key == "l": + a.ShowLogs = !a.ShowLogs + case keyMatch(key, km.LogSectionHeightIncrease) || key == "]": + if a.LogHeight < 80 { + a.LogHeight += 5 + } + case keyMatch(key, km.LogSectionHeightDecrease) || key == "[": + if a.LogHeight > 5 { + a.LogHeight -= 5 + } + + // Log horizontal scroll + case key == "right": + if a.ActivePanel == PanelLogs { + a.LogHScroll += 10 + } + case key == "left": + if a.ActivePanel == PanelLogs { + a.LogHScroll -= 10 + if a.LogHScroll < 0 { + a.LogHScroll = 0 + } + } + + // Container actions + case key == "enter": + return a, a.execSelectedCommand() + case key == "s": + return a, a.doAction((*docker.Client).StartContainer, "start") + case key == "x": + return a, a.doAction((*docker.Client).StopContainer, "stop") + case key == "r": + return a, a.doAction((*docker.Client).RestartContainer, "restart") + case key == "d": + a.confirmDelete() + case key == "p": + return a, a.doAction((*docker.Client).PauseContainer, "pause") + case key == "u": + return a, a.doAction((*docker.Client).UnpauseContainer, "unpause") + + // Docker exec + case keyMatch(key, km.Exec) || key == "e": + return a, a.execIntoContainer() + + // Inspect + case keyMatch(key, km.Inspect) || key == "i": + return a, a.inspectSelected() + + // Save logs + case keyMatch(key, km.SaveLogs) || key == "ctrl+s" || key == "S": + return a, a.saveLogs() + + // Help + case keyMatch(key, km.ToggleHelp) || key == "h": + a.ShowHelp = true + + // Force redraw + case keyMatch(key, km.ForceRedraw) || key == "f": + return a, tea.ClearScreen + + // Toggle mouse capture (controlled via View return) + case keyMatch(key, km.ToggleMouseCapture) || key == "m": + a.MouseEnabled = !a.MouseEnabled + + // Clear + case keyMatch(key, km.Clear) || key == "esc": + a.ShowHelp = false + } + + return a, nil +} + +func (a *App) handleInspectKey(msg tea.KeyPressMsg) (*App, tea.Cmd) { + switch msg.String() { + case "esc", "i", "q": + a.InspectMode = false + case "j", "down": + a.InspectScrollY++ + a.clampInspectScroll() + case "k", "up": + if a.InspectScrollY > 0 { + a.InspectScrollY-- + } + case "l", "right": + a.InspectScrollX += 5 + case "h", "left": + a.InspectScrollX -= 5 + if a.InspectScrollX < 0 { + a.InspectScrollX = 0 + } + case "g", "home": + a.InspectScrollY = 0 + a.InspectScrollX = 0 + case "G", "end": + lines := strings.Count(a.InspectData, "\n") + a.InspectScrollY = lines + a.clampInspectScroll() + } + return a, nil +} + +func (a *App) handleDeleteConfirmKey(msg tea.KeyPressMsg) (*App, tea.Cmd) { + k := msg.String() + km := &a.Config.Keymap + switch { + case keyMatch(k, km.DeleteConfirm) || k == "y" || k == "enter": + a.DeleteConfirm = false + id := a.DeleteTarget + if id == "" || a.CmdMgr == nil { + return a, nil + } + return a, func() tea.Msg { + err := a.CmdMgr.DeleteContainer(id, true) + return ContainerActionMsg{Action: "delete", ID: id, Err: err} + } + case keyMatch(k, km.DeleteDeny) || k == "n" || k == "esc": + a.DeleteConfirm = false + a.DeleteTarget = "" + } + return a, nil +} + +func (a *App) handleFilterKey(msg tea.KeyPressMsg) (*App, tea.Cmd) { + switch msg.String() { + case "esc": + a.FilterMode = false + a.FilterText = "" + case "enter", "/": + a.FilterMode = false + case "backspace": + if len(a.FilterText) > 0 { + a.FilterText = a.FilterText[:len(a.FilterText)-1] + } + case "ctrl+u": + a.FilterText = "" + case "left": + if a.FilterBy > 0 { + a.FilterBy-- + } + case "right": + if a.FilterBy < FilterByAll { + a.FilterBy++ + } + default: + if msg.Text != "" { + a.FilterText += msg.Text + } + } + return a, nil +} + +func (a *App) handleSearchKey(msg tea.KeyPressMsg) (*App, tea.Cmd) { + switch msg.String() { + case "esc": + a.SearchMode = false + a.SearchText = "" + a.SearchMatches = nil + case "enter", "#": + a.SearchMode = false + case "backspace": + if len(a.SearchText) > 0 { + a.SearchText = a.SearchText[:len(a.SearchText)-1] + a.updateSearchMatches() + } + case "ctrl+n", "down": + if len(a.SearchMatches) > 0 { + a.SearchIdx = (a.SearchIdx + 1) % len(a.SearchMatches) + a.LogScroll = a.SearchMatches[a.SearchIdx] + } + case "ctrl+p", "up": + if len(a.SearchMatches) > 0 { + a.SearchIdx-- + if a.SearchIdx < 0 { + a.SearchIdx = len(a.SearchMatches) - 1 + } + a.LogScroll = a.SearchMatches[a.SearchIdx] + } + case "right": + a.LogHScroll += 10 + case "left": + a.LogHScroll -= 10 + if a.LogHScroll < 0 { a.LogHScroll = 0 } + default: + if msg.Text != "" { + a.SearchText += msg.Text + a.updateSearchMatches() + } + } + return a, nil +} + +// ============================================================ +// MOUSE HANDLING +// ============================================================ + +func (a *App) handleMouseWheel(msg tea.MouseWheelMsg) (*App, tea.Cmd) { + // Block mouse in overlay modes + if a.DeleteConfirm || a.ShowHelp || a.FilterMode || a.SearchMode || a.Error != nil { + return a, nil + } + + // Inspect mode: route scroll to inspect panel + if a.InspectMode { + if msg.Button == tea.MouseWheelUp { + if a.InspectScrollY > 0 { + a.InspectScrollY-- + } + } else if msg.Button == tea.MouseWheelDown { + a.InspectScrollY++ + } + return a, nil + } + + if msg.Button == tea.MouseWheelUp { + a.scrollUp(1) + return a, a.loadLogsForSelected() + } else if msg.Button == tea.MouseWheelDown { + a.scrollDown(1) + return a, a.loadLogsForSelected() + } + return a, nil +} + +func (a *App) handleMouseClick(msg tea.MouseClickMsg) (*App, tea.Cmd) { + // Block mouse in overlay modes + if a.DeleteConfirm || a.ShowHelp || a.FilterMode || a.SearchMode || a.Error != nil { + return a, nil + } + if a.InspectMode { + return a, nil + } + x, y := msg.X, msg.Y + + // Header row (y == 0) → sort by column + if y == 0 { + col := a.headerColumnAt(x) + if col >= 0 { + a.toggleSort(SortColumn(col)) + } + return a, nil + } + + // Container area + containerH := a.containerAreaHeight() + cmdW := a.Width * 10 / 100 + if cmdW < 12 { cmdW = 12 } + tableW := a.Width - cmdW - 4 + + if y >= 1 && y <= containerH { + if x < tableW+2 { + // Click in container table → select container + row := y - 3 + a.ScrollOffset // 3 = header + border + title + if row >= 0 && row < len(a.filtered()) { + a.SelectedIdx = row + a.CmdSelectedIdx = 0 + a.syncSelectedID() + a.ensureVisible() + return a, a.loadLogsForSelected() + } + } else { + // Click in commands panel + a.ActivePanel = PanelCommands + } + return a, nil + } + + // Logs area + logsStart := 1 + containerH + logsEnd := logsStart + a.logsAreaHeight() + if y >= logsStart && y < logsEnd { + a.ActivePanel = PanelLogs + return a, nil + } + + return a, nil +} + +func (a *App) headerColumnAt(x int) int { + cw := a.colWidths() + cols := []int{3, cw.name, cw.state, cw.status, cw.cpu, cw.mem, cw.id, cw.image, cw.rx, cw.tx} + pos := 0 + for i, w := range cols { + pos += w + if x < pos { + if i == 0 { + return -1 // indicator column + } + return i - 1 // SortColumn index + } + } + return -1 +} + +// ============================================================ +// NAVIGATION & SCROLL +// ============================================================ + +func (a *App) nextPanel() { + if a.ShowLogs { + a.ActivePanel = Panel((int(a.ActivePanel) + 1) % 3) + } else { + if a.ActivePanel == PanelContainers { + a.ActivePanel = PanelCommands + } else { + a.ActivePanel = PanelContainers + } + } +} + +func (a *App) prevPanel() { + if a.ShowLogs { + p := int(a.ActivePanel) - 1 + if p < 0 { + p = 2 + } + a.ActivePanel = Panel(p) + } else { + if a.ActivePanel == PanelContainers { + a.ActivePanel = PanelCommands + } else { + a.ActivePanel = PanelContainers + } + } +} + +func (a *App) scrollDown(n int) { + if a.ActivePanel == PanelCommands { + c := a.selected() + if c == nil { return } + cmds := commandsForState(c.State) + a.CmdSelectedIdx += n + if a.CmdSelectedIdx >= len(cmds) { + a.CmdSelectedIdx = len(cmds) - 1 + } + if a.CmdSelectedIdx < 0 { a.CmdSelectedIdx = 0 } + return + } + if a.ActivePanel == PanelLogs { + a.LogScroll += n + max := len(a.LogLines) - a.logsVisibleRows() + if max < 0 { max = 0 } + if a.LogScroll > max { a.LogScroll = max } + } else { + cs := a.filtered() + a.SelectedIdx += n + if a.SelectedIdx >= len(cs) { + a.SelectedIdx = len(cs) - 1 + } + if a.SelectedIdx < 0 { a.SelectedIdx = 0 } + a.CmdSelectedIdx = 0 + a.syncSelectedID() + a.ensureVisible() + } +} + +func (a *App) scrollUp(n int) { + if a.ActivePanel == PanelCommands { + a.CmdSelectedIdx -= n + if a.CmdSelectedIdx < 0 { a.CmdSelectedIdx = 0 } + return + } + if a.ActivePanel == PanelLogs { + a.LogScroll -= n + if a.LogScroll < 0 { a.LogScroll = 0 } + } else { + a.SelectedIdx -= n + if a.SelectedIdx < 0 { a.SelectedIdx = 0 } + a.CmdSelectedIdx = 0 + a.syncSelectedID() + a.ensureVisible() + } +} + +func (a *App) scrollHome() { + if a.ActivePanel == PanelLogs { + a.LogScroll = 0 + } else { + a.SelectedIdx = 0 + a.ScrollOffset = 0 + a.syncSelectedID() + } +} + +func (a *App) scrollEnd() { + if a.ActivePanel == PanelLogs { + max := len(a.LogLines) - a.logsVisibleRows() + if max < 0 { max = 0 } + a.LogScroll = max + } else { + cs := a.filtered() + if len(cs) > 0 { + a.SelectedIdx = len(cs) - 1 + a.syncSelectedID() + a.ensureVisible() + } + } +} + +func (a *App) ensureVisible() { + vis := a.containerVisibleRows() + if a.SelectedIdx < a.ScrollOffset { + a.ScrollOffset = a.SelectedIdx + } + if a.SelectedIdx >= a.ScrollOffset+vis { + a.ScrollOffset = a.SelectedIdx - vis + 1 + } +} + +func (a *App) visibleRows() int { + return a.containerVisibleRows() +} + +func (a *App) clampInspectScroll() { + total := strings.Count(a.InspectData, "\n") + 1 + vis := a.Height - 4 + if vis < 1 { vis = 1 } + max := total - vis + if max < 0 { max = 0 } + if a.InspectScrollY > max { a.InspectScrollY = max } + if a.InspectScrollY < 0 { a.InspectScrollY = 0 } +} + +// ============================================================ +// SORTING +// ============================================================ + +func (a *App) toggleSort(col SortColumn) { + if a.SortCol == col { + switch a.SortOrd { + case SortNone: + a.SortOrd = SortAsc + case SortAsc: + a.SortOrd = SortDesc + case SortDesc: + a.SortOrd = SortNone + } + } else { + a.SortCol = col + a.SortOrd = SortAsc + } + a.sortContainers() +} + +func (a *App) sortContainers() { + var selectedID string + if sel := a.selected(); sel != nil { + selectedID = sel.ID + } + a.sortContainersWithID(selectedID) +} + +func (a *App) sortContainersWithID(selectedID string) { + cs := a.Containers + less := func(i, j int) bool { + var cmp int + if a.SortOrd == SortNone { + // Default sort: by CreatedAt, Name as tiebreaker + cmp = compareInt64(cs[i].CreatedAt, cs[j].CreatedAt) + } else { + switch a.SortCol { + case SortName: + cmp = strings.Compare(strings.ToLower(cs[i].Name), strings.ToLower(cs[j].Name)) + case SortState: + cmp = int(stateOrder(cs[i].State)) - int(stateOrder(cs[j].State)) + case SortStatus: + cmp = strings.Compare(cs[i].Status, cs[j].Status) + case SortCPU: + cmp = compareFloat(cs[i].CPUPercent, cs[j].CPUPercent) + case SortMemory: + cmp = compareUint(cs[i].MemUsage, cs[j].MemUsage) + case SortID: + cmp = strings.Compare(cs[i].ID, cs[j].ID) + case SortImage: + cmp = strings.Compare(strings.ToLower(cs[i].Image), strings.ToLower(cs[j].Image)) + case SortRX: + cmp = compareUint(cs[i].RxBytes, cs[j].RxBytes) + case SortTX: + cmp = compareUint(cs[i].TxBytes, cs[j].TxBytes) + } + } + // Tiebreaker: sort by name + if cmp == 0 { + cmp = strings.Compare(strings.ToLower(cs[i].Name), strings.ToLower(cs[j].Name)) + } + if a.SortOrd == SortDesc { + return cmp > 0 + } + return cmp < 0 + } + // Simple insertion sort (few items, stable) + for i := 1; i < len(cs); i++ { + for j := i; j > 0 && less(j, j-1); j-- { + cs[j], cs[j-1] = cs[j-1], cs[j] + } + } + // Restore selection by ID (H8) + if selectedID != "" { + for i, c := range cs { + if c.ID == selectedID { + a.SelectedIdx = i + break + } + } + } + // Clamp and sync SelectedID + if a.SelectedIdx >= len(cs) { + a.SelectedIdx = len(cs) - 1 + } + if a.SelectedIdx < 0 { + a.SelectedIdx = 0 + } + a.syncSelectedID() +} + +// stateOrder returns semantic sort order matching Rust (H10) +func stateOrder(s ContainerState) int { + switch s { + case RunningHealthy: return 0 + case RunningUnhealthy: return 1 + case Paused: return 2 + case Restarting: return 3 + case Removing: return 4 + case Exited: return 5 + case Dead: return 6 + case Created: return 7 + case Unknown: return 8 + default: return 9 + } +} + +func compareInt64(a, b int64) int { + if a < b { return -1 } + if a > b { return 1 } + return 0 +} + +// ============================================================ +// SEARCH +// ============================================================ + +func (a *App) updateSearchMatches() { + a.SearchMatches = nil + a.SearchIdx = 0 + if a.SearchText == "" { + return + } + caseSensitive := a.Config != nil && a.Config.LogSearchCaseSensitive + term := a.SearchText + if !caseSensitive { + term = strings.ToLower(term) + } + for i, line := range a.LogLines { + haystack := line + if !caseSensitive { + haystack = strings.ToLower(haystack) + } + if strings.Contains(haystack, term) { + a.SearchMatches = append(a.SearchMatches, i) + } + } +} + +// ============================================================ +// CONTAINER ACTIONS +// ============================================================ + +func (a *App) selected() *Container { + cs := a.filtered() + if a.SelectedIdx >= 0 && a.SelectedIdx < len(cs) { + id := cs[a.SelectedIdx].ID + for i := range a.Containers { + if a.Containers[i].ID == id { + return &a.Containers[i] + } + } + } + return nil +} + +// syncSelectedID updates SelectedID to match current SelectedIdx +func (a *App) syncSelectedID() { + cs := a.filtered() + if a.SelectedIdx >= 0 && a.SelectedIdx < len(cs) { + a.SelectedID = cs[a.SelectedIdx].ID + } +} + +// syncLogSelection saves scroll state to old container and restores from new one +func (a *App) syncLogSelection() { + sel := a.selected() + newID := "" + if sel != nil { newID = sel.ID } + + if newID == a.PrevSelectedID { return } + + // Save scroll position to previous container + if a.PrevSelectedID != "" { + for i := range a.Containers { + if a.Containers[i].ID == a.PrevSelectedID { + a.Containers[i].LogScroll = a.LogScroll + a.Containers[i].LogHScroll = a.LogHScroll + break + } + } + } + + // Restore from new container + if sel != nil { + a.LogLines = sel.Logs + a.LogScroll = sel.LogScroll + a.LogHScroll = sel.LogHScroll + } else { + a.LogLines = nil + a.LogScroll = 0 + a.LogHScroll = 0 + } + a.PrevSelectedID = newID +} + +func (a *App) doAction(fn func(*docker.Client, string) error, action string) tea.Cmd { + c := a.selected() + if c == nil || a.CmdMgr == nil { return nil } + id := c.ID + mgr := a.CmdMgr + return func() tea.Msg { + err := fn(mgr, id) + return ContainerActionMsg{Action: action, ID: id, Err: err} + } +} + +func (a *App) confirmDelete() { + c := a.selected() + if c == nil { return } + a.DeleteConfirm = true + a.DeleteTarget = c.ID +} + +func (a *App) execSelectedCommand() tea.Cmd { + c := a.selected() + if c == nil { return nil } + cmds := commandsForState(c.State) + if len(cmds) == 0 { return nil } + idx := a.CmdSelectedIdx + if idx >= len(cmds) { idx = 0 } + cmd := cmds[idx] + switch cmd { + case "start": + return a.doAction((*docker.Client).StartContainer, "start") + case "stop": + return a.doAction((*docker.Client).StopContainer, "stop") + case "pause": + return a.doAction((*docker.Client).PauseContainer, "pause") + case "resume": + return a.doAction((*docker.Client).UnpauseContainer, "resume") + case "restart": + return a.doAction((*docker.Client).RestartContainer, "restart") + case "delete": + a.confirmDelete() + } + return nil +} + +func (a *App) inspectSelected() tea.Cmd { + c := a.selected() + if c == nil || a.CmdMgr == nil { return nil } + id := c.ID + mgr := a.CmdMgr + return func() tea.Msg { + details, err := mgr.InspectContainer(id) + if err != nil { + return ErrorMsg{Err: err} + } + data, _ := json.MarshalIndent(details, "", " ") + return InspectLoadedMsg{Data: string(data)} + } +} + +func (a *App) saveLogs() tea.Cmd { + c := a.selected() + if c == nil { return nil } + name := c.Name + lines := a.LogLines + saveDir := a.Config.DirSave + return func() tea.Msg { + filename := fmt.Sprintf("oxker_%s_%s.log", name, time.Now().Format("20060102_150405")) + if saveDir != "" { + filename = filepath.Join(saveDir, filename) + } + var sb strings.Builder + for _, l := range lines { + sb.WriteString(l + "\n") + } + if err := os.WriteFile(filename, []byte(sb.String()), 0644); err != nil { + return ErrorMsg{Err: err} + } + return ContainerActionMsg{Action: fmt.Sprintf("saved to %s", filename), Err: nil} + } +} + +func (a *App) execIntoContainer() tea.Cmd { + c := a.selected() + if c == nil { + return nil + } + if c.State != RunningHealthy && c.State != RunningUnhealthy { + return a.setInfo("can only exec into running containers") + } + + id := c.ID + shortID := id + if len(shortID) > 12 { shortID = shortID[:12] } + + w, h := a.execPanelSize() + cmd := exec.Command("docker", "exec", "-it", id, "sh") + + // Start with PTY + ptmx, err := pty.Start(cmd) + if err != nil { + return a.setInfo(fmt.Sprintf("exec failed: %v", err)) + } + _ = pty.Setsize(ptmx, &pty.Winsize{Rows: uint16(h), Cols: uint16(w)}) + + // Create VTE with matching size + vt := vt10x.New(vt10x.WithSize(w, h), vt10x.WithWriter(ptmx)) + + a.ExecMode = true + a.ExecPTY = ptmx + a.ExecCmd = cmd + a.ExecVT = vt + a.ExecDone = make(chan struct{}) + a.ExecContainer = c.Name + a.ExecImage = c.Image + a.ExecID = shortID + a.ExecSelActive = false + a.ExecSelecting = false + + // Start VTE parser in background — it reads PTY and updates the screen buffer. + // When PTY closes (shell exits), signal via ExecDone. + done := a.ExecDone + go func() { + defer close(done) + br := bufio.NewReader(ptmx) + for { + if err := vt.Parse(br); err != nil { + return + } + } + // Also wait for the process to finish + }() + + // Tick to refresh the exec view + return a.execTick() +} + +func (a *App) execTick() tea.Cmd { + return tea.Tick(50*time.Millisecond, func(t time.Time) tea.Msg { + return ExecOutputMsg{} + }) +} + +func (a *App) execPanelSize() (w, h int) { + w = a.Width * 70 / 100 + if w < 40 { w = 40 } + w -= 2 // subtract border columns + h = a.Height * 70 / 100 + if h < 10 { h = 10 } + h -= 2 // subtract border rows (top + bottom) + return +} + +func (a *App) execClose() { + if a.ExecPTY != nil { + a.ExecPTY.Close() + a.ExecPTY = nil + } + if a.ExecCmd != nil && a.ExecCmd.Process != nil { + a.ExecCmd.Process.Kill() + a.ExecCmd.Wait() + a.ExecCmd = nil + } + a.ExecVT = nil + a.ExecDone = nil + a.ExecMode = false +} + +func (a *App) execWriteKey(msg tea.KeyPressMsg) { + if a.ExecPTY == nil { return } + + var b []byte + + // Check for ctrl+key combinations first + if msg.Mod&tea.ModCtrl != 0 { + switch msg.Code { + case 'c': + b = []byte{3} + case 'd': + b = []byte{4} + case 'z': + b = []byte{26} + case 'l': + b = []byte{12} + case 'a': + b = []byte{1} + case 'e': + b = []byte{5} + case 'u': + b = []byte{21} + case 'k': + b = []byte{11} + case 'w': + b = []byte{23} + } + } else { + switch msg.Code { + case tea.KeyEnter: + b = []byte{'\r'} + case tea.KeyTab: + b = []byte{'\t'} + case tea.KeyBackspace: + b = []byte{127} + case tea.KeyEscape: + b = []byte{27} + case tea.KeyUp: + b = []byte{27, '[', 'A'} + case tea.KeyDown: + b = []byte{27, '[', 'B'} + case tea.KeyRight: + b = []byte{27, '[', 'C'} + case tea.KeyLeft: + b = []byte{27, '[', 'D'} + case tea.KeySpace: + b = []byte{' '} + default: + // Regular character input + if msg.Text != "" { + b = []byte(msg.Text) + } else if msg.Code > 0 && msg.Code < 128 { + b = []byte{byte(msg.Code)} + } + } + } + if len(b) > 0 { + a.ExecPTY.Write(b) + } +} + +// execPanelOrigin returns the top-left screen position of the content area +func (a *App) execPanelOrigin() (x, y int) { + w, h := a.execPanelSize() + panelW := w + 2 + panelH := h + 2 + x = (a.Width - panelW) / 2 + y = (a.Height - panelH) / 2 + x += 1 + y += 1 + return +} + +// execMouseToContent maps screen mouse position to VTE row/col +func (a *App) execMouseToContent(mx, my int) (row, col int) { + ox, oy := a.execPanelOrigin() + w, h := a.execPanelSize() + col = mx - ox + row = my - oy + if col < 0 || col >= w || row < 0 || row >= h { + return -1, -1 + } + return row, col +} + +// execGetVisibleLines reads the VTE screen buffer into strings +func (a *App) execGetVisibleLines() []string { + if a.ExecVT == nil { + return nil + } + a.ExecVT.Lock() + defer a.ExecVT.Unlock() + cols, rows := a.ExecVT.Size() + lines := make([]string, rows) + for y := 0; y < rows; y++ { + var sb strings.Builder + for x := 0; x < cols; x++ { + g := a.ExecVT.Cell(x, y) + if g.Char == 0 { + sb.WriteRune(' ') + } else { + sb.WriteRune(g.Char) + } + } + lines[y] = strings.TrimRight(sb.String(), " ") + } + return lines +} + +// execSelectedText extracts text from the selection range +func (a *App) execSelectedText(visible []string, w int) string { + if !a.ExecSelActive { + return "" + } + sr, sc, er, ec := a.ExecSelStartR, a.ExecSelStartC, a.ExecSelEndR, a.ExecSelEndC + if sr > er || (sr == er && sc > ec) { + sr, sc, er, ec = er, ec, sr, sc + } + + var sb strings.Builder + for r := sr; r <= er && r < len(visible); r++ { + runes := []rune(visible[r]) + if len(runes) > w { + runes = runes[:w] + } + cStart := 0 + cEnd := len(runes) + if r == sr { cStart = sc } + if r == er { cEnd = ec + 1 } + if cStart > len(runes) { cStart = len(runes) } + if cEnd > len(runes) { cEnd = len(runes) } + if cStart < cEnd { + sb.WriteString(string(runes[cStart:cEnd])) + } + if r < er { + sb.WriteString("\n") + } + } + return sb.String() +} + +// execCopySelection copies the mouse-selected text to clipboard +func (a *App) execCopySelection() tea.Cmd { + w, _ := a.execPanelSize() + visible := a.execGetVisibleLines() + text := a.execSelectedText(visible, w) + if text == "" { + return nil + } + return a.copyToClipboard(text) +} + +// copyToClipboard sends text to system clipboard +func (a *App) copyToClipboard(text string) tea.Cmd { + return func() tea.Msg { + // Try native clipboard tools first + for _, cmd := range [][]string{ + {"wl-copy"}, + {"xclip", "-selection", "clipboard"}, + {"xsel", "--clipboard", "--input"}, + } { + c := exec.Command(cmd[0], cmd[1:]...) + c.Stdin = strings.NewReader(text) + if err := c.Run(); err == nil { + return ContainerActionMsg{Action: "copied to clipboard"} + } + } + // Fallback: OSC 52 escape sequence (works in most modern terminals) + encoded := base64.StdEncoding.EncodeToString([]byte(text)) + fmt.Fprintf(os.Stderr, "\x1b]52;c;%s\x07", encoded) + return ContainerActionMsg{Action: "copied to clipboard (OSC 52)"} + } +} + +func (a *App) updateExecMode(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case ContainerActionMsg: + return a, a.setInfo(msg.Action) + + case InfoDismissMsg: + a.InfoText = "" + return a, nil + + case ExecOutputMsg: + // VTE parses in background; just keep ticking to refresh the view + if a.ExecMode { + // Check if the exec process has exited + select { + case <-a.ExecDone: + a.execClose() + return a, nil + default: + } + return a, a.execTick() + } + return a, nil + + case ExecExitMsg: + a.execClose() + return a, nil + + case tea.WindowSizeMsg: + a.Width = msg.Width + a.Height = msg.Height + // Resize VTE + PTY to match new panel size + if a.ExecVT != nil && a.ExecPTY != nil { + w, h := a.execPanelSize() + a.ExecVT.Lock() + a.ExecVT.Resize(w, h) + a.ExecVT.Unlock() + _ = pty.Setsize(a.ExecPTY, &pty.Winsize{Rows: uint16(h), Cols: uint16(w)}) + } + return a, nil + + case tea.KeyPressMsg: + // Clear selection on any keypress + a.ExecSelActive = false + a.ExecSelecting = false + // Ctrl+Q exits exec mode + if msg.Code == 'q' && msg.Mod&tea.ModCtrl != 0 { + a.execClose() + return a, nil + } + // Forward all keys directly to PTY + a.execWriteKey(msg) + return a, nil + + case tea.MouseClickMsg: + if msg.Button == tea.MouseLeft { + r, c := a.execMouseToContent(msg.X, msg.Y) + if r >= 0 { + a.ExecSelecting = true + a.ExecSelActive = true + a.ExecSelStartR = r + a.ExecSelStartC = c + a.ExecSelEndR = r + a.ExecSelEndC = c + } + } + return a, nil + + case tea.MouseMotionMsg: + if a.ExecSelecting { + r, c := a.execMouseToContent(msg.X, msg.Y) + if r >= 0 { + a.ExecSelEndR = r + a.ExecSelEndC = c + } + } + return a, nil + + case tea.MouseReleaseMsg: + if a.ExecSelecting { + a.ExecSelecting = false + r, c := a.execMouseToContent(msg.X, msg.Y) + if r >= 0 { + a.ExecSelEndR = r + a.ExecSelEndC = c + } + if a.ExecSelStartR == a.ExecSelEndR && a.ExecSelStartC == a.ExecSelEndC { + a.ExecSelActive = false + } else { + return a, a.execCopySelection() + } + } + return a, nil + } + return a, nil +} + +func (a *App) viewExec() string { + w, h := a.execPanelSize() + + // Title bar + title := fmt.Sprintf(" exec: %s (%s) [%s] ", a.ExecContainer, a.ExecImage, a.ExecID) + hint := " Ctrl+Q to exit " + + // Styles + borderColor := lipgloss.NewStyle().Foreground(cGreen).Background(cMantle) + titleStyle := lipgloss.NewStyle().Foreground(cPeach).Background(cMantle).Bold(true) + hintStyle := lipgloss.NewStyle().Foreground(cOverlay0).Background(cMantle) + lineStyle := lipgloss.NewStyle().Foreground(cText).Background(cBase) + cursorStyle := lipgloss.NewStyle().Foreground(cBase).Background(cText) + selStyle := lipgloss.NewStyle().Foreground(cBase).Background(cBlue) + + // Top border + titleLen := len([]rune(title)) + hintLen := len([]rune(hint)) + topW := w + 2 + topAvail := topW - 2 - titleLen + if topAvail < 0 { topAvail = 0 } + topBorder := borderColor.Render("╭") + + borderColor.Render("─") + + titleStyle.Render(title) + + borderColor.Render(strings.Repeat("─", max(0, topAvail-1))) + + borderColor.Render("╮") + + // Bottom border + botAvail := topW - 2 - hintLen + if botAvail < 0 { botAvail = 0 } + botBorder := borderColor.Render("╰") + + borderColor.Render("─") + + hintStyle.Render(hint) + + borderColor.Render(strings.Repeat("─", max(0, botAvail-1))) + + borderColor.Render("╯") + + // Read VTE screen buffer + var curX, curY int + visible := make([]string, h) + if a.ExecVT != nil { + a.ExecVT.Lock() + cur := a.ExecVT.Cursor() + curX, curY = cur.X, cur.Y + for y := 0; y < h; y++ { + var sb strings.Builder + for x := 0; x < w; x++ { + g := a.ExecVT.Cell(x, y) + if g.Char == 0 { + sb.WriteRune(' ') + } else { + sb.WriteRune(g.Char) + } + } + visible[y] = sb.String() + } + a.ExecVT.Unlock() + } + + // Normalize selection bounds + selSR, selSC, selER, selEC := a.ExecSelStartR, a.ExecSelStartC, a.ExecSelEndR, a.ExecSelEndC + if selSR > selER || (selSR == selER && selSC > selEC) { + selSR, selSC, selER, selEC = selER, selEC, selSR, selSC + } + + // Render content lines from VTE buffer + var content strings.Builder + for y := 0; y < h; y++ { + var line string + if y < len(visible) { + line = visible[y] + } + runes := []rune(line) + // Pad to width + for len(runes) < w { + runes = append(runes, ' ') + } + if len(runes) > w { + runes = runes[:w] + } + + // Check if this line has selection or cursor + hasSel := a.ExecSelActive && y >= selSR && y <= selER + hasCursor := y == curY + + if !hasSel && !hasCursor { + // Fast path: render entire line at once + content.WriteString(borderColor.Render("│") + lineStyle.Render(string(runes)) + borderColor.Render("│") + "\n") + } else { + // Char-by-char for cursor and selection + var sb strings.Builder + for x := 0; x < w; x++ { + ch := string(runes[x]) + isCursor := hasCursor && x == curX + inSel := hasSel + if inSel { + colStart := 0 + colEnd := w + if y == selSR { colStart = selSC } + if y == selER { colEnd = selEC + 1 } + inSel = x >= colStart && x < colEnd + } + + if inSel { + sb.WriteString(selStyle.Render(ch)) + } else if isCursor { + sb.WriteString(cursorStyle.Render(ch)) + } else { + sb.WriteString(lineStyle.Render(ch)) + } + } + content.WriteString(borderColor.Render("│") + sb.String() + borderColor.Render("│") + "\n") + } + } + + panel := topBorder + "\n" + content.String() + botBorder + bg := a.viewNormal() + return placeOverlay(bg, panel, lipgloss.Center, lipgloss.Center, a.Width, a.Height) +} + +var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;?]*[a-zA-Z]`) + +func stripAnsi(s string) string { + return ansiRegex.ReplaceAllString(s, "") +} + +func (a *App) setInfo(text string) tea.Cmd { + a.InfoText = text + return tea.Tick(4*time.Second, func(t time.Time) tea.Msg { + return InfoDismissMsg{} + }) +} + +// ============================================================ +// DATA LOADING +// ============================================================ + +func (a *App) loadContainers() tea.Cmd { + mgr := a.CmdMgr + showSelf := a.Config.ShowSelf + return func() tea.Msg { + if mgr == nil { + return ErrorMsg{Err: fmt.Errorf("docker not connected")} + } + containers, err := mgr.ListContainers(true) + if err != nil { + return ErrorMsg{Err: err} + } + result := make([]Container, 0, len(containers)) + for _, c := range containers { + name := "" + if len(c.Names) > 0 { + name = strings.TrimPrefix(c.Names[0], "/") + } + // Skip self (oxker container) unless ShowSelf is set + if !showSelf && strings.Contains(c.Image, "oxker") { + continue + } + result = append(result, Container{ + ID: c.ID, + Name: name, + Image: c.Image, + State: mapState(c.State, c.Status), + StateStr: c.State, + Status: c.Status, + Ports: mapPorts(c.Ports), + CreatedAt: c.Created, + }) + } + return ContainersLoadedMsg{Containers: result} + } +} + +func (a *App) updateAllStats() []tea.Cmd { + if a.CmdMgr == nil { return nil } + if a.PendingStats == nil { a.PendingStats = make(map[string]bool) } + var cmds []tea.Cmd + mgr := a.CmdMgr + for _, c := range a.Containers { + if c.State != RunningHealthy && c.State != RunningUnhealthy && c.State != Paused { + continue + } + id := c.ID + if a.PendingStats[id] { + continue // already in-flight + } + a.PendingStats[id] = true + cmds = append(cmds, func() tea.Msg { + stats, err := mgr.ContainerStats(id) + if err != nil { + return nil // silently ignore stats errors + } + cpu := calculateCPU(stats) + memUsage := stats.MemoryStats.Usage + // Subtract cache + if cache, ok := stats.MemoryStats.Stats["inactive_file"]; ok { + if memUsage > cache { + memUsage -= cache + } + } + memLimit := stats.MemoryStats.Limit + + var rx, tx uint64 + for _, net := range stats.Networks { + rx += net.RxBytes + tx += net.TxBytes + } + + return StatsUpdatedMsg{ + ID: id, CPU: cpu, + MemUsage: memUsage, MemLimit: memLimit, + Rx: rx, Tx: tx, + } + }) + } + return cmds +} + +func (a *App) loadLogsForSelected() tea.Cmd { + a.syncLogSelection() + c := a.selected() + if c == nil || a.CmdMgr == nil { return nil } + id := c.ID + since := c.LogsSince + mgr := a.CmdMgr + showStderr := a.Config.ShowStdErr + return func() tea.Msg { + opts := container.LogsOptions{ + Follow: false, ShowStdout: true, ShowStderr: showStderr, + Timestamps: true, + } + if since != "" { + opts.Since = since + } else { + opts.Tail = "500" + } + rc, err := mgr.Logs(id, opts) + if err != nil { + // Container may have been removed between list and log fetch; silently return empty + return LogsLoadedMsg{ID: id, Lines: nil} + } + defer rc.Close() + data, err := io.ReadAll(rc) + if err != nil { + return LogsLoadedMsg{ID: id, Lines: nil} + } + raw := strings.Split(strings.TrimRight(string(data), "\n"), "\n") + var cleaned []string + for _, line := range raw { + if len(line) > 8 && (line[0] == 0 || line[0] == 1 || line[0] == 2) { + line = line[8:] + } + if line != "" { + cleaned = append(cleaned, line) + } + } + return LogsLoadedMsg{ID: id, Lines: cleaned, Since: since} + } +} + +// ============================================================ +// CPU CALCULATION (matching original Rust formula) +// ============================================================ + +func calculateCPU(stats *container.StatsResponse) float64 { + // Guard against uint64 underflow (saturating_sub equivalent) + if stats.CPUStats.CPUUsage.TotalUsage < stats.PreCPUStats.CPUUsage.TotalUsage { + return 0 + } + if stats.CPUStats.SystemUsage < stats.PreCPUStats.SystemUsage { + return 0 + } + cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage - stats.PreCPUStats.CPUUsage.TotalUsage) + systemDelta := float64(stats.CPUStats.SystemUsage - stats.PreCPUStats.SystemUsage) + + if cpuDelta <= 0 || systemDelta <= 0 { + return 0 + } + + onlineCPUs := float64(stats.CPUStats.OnlineCPUs) + if onlineCPUs == 0 { + onlineCPUs = float64(len(stats.CPUStats.CPUUsage.PercpuUsage)) + } + if onlineCPUs == 0 { + onlineCPUs = 1 + } + + return (cpuDelta / systemDelta) * onlineCPUs * 100.0 +} + +// ============================================================ +// VIEW +// ============================================================ + +// Charm.sh / Catppuccin Mocha inspired color palette +var ( + cBase = lipgloss.Color("#1e1e2e") + cMantle = lipgloss.Color("#181825") + cSurface0 = lipgloss.Color("#313244") + cSurface1 = lipgloss.Color("#45475a") + cOverlay0 = lipgloss.Color("#6c7086") + cText = lipgloss.Color("#cdd6f4") + cSubtext0 = lipgloss.Color("#a6adc8") + cRed = lipgloss.Color("#f38ba8") + cGreen = lipgloss.Color("#a6e3a1") + cYellow = lipgloss.Color("#f9e2af") + cBlue = lipgloss.Color("#89b4fa") + cMauve = lipgloss.Color("#cba6f7") + cTeal = lipgloss.Color("#94e2d5") + cPeach = lipgloss.Color("#fab387") + cLavender = lipgloss.Color("#b4befe") + cFlamingo = lipgloss.Color("#f2cdcd") +) + +// colorOr returns the config color if non-empty, otherwise the default +func colorOr(cfgColor string, def lipgloss.Color) lipgloss.Color { + if cfgColor != "" { return lipgloss.Color(cfgColor) } + return def +} + +// Resolved colors from config with defaults +func (a *App) borderSelected() lipgloss.Color { return colorOr(a.Config.AppColors.Borders.Selected, cBlue) } +func (a *App) borderUnselected() lipgloss.Color { return colorOr(a.Config.AppColors.Borders.Unselected, cSurface0) } + +func (a *App) cmdColorCfg(cmd string) lipgloss.Color { + cc := a.Config.AppColors.Commands + switch cmd { + case "start": return colorOr(cc.Start, cGreen) + case "resume": return colorOr(cc.Resume, cBlue) + case "restart": return colorOr(cc.Restart, cMauve) + case "pause": return colorOr(cc.Pause, cYellow) + case "stop": return colorOr(cc.Stop, cRed) + case "delete": return colorOr(cc.Delete, cOverlay0) + default: return cText + } +} + +func (a *App) stateColorCfg(s ContainerState) lipgloss.Color { + cs := a.Config.AppColors.ContainerState + switch s { + case RunningHealthy: return colorOr(cs.RunningHealthy, cGreen) + case RunningUnhealthy: return colorOr(cs.RunningUnhealthy, cPeach) + case Paused: return colorOr(cs.Paused, cYellow) + case Exited: return colorOr(cs.Exited, cRed) + case Dead: return colorOr(cs.Dead, cRed) + case Restarting: return colorOr(cs.Restarting, cTeal) + case Removing: return colorOr(cs.Removing, cRed) + default: return colorOr(cs.Unknown, cOverlay0) + } +} + +func (a *App) View() tea.View { + var content string + if a.Width == 0 || a.Height == 0 { + content = "Loading..." + } else if a.Width < 60 || a.Height < 10 { + content = a.centeredMsg(fmt.Sprintf("Terminal too small (%dx%d)\nMinimum: 60x10", a.Width, a.Height)) + } else if a.State == StateLoading { + content = a.centeredMsg("Loading Docker containers...") + } else if a.ExecMode { + content = a.viewExec() + } else if a.InspectMode { + content = a.viewInspect() + } else { + content = a.viewNormal() + } + + mouseMode := tea.MouseModeCellMotion + if !a.MouseEnabled { + mouseMode = tea.MouseModeNone + } + + return tea.View{ + Content: content, + AltScreen: true, + MouseMode: mouseMode, + } +} + +func (a *App) viewNormal() string { + containerH := a.containerAreaHeight() + logsH := a.logsAreaHeight() + chartsH := a.chartsAreaHeight() + + // Clamp + total := 1 + containerH + logsH + chartsH + if total > a.Height { + logsH -= total - a.Height + if logsH < 3 { logsH = 3 } + } + + var sections []string + sections = append(sections, a.viewHeader()) + sections = append(sections, a.viewContainerSection(containerH)) + if a.ShowLogs { + sections = append(sections, a.viewLogs(logsH)) + } + sections = append(sections, a.viewBottom(chartsH)) + + result := lipgloss.JoinVertical(lipgloss.Left, sections...) + + // Clamp to terminal height to prevent overflow causing layout jumping + lines := strings.Split(result, "\n") + if len(lines) > a.Height { + lines = lines[:a.Height] + result = strings.Join(lines, "\n") + } + + // Overlays + if a.ShowHelp { + result = a.overlayHelp(result) + } + if a.DeleteConfirm { + result = a.overlayDeleteConfirm(result) + } + if a.Error != nil { + result = a.overlayError(result) + } + if a.InfoText != "" { + result = a.overlayInfo(result) + } + + return result +} + +func (a *App) centeredMsg(msg string) string { + return lipgloss.Place(a.Width, a.Height, lipgloss.Center, lipgloss.Center, + lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(cBlue).Padding(1, 3).Render(msg)) +} + +// --- Header --- + +func (a *App) viewHeader() string { + bg := cMantle + fg := cText + hdr := lipgloss.NewStyle().Background(bg).Foreground(fg) + cw := a.colWidths() + + spinnerFrames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + spinner := lipgloss.NewStyle().Background(bg).Foreground(cMauve).Render(spinnerFrames[a.LoadingIdx]) + + sortIcon := func(col SortColumn) string { + if a.SortCol == col && a.SortOrd != SortNone { + if a.SortOrd == SortAsc { return " ▲" } + return " ▼" + } + return "" + } + + var b strings.Builder + b.WriteString(hdr.Width(1).Render(spinner)) + b.WriteString(hdr.Width(2).Render(" ")) + b.WriteString(hdr.Width(cw.name).Render("name" + sortIcon(SortName))) + b.WriteString(hdr.Width(cw.state).Render("state" + sortIcon(SortState))) + b.WriteString(hdr.Width(cw.status).Render("status" + sortIcon(SortStatus))) + b.WriteString(hdr.Width(cw.cpu).Render("cpu" + sortIcon(SortCPU))) + b.WriteString(hdr.Width(cw.mem).Render("memory/limit" + sortIcon(SortMemory))) + b.WriteString(hdr.Width(cw.id).Render("id" + sortIcon(SortID))) + b.WriteString(hdr.Width(cw.image).Render("image" + sortIcon(SortImage))) + b.WriteString(hdr.Width(cw.rx).Align(lipgloss.Right).Render("rx" + sortIcon(SortRX))) + b.WriteString(hdr.Width(cw.tx).Align(lipgloss.Right).Render("tx" + sortIcon(SortTX))) + + content := b.String() + contentW := lipgloss.Width(content) + remaining := a.Width - contentW + if remaining > 0 { + helpText := "( h ) show help" + if a.ShowHelp { helpText = "( h ) exit help" } + hint := lipgloss.NewStyle().Background(bg).Foreground(cPeach).Width(remaining).Align(lipgloss.Right).Render(helpText) + content += hint + } + + return content +} + +type colW struct{ name, state, status, cpu, mem, id, image, rx, tx int } + +func (a *App) colWidths() colW { + cmdW := a.Width * 10 / 100 + if cmdW < 12 { cmdW = 12 } + inner := a.Width - cmdW - 6 - 3 // -6 for borders, -3 for row selector prefix + if inner < 60 { inner = 60 } + + // Fixed-width columns (content has predictable size) + const ( + cpuW = 8 // "99.99%" + idW = 10 // 8 chars + pad + rxW = 9 // "999.9 MB" + txW = 9 + ) + + // Compute max widths from data for variable columns (use rune count for unicode) + nameMax, stateMax, statusMax, memMax, imageMax := 6, 7, 8, 8, 7 // header minimums + for _, c := range a.filtered() { + if n := len([]rune(c.Name)); n > nameMax { nameMax = n } + stateLen := len([]rune(c.StateStr)) + 2 // icon + space + if stateLen > stateMax { stateMax = stateLen } + if n := len([]rune(c.Status)); n > statusMax { statusMax = n } + memStr := fmtBytes(c.MemUsage) + " / " + fmtBytes(c.MemLimit) + if n := len([]rune(memStr)); n > memMax { memMax = n } + if n := len([]rune(c.Image)); n > imageMax { imageMax = n } + } + + fixed := cpuW + idW + rxW + txW + avail := inner - fixed + if avail < 30 { avail = 30 } + + // Proportional distribution of variable columns + total := nameMax + stateMax + statusMax + memMax + imageMax + if total == 0 { total = 1 } + + nameW := avail * nameMax / total + stateW := avail * stateMax / total + statusW := avail * statusMax / total + memW := avail * memMax / total + imageW := avail - nameW - stateW - statusW - memW // give remainder to image + + // Apply minimums + if nameW < 6 { nameW = 6 } + if stateW < 7 { stateW = 7 } + if statusW < 8 { statusW = 8 } + if memW < 8 { memW = 8 } + if imageW < 7 { imageW = 7 } + + return colW{ + name: nameW, state: stateW, status: statusW, + cpu: cpuW, mem: memW, id: idW, + image: imageW, rx: rxW, tx: txW, + } +} + +// --- Container Section --- + +func (a *App) viewContainerSection(h int) string { + cmdW := a.Width * 10 / 100 + if cmdW < 12 { cmdW = 12 } + tableW := a.Width - cmdW - 4 + table := a.viewContainerTable(tableW, h) + cmds := a.viewCommands(cmdW, h) + return lipgloss.JoinHorizontal(lipgloss.Top, table, cmds) +} + +func (a *App) viewContainerTable(w, h int) string { + bc := a.borderUnselected() + if a.ActivePanel == PanelContainers { bc = a.borderSelected() } + style := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(bc).Width(w).Height(h - 2) + + cs := a.filtered() + total := len(cs) + sel := a.SelectedIdx + 1 + if total == 0 { sel = 0 } + + title := fmt.Sprintf("Containers %d/%d", sel, total) + + var sb strings.Builder + sb.WriteString(lipgloss.NewStyle().Foreground(cPeach).Bold(true).Render(title) + "\n") + + if a.FilterMode { + filterLabel := lipgloss.NewStyle().Foreground(cYellow).Bold(true).Render(fmt.Sprintf("Filter(%s): ", a.FilterBy)) + filterInput := lipgloss.NewStyle().Foreground(cText).Render(a.FilterText + "_") + sb.WriteString(filterLabel + filterInput + "\n") + } + + vis := h - 4 + if vis < 1 { vis = 1 } + end := a.ScrollOffset + vis + if end > len(cs) { end = len(cs) } + cw := a.colWidths() + + if total == 0 { + sb.WriteString(lipgloss.NewStyle().Foreground(cOverlay0).Render(" No containers found")) + return style.Render(sb.String()) + } + + for idx := a.ScrollOffset; idx < end; idx++ { + c := cs[idx] + selected := idx == a.SelectedIdx + + var row strings.Builder + if selected { + row.WriteString(lipgloss.NewStyle().Foreground(cBlue).Render(" > ")) + } else { + row.WriteString(" ") + } + + textStyle := lipgloss.NewStyle().Foreground(cBlue) + rxStyle := lipgloss.NewStyle().Foreground(cTeal) + txStyle := lipgloss.NewStyle().Foreground(cPeach) + icon, _ := stateStyle(c.State) + sc := a.stateColorCfg(c.State) + row.WriteString(textStyle.Render(padR(c.Name, cw.name))) + stateText := icon + " " + c.StateStr + row.WriteString(lipgloss.NewStyle().Foreground(sc).Render(padR(stateText, cw.state))) + row.WriteString(lipgloss.NewStyle().Foreground(sc).Render(padR(trunc(c.Status, cw.status-1), cw.status))) + row.WriteString(padR(fmt.Sprintf("%05.2f%%", c.CPUPercent), cw.cpu)) + row.WriteString(padR(fmtBytes(c.MemUsage)+" / "+fmtBytes(c.MemLimit), cw.mem)) + idStr := c.ID + if len(idStr) > 8 { idStr = idStr[:8] } + row.WriteString(textStyle.Render(padR(idStr, cw.id))) + row.WriteString(textStyle.Render(padR(trunc(c.Image, cw.image-1), cw.image))) + row.WriteString(rxStyle.Render(padL(fmtBytes(c.RxBytes), cw.rx))) + row.WriteString(txStyle.Render(padL(fmtBytes(c.TxBytes), cw.tx))) + + if selected { + sb.WriteString(lipgloss.NewStyle().Bold(true).Render(row.String())) + } else { + sb.WriteString(row.String()) + } + sb.WriteString("\n") + } + + return style.Render(sb.String()) +} + +func (a *App) viewCommands(w, h int) string { + bc := a.borderUnselected() + if a.ActivePanel == PanelCommands { bc = a.borderSelected() } + style := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(bc).Width(w).Height(h - 2) + + c := a.selected() + if c == nil { return style.Render("") } + + cmds := commandsForState(c.State) + if a.CmdSelectedIdx >= len(cmds) { + a.CmdSelectedIdx = 0 + } + arrow := lipgloss.NewStyle().Foreground(cBlue).Bold(true).Render("▶ ") + + var sb strings.Builder + for i, cmd := range cmds { + color := a.cmdColorCfg(cmd) + if i == a.CmdSelectedIdx { + sb.WriteString(arrow + lipgloss.NewStyle().Foreground(color).Bold(true).Render(cmd) + "\n") + } else { + sb.WriteString(" " + lipgloss.NewStyle().Foreground(color).Bold(true).Render(cmd) + "\n") + } + } + + return style.Render(sb.String()) +} + +// --- Logs --- + +func (a *App) viewLogs(h int) string { + bc := a.borderUnselected() + if a.ActivePanel == PanelLogs { bc = a.borderSelected() } + style := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(bc).Width(a.Width - 2).Height(h - 2) + + c := a.selected() + name := "" + if c != nil { name = c.Name } + + total := len(a.LogLines) + vis := a.logsVisibleRows() + end := a.LogScroll + vis + if end > total { end = total } + + title := fmt.Sprintf("Logs %d/%d - %s", end, total, name) + + var sb strings.Builder + sb.WriteString(lipgloss.NewStyle().Foreground(cPeach).Bold(true).Render(title) + "\n") + + if a.SearchMode { + searchLabel := lipgloss.NewStyle().Foreground(cYellow).Bold(true).Render("Search: ") + searchInput := lipgloss.NewStyle().Foreground(cText).Render(a.SearchText + "_") + matchInfo := lipgloss.NewStyle().Foreground(cSubtext0).Render(fmt.Sprintf(" (%d matches)", len(a.SearchMatches))) + sb.WriteString(searchLabel + searchInput + matchInfo + "\n") + } + + rows := h - 4 + if rows < 1 { rows = 1 } + + if total == 0 { + if c != nil && c.LogsSince == "" { + sb.WriteString(lipgloss.NewStyle().Foreground(cOverlay0).Render("parsing logs...")) + } else { + sb.WriteString(lipgloss.NewStyle().Foreground(cOverlay0).Render("No logs available")) + } + } else { + start := a.LogScroll + end := start + rows + if end > total { end = total; start = end - rows; if start < 0 { start = 0 } } + + maxW := a.Width - 8 + if maxW < 20 { maxW = 20 } + + for i := start; i < end; i++ { + line := stripAnsi(a.LogLines[i]) + runes := []rune(line) + // Horizontal scroll (rune-based for UTF-8) + if a.LogHScroll > 0 && len(runes) > a.LogHScroll { + runes = runes[a.LogHScroll:] + } else if a.LogHScroll > 0 { + runes = nil + } + if len(runes) > maxW { runes = runes[:maxW] } + line = string(runes) + + prefix := " " + if i == end-1 { + prefix = lipgloss.NewStyle().Foreground(cBlue).Render("▶ ") + } + + // Highlight search matches (substring-level) + if a.SearchText != "" { + caseSensitive := a.Config != nil && a.Config.LogSearchCaseSensitive + line = highlightMatches(line, a.SearchText, caseSensitive) + } + + sb.WriteString(prefix + line + "\n") + } + } + + return style.Render(sb.String()) +} + +// --- Bottom: Charts + Ports --- + +func (a *App) viewBottom(h int) string { + c := a.selected() + portsW := a.Width * 18 / 100 + if portsW < 22 { portsW = 22 } + remaining := a.Width - portsW - 8 + chartW := remaining / 3 + + cpu := a.viewSparkChart("cpu", chartW, h, c) + mem := a.viewSparkChart("memory", chartW, h, c) + bw := a.viewBWChart(chartW, h, c) + ports := a.viewPorts(portsW, h, c) + + return lipgloss.JoinHorizontal(lipgloss.Top, cpu, mem, bw, ports) +} + +func (a *App) viewSparkChart(typ string, w, h int, c *Container) string { + bc := a.borderUnselected() + var title string + var data []float64 + var color lipgloss.Color + var yLabel string + + if c != nil { + isActive := c.State == RunningHealthy || c.State == RunningUnhealthy + switch typ { + case "cpu": + title = fmt.Sprintf("cpu %05.2f%%", c.CPUPercent) + data = c.CPUHist + if isActive { color = cGreen } else { color = cOverlay0 } + case "memory": + title = fmt.Sprintf("memory %s", fmtBytes(c.MemUsage)) + for _, v := range c.MemHist { data = append(data, float64(v)) } + if isActive { color = cBlue } else { color = cOverlay0 } + } + } else { + switch typ { + case "cpu": + title = "cpu 00.00%" + color = cGreen + case "memory": + title = "memory 0.00 kB" + color = cBlue + } + } + + // Y-axis label: max value + maxVal := 0.0 + for _, v := range data { + if v > maxVal { maxVal = v } + } + if typ == "cpu" && maxVal > 0 { + yLabel = fmt.Sprintf("%05.2f%%", maxVal) + } else if typ == "memory" && maxVal > 0 { + yLabel = fmtBytes(uint64(maxVal)) + } + + return a.renderBrailleChart(title, yLabel, data, color, bc, w, h) +} + +func (a *App) viewBWChart(w, h int, c *Container) string { + bc := a.borderUnselected() + + rxRate, txRate := "0.00 kB/s", "0.00 kB/s" + var rxData, txData []float64 + if c != nil { + rxData = histDeltas(c.RxHist) + txData = histDeltas(c.TxHist) + if len(rxData) > 0 { + rxRate = fmtRate(uint64(rxData[len(rxData)-1])) + } + if len(txData) > 0 { + txRate = fmtRate(uint64(txData[len(txData)-1])) + } + } + + title := lipgloss.NewStyle().Foreground(cTeal).Render("rx: "+rxRate) + + " " + lipgloss.NewStyle().Foreground(cPeach).Render("tx: "+txRate) + + return a.renderBrailleBWChart(title, rxData, txData, rxRate, txRate, bc, w, h) +} + +// renderBrailleChart renders a single-dataset braille chart with Y-axis label and title in border +func (a *App) renderBrailleChart(title, yLabel string, data []float64, color, borderColor lipgloss.Color, w, h int) string { + innerH := h - 2 // border takes 2 + innerW := w - 2 // border takes 2 + if innerH < 1 { innerH = 1 } + if innerW < 4 { innerW = 4 } + + yLabelW := 0 + if yLabel != "" { + yLabelW = len([]rune(yLabel)) + 1 // +1 for separator + } + chartW := innerW - yLabelW + if chartW < 2 { chartW = 2 } + + chart := brailleChart(data, chartW, innerH, color) + chartLines := strings.Split(chart, "\n") + if len(chartLines) == 0 { + chartLines = []string{""} + } + + // Build content with Y-axis label + var sb strings.Builder + yStyle := lipgloss.NewStyle().Foreground(color) + for i, line := range chartLines { + if i == 0 && yLabel != "" { + sb.WriteString(yStyle.Render(padL(yLabel, yLabelW-1))) + sb.WriteString(lipgloss.NewStyle().Foreground(cSurface0).Render("│")) + } else if yLabelW > 0 { + sb.WriteString(strings.Repeat(" ", yLabelW-1)) + sb.WriteString(lipgloss.NewStyle().Foreground(cSurface0).Render("│")) + } + sb.WriteString(line) + if i < len(chartLines)-1 { + sb.WriteString("\n") + } + } + + // Title in top border (like original Rust) + titleRendered := " " + lipgloss.NewStyle().Foreground(cText).Bold(true).Render(title) + " " + titleW := lipgloss.Width(titleRendered) + topBorder := "╭" + leftDash := (w - 2 - titleW) / 2 + if leftDash < 1 { leftDash = 1 } + rightDash := w - 2 - titleW - leftDash + if rightDash < 1 { rightDash = 1 } + topBorder += strings.Repeat("─", leftDash) + titleRendered + strings.Repeat("─", rightDash) + "╮" + + // Bottom border + bottomBorder := "╰" + strings.Repeat("─", w-2) + "╯" + + // Side borders around content + contentLines := strings.Split(sb.String(), "\n") + var result strings.Builder + bStyle := lipgloss.NewStyle().Foreground(borderColor) + result.WriteString(bStyle.Render(topBorder) + "\n") + for i := 0; i < innerH; i++ { + line := "" + if i < len(contentLines) { line = contentLines[i] } + lineW := lipgloss.Width(line) + pad := innerW - lineW + if pad < 0 { pad = 0 } + result.WriteString(bStyle.Render("│") + line + strings.Repeat(" ", pad) + bStyle.Render("│")) + if i < innerH-1 { + result.WriteString("\n") + } + } + result.WriteString("\n" + bStyle.Render(bottomBorder)) + + return result.String() +} + +// renderBrailleBWChart renders RX/TX as two braille charts stacked, with title in border +func (a *App) renderBrailleBWChart(title string, rxData, txData []float64, rxRate, txRate string, borderColor lipgloss.Color, w, h int) string { + innerH := h - 2 + innerW := w - 2 + if innerH < 2 { innerH = 2 } + if innerW < 4 { innerW = 4 } + + halfH := (innerH - 1) / 2 // -1 for separator between rx/tx + if halfH < 1 { halfH = 1 } + + // Y-axis label width (use rx or tx rate) + yLabelW := 0 + rxYLabel := fmtRateShort(rxData) + txYLabel := fmtRateShort(txData) + if len([]rune(rxYLabel)) > yLabelW { yLabelW = len([]rune(rxYLabel)) } + if len([]rune(txYLabel)) > yLabelW { yLabelW = len([]rune(txYLabel)) } + if yLabelW > 0 { yLabelW += 1 } // separator + chartW := innerW - yLabelW + if chartW < 2 { chartW = 2 } + + rxChart := brailleChart(rxData, chartW, halfH, cTeal) + txChart := brailleChart(txData, chartW, halfH, cPeach) + + rxLines := strings.Split(rxChart, "\n") + txLines := strings.Split(txChart, "\n") + + // Build content + var sb strings.Builder + yStyle := lipgloss.NewStyle() + sepStyle := lipgloss.NewStyle().Foreground(cSurface0) + + // RX section + for i, line := range rxLines { + if i == 0 && rxYLabel != "" { + sb.WriteString(yStyle.Foreground(cTeal).Render(padL(rxYLabel, yLabelW-1))) + sb.WriteString(sepStyle.Render("│")) + } else if yLabelW > 0 { + sb.WriteString(strings.Repeat(" ", yLabelW-1)) + sb.WriteString(sepStyle.Render("│")) + } + sb.WriteString(line + "\n") + } + + // TX section + for i, line := range txLines { + if i == 0 && txYLabel != "" { + sb.WriteString(yStyle.Foreground(cPeach).Render(padL(txYLabel, yLabelW-1))) + sb.WriteString(sepStyle.Render("│")) + } else if yLabelW > 0 { + sb.WriteString(strings.Repeat(" ", yLabelW-1)) + sb.WriteString(sepStyle.Render("│")) + } + sb.WriteString(line) + if i < len(txLines)-1 { + sb.WriteString("\n") + } + } + + // Title in top border + titleW := lipgloss.Width(title) + titleRendered := " " + title + " " + titleRW := titleW + 2 + topBorder := "╭" + leftDash := (w - 2 - titleRW) / 2 + if leftDash < 1 { leftDash = 1 } + rightDash := w - 2 - titleRW - leftDash + if rightDash < 1 { rightDash = 1 } + topBorder += strings.Repeat("─", leftDash) + titleRendered + strings.Repeat("─", rightDash) + "╮" + + bottomBorder := "╰" + strings.Repeat("─", w-2) + "╯" + + contentLines := strings.Split(sb.String(), "\n") + var result strings.Builder + bStyle := lipgloss.NewStyle().Foreground(borderColor) + result.WriteString(bStyle.Render(topBorder) + "\n") + for i := 0; i < innerH; i++ { + line := "" + if i < len(contentLines) { line = contentLines[i] } + lineW := lipgloss.Width(line) + pad := innerW - lineW + if pad < 0 { pad = 0 } + result.WriteString(bStyle.Render("│") + line + strings.Repeat(" ", pad) + bStyle.Render("│")) + if i < innerH-1 { + result.WriteString("\n") + } + } + result.WriteString("\n" + bStyle.Render(bottomBorder)) + + return result.String() +} + +func fmtRateShort(data []float64) string { + if len(data) == 0 { return "" } + maxV := 0.0 + for _, v := range data { + if v > maxV { maxV = v } + } + if maxV == 0 { return "" } + return fmtRate(uint64(maxV)) +} + +// histDeltas computes per-interval deltas from cumulative history +func histDeltas(hist []uint64) []float64 { + if len(hist) < 2 { + return nil + } + deltas := make([]float64, 0, len(hist)-1) + for i := 1; i < len(hist); i++ { + if hist[i] >= hist[i-1] { + deltas = append(deltas, float64(hist[i]-hist[i-1])) + } else { + deltas = append(deltas, 0) + } + } + return deltas +} + +func (a *App) viewPorts(w, h int, c *Container) string { + style := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(cSurface0).Width(w).Height(h - 2) + + var sb strings.Builder + titleStr := "ports" + pad := (w - len(titleStr)) / 2 + if pad < 0 { pad = 0 } + sb.WriteString(strings.Repeat(" ", pad) + lipgloss.NewStyle().Foreground(cText).Bold(true).Render(titleStr) + "\n") + + if c == nil || len(c.Ports) == 0 { + sb.WriteString(lipgloss.NewStyle().Foreground(cOverlay0).Render("no ports")) + } else { + sb.WriteString(lipgloss.NewStyle().Foreground(cOverlay0).Render(fmt.Sprintf("%-10s %8s %8s", "ip", "private", "public")) + "\n") + for _, p := range c.Ports { + ip := p.IP + pubStr := "" + if p.Public > 0 { + pubStr = fmt.Sprintf("%d", p.Public) + } + sb.WriteString(fmt.Sprintf("%-10s %8d %8s\n", ip, p.Private, pubStr)) + } + } + + return style.Render(sb.String()) +} + +// --- Overlays --- + +func (a *App) overlayDeleteConfirm(base string) string { + name := "" + for _, c := range a.Containers { + if c.ID == a.DeleteTarget { name = c.Name; break } + } + if a.DeleteTarget == "" { + a.DeleteConfirm = false + return base + } + box := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(cRed). + Background(cMantle). + Padding(1, 3). + Render(fmt.Sprintf("Delete container %s?\n\n (y) Confirm (n) Cancel", + lipgloss.NewStyle().Bold(true).Foreground(cPeach).Render(name))) + + return placeOverlay(base, box, lipgloss.Center, lipgloss.Center, a.Width, a.Height) +} + +func (a *App) overlayError(base string) string { + msg := fmt.Sprintf("Error: %v\n\nPress (c) to clear or (esc) to dismiss", a.Error) + if a.ConnectCountdown > 0 { + msg = fmt.Sprintf("Error: %v\n\nExiting in %d seconds... Press (q) to quit now", a.Error, a.ConnectCountdown) + } + box := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(cRed). + Foreground(cRed). + Padding(1, 3). + Render(msg) + + return placeOverlay(base, box, lipgloss.Center, lipgloss.Center, a.Width, a.Height) +} + +func (a *App) overlayInfo(base string) string { + box := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(cTeal). + Padding(0, 2). + Render(a.InfoText) + + // Bottom right — overlay on base + return placeOverlay(base, box, lipgloss.Right, lipgloss.Bottom, a.Width, a.Height) +} + +// --- Inspect View --- + +func (a *App) viewInspect() string { + lines := strings.Split(a.InspectData, "\n") + total := len(lines) + maxLineW := 0 + for _, l := range lines { + if len(l) > maxLineW { maxLineW = len(l) } + } + + vis := a.Height - 4 + if vis < 1 { vis = 1 } + start := a.InspectScrollY + if start >= total { start = total - 1 } + if start < 0 { start = 0 } + end := start + vis + if end > total { end = total } + + c := a.selected() + name := "" + if c != nil { name = c.Name + " " + c.ID[:minI(8, len(c.ID))] } + + titleTop := fmt.Sprintf("inspecting: %s (esc/i/q to exit)", name) + titleBot := fmt.Sprintf("↑ %d/%d ↓ ← %d/%d →", a.InspectScrollY, total, a.InspectScrollX, maxLineW) + + var sb strings.Builder + for i := start; i < end; i++ { + line := lines[i] + if a.InspectScrollX > 0 && len(line) > a.InspectScrollX { + line = line[a.InspectScrollX:] + } else if a.InspectScrollX > 0 { + line = "" + } + // Wrap long lines instead of truncating + lineW := a.Width - 6 + if lineW < 10 { lineW = 10 } + for len(line) > lineW { + sb.WriteString(line[:lineW] + "\n") + line = line[lineW:] + } + sb.WriteString(line + "\n") + } + + style := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(cBlue). + Width(a.Width - 2). + Height(a.Height - 2) + + header := lipgloss.NewStyle().Foreground(cPeach).Bold(true).Render(titleTop) + footer := lipgloss.NewStyle().Foreground(cSubtext0).Render(titleBot) + content := lipgloss.NewStyle().Foreground(cOverlay0).Render(sb.String()) + + return style.Render(header + "\n" + content + "\n" + footer) +} + +// --- Help View --- + +func (a *App) overlayHelp(base string) string { + configPath := "" + if a.Config != nil && a.Config.DirConfig != "" { + configPath = a.Config.DirConfig + } + + var sb strings.Builder + sb.WriteString(" oxker — Docker TUI (Go)\n\n") + if configPath != "" { + sb.WriteString(fmt.Sprintf(" Config: %s\n\n", configPath)) + } + sb.WriteString(` Navigation + j/k ↑/↓ Scroll containers/logs/commands + J/K Scroll 10x speed + PgUp/PgDn Page scroll + g/G Top / Bottom + Tab/Shift+Tab Switch panels (Containers → Commands → Logs) + ←/→ Scroll logs horizontally + + Sorting + 1 Name 2 State 3 Status + 4 CPU 5 Memory 6 ID + 7 Image 8 RX 9 TX + 0 Reset sort + + Filtering & Search + / Filter containers (←/→ change filter: Name/Image/Status/All) + # Search logs (↑/↓ or Ctrl+N/P next/prev match) + + Container Actions + s Start x Stop r Restart + p Pause u Unpause d Delete + e Exec into container (sh) + Enter Execute selected command (in Commands panel) + + Display + l Toggle logs panel + [ / ] Resize logs panel + i Inspect container (JSON, scroll with j/k/h/l) + S / Ctrl+S Save logs to file + c Clear errors + h Toggle help + Esc Dismiss overlays + + Quit + q / Ctrl+C`) + + help := sb.String() + + box := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(cMauve). + Padding(1, 3). + Render(help) + + return placeOverlay(base, box, lipgloss.Center, lipgloss.Center, a.Width, a.Height) +} + +// ============================================================ +// OVERLAY HELPER +// ============================================================ + +// placeOverlay renders fg on top of bg at the given position. +// hPos: lipgloss.Left, Center, Right. vPos: lipgloss.Top, Center, Bottom. +func placeOverlay(bg string, fg string, hPos, vPos lipgloss.Position, bgW, bgH int) string { + bgLines := strings.Split(bg, "\n") + fgLines := strings.Split(fg, "\n") + fgW := 0 + for _, l := range fgLines { + if w := lipgloss.Width(l); w > fgW { fgW = w } + } + fgH := len(fgLines) + + // Compute top-left corner of overlay + var x, y int + switch hPos { + case lipgloss.Left: + x = 0 + case lipgloss.Right: + x = bgW - fgW + default: // Center + x = (bgW - fgW) / 2 + } + switch vPos { + case lipgloss.Top: + y = 0 + case lipgloss.Bottom: + y = bgH - fgH + default: // Center + y = (bgH - fgH) / 2 + } + if x < 0 { x = 0 } + if y < 0 { y = 0 } + + // Pad bg to ensure enough lines + for len(bgLines) < bgH { + bgLines = append(bgLines, strings.Repeat(" ", bgW)) + } + + // Overlay fg onto bg line by line + for i, fgLine := range fgLines { + bgIdx := y + i + if bgIdx >= len(bgLines) { break } + + // Pad the fg line to fgW so overlay has consistent width + fgLineW := lipgloss.Width(fgLine) + if fgLineW < fgW { + fgLine += strings.Repeat(" ", fgW-fgLineW) + } + + // Build: [left padding] + [fg line] + [right padding] + // We sacrifice the bg content under the overlay (acceptable for small overlays) + bgLine := bgLines[bgIdx] + bgLineW := lipgloss.Width(bgLine) + + left := strings.Repeat(" ", x) + rightW := bgW - x - fgW + right := "" + if rightW > 0 { + right = strings.Repeat(" ", rightW) + } + + // Try to preserve bg content outside the overlay area + // Left part: use bg line if it has content + if bgLineW > 0 && x > 0 { + left = lipgloss.NewStyle().Width(x).MaxWidth(x).Inline(true).Render(bgLine) + if lipgloss.Width(left) < x { + left += strings.Repeat(" ", x-lipgloss.Width(left)) + } + } + + bgLines[bgIdx] = left + fgLine + right + } + + return strings.Join(bgLines[:bgH], "\n") +} + +// ============================================================ +// LAYOUT HELPERS +// ============================================================ + +func (a *App) containerAreaHeight() int { + h := a.Height * 30 / 100 + if h < 6 { h = 6 } + return h +} + +func (a *App) logsAreaHeight() int { + if !a.ShowLogs { return 0 } + ch := a.containerAreaHeight() + bh := a.chartsAreaHeight() + lh := a.Height - ch - bh - 1 + if lh < 5 { lh = 5 } + // Apply log height percentage + maxLogs := a.Height * a.LogHeight / 100 + if lh > maxLogs { lh = maxLogs } + return lh +} + +func (a *App) chartsAreaHeight() int { + h := a.Height * 22 / 100 + if h < 7 { h = 7 } + return h +} + +func (a *App) containerVisibleRows() int { + h := a.containerAreaHeight() - 4 + if h < 1 { h = 1 } + return h +} + +func (a *App) logsVisibleRows() int { + h := a.logsAreaHeight() - 4 + if h < 1 { h = 1 } + return h +} + +// ============================================================ +// FILTERING +// ============================================================ + +func (a *App) filtered() []Container { + if a.FilterText == "" { return a.Containers } + term := strings.ToLower(a.FilterText) + var out []Container + for _, c := range a.Containers { + match := false + switch a.FilterBy { + case FilterByName: + match = strings.Contains(strings.ToLower(c.Name), term) + case FilterByImage: + match = strings.Contains(strings.ToLower(c.Image), term) + case FilterByStatus: + match = strings.Contains(strings.ToLower(c.Status), term) + case FilterByAll: + match = strings.Contains(strings.ToLower(c.Name), term) || + strings.Contains(strings.ToLower(c.Image), term) || + strings.Contains(strings.ToLower(c.Status), term) + } + if match { out = append(out, c) } + } + return out +} + +// ============================================================ +// HELPERS +// ============================================================ + +func stateStyle(s ContainerState) (string, lipgloss.Color) { + switch s { + case RunningHealthy: return "✓", cGreen + case RunningUnhealthy: return "!", cPeach + case Paused: return "‖", cYellow + case Exited: return "✖", cRed + case Dead: return "✖", cRed + case Restarting: return "↻", cTeal + case Removing: return "…", cRed + case Created: return "?", cOverlay0 + default: return "?", cOverlay0 + } +} + +func commandsForState(s ContainerState) []string { + switch s { + case RunningHealthy: + return []string{"pause", "restart", "stop", "delete"} + case RunningUnhealthy: + return []string{"pause", "restart", "stop", "delete"} + case Paused: + return []string{"resume", "stop", "delete"} + case Exited, Dead, Created: + return []string{"start", "restart", "delete"} + case Unknown: + return []string{"delete"} + case Restarting: + return []string{"stop", "delete"} + case Removing: + return []string{"delete"} + default: + return []string{"delete"} + } +} + +func cmdColor(cmd string) lipgloss.Color { + switch cmd { + case "start": + return cGreen + case "resume": + return cBlue + case "restart": + return cMauve + case "pause": + return cYellow + case "stop": + return cRed + case "delete": + return cOverlay0 + default: + return cText + } +} + +// brailleChart renders data as a braille dot chart (matching original Rust ratatui style). +// Each terminal cell is a 2×4 braille grid, giving (w*2) × (h*4) resolution. +func brailleChart(data []float64, w, h int, color lipgloss.Color) string { + if w <= 0 || h <= 0 { return "" } + + // Braille dot offsets: each char is 2 cols × 4 rows + // Bit positions: col0=[0,1,2,6] col1=[3,4,5,7] for rows 0,1,2,3 + dotBits := [2][4]rune{ + {0x01, 0x02, 0x04, 0x40}, // left column (col 0) + {0x08, 0x10, 0x20, 0x80}, // right column (col 1) + } + + pixW := w * 2 // horizontal pixel resolution + pixH := h * 4 // vertical pixel resolution + + // Take last pixW data points (each data point = 1 pixel column) + start := 0 + if len(data) > pixW { start = len(data) - pixW } + visible := data[start:] + + maxV := 0.0 + for _, v := range visible { + if v > maxV { maxV = v } + } + if maxV == 0 { maxV = 1 } + + // Create pixel grid (row 0 = bottom) + grid := make([][]bool, pixH) + for i := range grid { + grid[i] = make([]bool, pixW) + } + + // Plot data points + for i, v := range visible { + py := int(v / maxV * float64(pixH-1)) + if py >= pixH { py = pixH - 1 } + if py < 0 { py = 0 } + grid[py][i] = true + } + + // Render braille characters + s := lipgloss.NewStyle().Foreground(color) + dimS := lipgloss.NewStyle().Foreground(cSurface1) + var lines []string + + for cellRow := h - 1; cellRow >= 0; cellRow-- { + var b strings.Builder + for cellCol := 0; cellCol < w; cellCol++ { + var ch rune = 0x2800 // braille base + hasDot := false + for dc := 0; dc < 2; dc++ { + for dr := 0; dr < 4; dr++ { + px := cellCol*2 + dc + // Map: cellRow*4 + dr, but row 0 of cell = top visually = highest pixel + py := cellRow*4 + (3 - dr) + if px < len(grid[0]) && py >= 0 && py < pixH && grid[py][px] { + ch |= dotBits[dc][dr] + hasDot = true + } + } + } + if hasDot { + b.WriteString(s.Render(string(ch))) + } else { + b.WriteString(dimS.Render(string(rune(0x2800)))) // empty braille + } + } + lines = append(lines, b.String()) + } + return strings.Join(lines, "\n") +} + +// SI units (1000-based, matching original) +func fmtBytes(b uint64) string { + if b == 0 { return "0.00 kB" } + kb := float64(b) / 1000 + if kb < 1000 { return fmt.Sprintf("%.2f kB", kb) } + mb := kb / 1000 + if mb < 1000 { return fmt.Sprintf("%.2f MB", mb) } + gb := mb / 1000 + return fmt.Sprintf("%.2f GB", gb) +} + +func fmtRate(b uint64) string { + if b == 0 { return "0.00 kb/s" } + kb := float64(b) / 1000 + if kb < 1000 { return fmt.Sprintf("%.2f kb/s", kb) } + mb := kb / 1000 + if mb < 1000 { return fmt.Sprintf("%.2f Mb/s", mb) } + gb := mb / 1000 + return fmt.Sprintf("%.2f Gb/s", gb) +} + +func trunc(s string, n int) string { + if n <= 0 { return "" } + r := []rune(s) + if len(r) <= n { return s } + if n <= 3 { return string(r[:n]) } + return string(r[:n-3]) + "..." +} + +func padR(s string, w int) string { + r := []rune(s) + if len(r) >= w { return string(r[:w]) } + return s + strings.Repeat(" ", w-len(r)) +} + +func padL(s string, w int) string { + r := []rune(s) + if len(r) >= w { return string(r[:w]) } + return strings.Repeat(" ", w-len(r)) + s +} + +func highlightMatches(line, search string, caseSensitive bool) string { + haystack := line + term := search + if !caseSensitive { + haystack = strings.ToLower(line) + term = strings.ToLower(search) + } + idx := strings.Index(haystack, term) + if idx < 0 { + return line + } + hl := lipgloss.NewStyle().Foreground(cYellow).Bold(true) + var result strings.Builder + for idx >= 0 { + result.WriteString(line[:idx]) + result.WriteString(hl.Render(line[idx : idx+len(search)])) + line = line[idx+len(search):] + if !caseSensitive { + haystack = strings.ToLower(line) + } else { + haystack = line + } + idx = strings.Index(haystack, term) + } + result.WriteString(line) + return result.String() +} + +func minI(a, b int) int { if a < b { return a }; return b } +func maxI(a, b int) int { if a > b { return a }; return b } + +// keyMatch checks if a key string matches any key in the slice (from config keymap) +func keyMatch(key string, bindings []string) bool { + for _, b := range bindings { + if key == b { return true } + } + return false +} + +func compareFloat(a, b float64) int { + if math.Abs(a-b) < 0.001 { return 0 } + if a < b { return -1 } + return 1 +} + +func compareUint(a, b uint64) int { + if a == b { return 0 } + if a < b { return -1 } + return 1 +} + +func appendMax(s []float64, v float64, max int) []float64 { + if len(s) >= max { + copy(s, s[1:]) + s[len(s)-1] = v + return s + } + return append(s, v) +} + +func appendMaxU(s []uint64, v uint64, max int) []uint64 { + if len(s) >= max { + copy(s, s[1:]) + s[len(s)-1] = v + return s + } + return append(s, v) +} + +func mapState(state, status string) ContainerState { + switch strings.ToLower(state) { + case "running": + if strings.Contains(strings.ToLower(status), "unhealthy") { + return RunningUnhealthy + } + return RunningHealthy + case "exited": + return Exited + case "paused": + return Paused + case "restarting": + return Restarting + case "removing": + return Removing + case "dead": + return Dead + case "created": + return Created + default: + return Unknown + } +} + +func mapPorts(ports []container.Port) []ContainerPorts { + result := make([]ContainerPorts, len(ports)) + for i, p := range ports { + result[i] = ContainerPorts{IP: p.IP, Private: int(p.PrivatePort), Public: int(p.PublicPort)} + } + return result +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..3ef5ff3 --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/docker/client.go b/internal/docker/client.go new file mode 100644 index 0000000..66a82d6 --- /dev/null +++ b/internal/docker/client.go @@ -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() +} diff --git a/internal/utils/ratelimiter.go b/internal/utils/ratelimiter.go new file mode 100644 index 0000000..421f433 --- /dev/null +++ b/internal/utils/ratelimiter.go @@ -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{} +} diff --git a/internal/utils/scroll.go b/internal/utils/scroll.go new file mode 100644 index 0000000..6297b63 --- /dev/null +++ b/internal/utils/scroll.go @@ -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" + } +} diff --git a/internal/utils/stats.go b/internal/utils/stats.go new file mode 100644 index 0000000..b7ebbda --- /dev/null +++ b/internal/utils/stats.go @@ -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, + } +} diff --git a/internal/utils/table.go b/internal/utils/table.go new file mode 100644 index 0000000..216c9d8 --- /dev/null +++ b/internal/utils/table.go @@ -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() +} diff --git a/source/oxker b/source/oxker new file mode 160000 index 0000000..e020eb1 --- /dev/null +++ b/source/oxker @@ -0,0 +1 @@ +Subproject commit e020eb157ca317b0a2ba100f0024f444698552a2 diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs deleted file mode 100644 index 252d982..0000000 --- a/src/app_data/container_state.rs +++ /dev/null @@ -1,1684 +0,0 @@ -use std::{ - cmp::Ordering, - collections::{HashSet, VecDeque}, - fmt, - net::IpAddr, -}; - -use bollard::secret::{ContainerSummaryHealthStatusEnum, PortSummary}; -use jiff::{Timestamp, tz::TimeZone}; -use ratatui::{ - layout::Size, - style::Color, - text::{Line, Text}, - widgets::ListState, -}; - -use crate::config::AppColors; - -use super::Header; - -const ONE_KB: f64 = 1000.0; -const ONE_MB: f64 = ONE_KB * 1000.0; -const ONE_GB: f64 = ONE_MB * 1000.0; - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub enum ScrollDirection { - // Next, - // Previous, - Up, - Down, - Left, - Right, -} - -#[derive(Debug, Clone, Eq, Hash, PartialEq)] -pub struct ContainerId(String); - -impl From<&str> for ContainerId { - fn from(x: &str) -> Self { - Self(x.to_owned()) - } -} - -impl ContainerId { - pub const fn get(&self) -> &str { - self.0.as_str() - } - - /// Only return first 8 chars of id, is usually more than enough for uniqueness - /// need to update tests to use real ids, or atleast strings of the correct-ish length - pub fn get_short(&self) -> String { - self.0.chars().take(8).collect::() - } -} - -impl Ord for ContainerId { - fn cmp(&self, other: &Self) -> Ordering { - self.0.cmp(&other.0) - } -} - -impl PartialOrd for ContainerId { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -pub trait Contains { - fn contains(&self, input: &str) -> bool; -} -/// ContainerName and ContainerImage are simple structs, used so can implement custom fmt functions to them -macro_rules! unit_struct { - ($name:ident) => { - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] - pub struct $name(String); - - impl From for $name { - fn from(value: String) -> Self { - Self(value) - } - } - - #[cfg(test)] - impl From<&str> for $name { - fn from(value: &str) -> Self { - Self(value.to_owned()) - } - } - - impl $name { - pub const fn get(&self) -> &str { - self.0.as_str() - } - - pub fn set(&mut self, value: String) { - self.0 = value; - } - } - - impl Contains for $name { - fn contains(&self, input: &str) -> bool { - self.0.to_lowercase().contains(input) - } - } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - if self.0.chars().count() >= 30 { - write!(f, "{}…", self.0.chars().take(29).collect::()) - } else { - write!(f, "{}", self.0) - } - } - } - }; -} - -unit_struct!(ContainerName); -unit_struct!(ContainerImage); - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ContainerPorts { - pub ip: Option, - pub private: u16, - pub public: Option, -} - -impl From for ContainerPorts { - fn from(value: PortSummary) -> Self { - Self { - ip: value.ip.and_then(|i| i.parse::().ok()), - private: value.private_port, - public: value.public_port, - } - } -} - -impl ContainerPorts { - pub fn len_ip(&self) -> usize { - self.ip - .as_ref() - .map_or(0, |i| i.to_string().chars().count()) - } - pub fn len_private(&self) -> usize { - format!("{}", self.private).chars().count() - } - pub fn len_public(&self) -> usize { - format!("{}", self.public.unwrap_or_default()) - .chars() - .count() - } - - /// Return as tuple of Strings, ip address, private port, and public port - pub fn get_all(&self) -> (String, String, String) { - ( - self.ip - .as_ref() - .map_or(String::new(), std::string::ToString::to_string), - format!("{}", self.private), - self.public.map_or(String::new(), |s| s.to_string()), - ) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StatefulList { - pub state: ListState, - pub items: Vec, -} - -impl StatefulList { - pub fn new(items: Vec) -> Self { - Self { - state: ListState::default(), - items, - } - } - - pub fn end(&mut self) { - let len = self.items.len(); - if len > 0 { - self.state.select(Some(self.items.len() - 1)); - } - } - - pub fn start(&mut self) { - self.state.select(Some(0)); - } - - pub fn scroll(&mut self, scroll: &ScrollDirection) { - match scroll { - ScrollDirection::Down => self.next(), - ScrollDirection::Up => self.previous(), - // TODO set offset - _ => (), - } - } - - fn next(&mut self) { - if !self.items.is_empty() { - self.state.select(Some( - self.state.selected().map_or( - 0, - |i| { - if i < self.items.len() - 1 { i + 1 } else { i } - }, - ), - )); - } - } - - fn previous(&mut self) { - if !self.items.is_empty() { - self.state.select(Some( - self.state - .selected() - .map_or(0, |i| if i == 0 { 0 } else { i - 1 }), - )); - } - } - - /// Return the current status of the select list, e.g. 2/5, - /// MAYBE add up down arrows, check if at start or end etc - pub fn get_state_title(&self) -> String { - if self.items.is_empty() { - String::new() - } else { - let len = self.items.len(); - let count = self - .state - .selected() - .map_or(0, |value| if len > 0 { value + 1 } else { value }); - format!(" {count}/{len}") - } - } -} - -/// Store the containers status in a struct, so can then check for healthy/unhealthy status -/// It's usually something like "Up 1 hour", "Exited (0) 10 hours ago", "Up 10 minutes (unhealthy)" -#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] -pub struct ContainerStatus(String); - -impl From for ContainerStatus { - fn from(value: String) -> Self { - Self(value) - } -} - -impl ContainerStatus { - /// Check if a container is unhealthy - pub fn unhealthy(&self) -> bool { - self.contains("(unhealthy)") - } - - /// Get a reference to the source string - pub const fn get(&self) -> &String { - &self.0 - } -} - -impl Contains for ContainerStatus { - /// Check if the state contains a specific string - fn contains(&self, item: &str) -> bool { - self.0.to_lowercase().contains(item) - } -} - -/// By default a container's running status will be healthy -#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd)] -pub enum RunningState { - Healthy, - Unhealthy, -} -/// States of the container -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum State { - Dead, - Exited, - Paused, - Removing, - Restarting, - Running(RunningState), - Unknown, -} - -impl State { - /// The container is alive if the start is Running, either healthy or unhealthy - pub const fn is_alive(self) -> bool { - matches!(self, Self::Running(_)) - } - - /// Check if state is running & healthy - pub const fn is_healthy(self) -> bool { - match self { - Self::Running(x) => match x { - RunningState::Healthy => true, - RunningState::Unhealthy => false, - }, - _ => false, - } - } - /// Color of the state for the containers section - pub const fn get_color(self, colors: AppColors) -> Color { - match self { - Self::Dead => colors.container_state.dead, - Self::Exited => colors.container_state.exited, - Self::Paused => colors.container_state.paused, - Self::Removing => colors.container_state.removing, - Self::Restarting => colors.container_state.restarting, - Self::Running(RunningState::Healthy) => colors.container_state.running_healthy, - Self::Running(RunningState::Unhealthy) => colors.container_state.running_unhealthy, - Self::Unknown => colors.container_state.unknown, - } - } - /// Dirty way to create order for the state, rather than impl Ord - pub const fn order(self) -> u8 { - match self { - Self::Running(RunningState::Healthy) => 0, - Self::Running(RunningState::Unhealthy) => 1, - Self::Paused => 2, - Self::Restarting => 3, - Self::Removing => 4, - Self::Exited => 5, - Self::Dead => 6, - Self::Unknown => 7, - } - } -} - -/// Need status, to check if container is unhealthy or not -impl From<(&str, &ContainerStatus)> for State { - fn from((input, status): (&str, &ContainerStatus)) -> Self { - match input { - "dead" => Self::Dead, - "exited" => Self::Exited, - "paused" => Self::Paused, - "removing" => Self::Removing, - "restarting" => Self::Restarting, - "running" => { - if status.unhealthy() { - Self::Running(RunningState::Unhealthy) - } else { - Self::Running(RunningState::Healthy) - } - } - _ => Self::Unknown, - } - } -} - -/// Need status, to check if container is unhealthy or not -impl - From<( - &bollard::secret::ContainerSummaryStateEnum, - &ContainerStatus, - )> for State -{ - fn from( - (input, status): ( - &bollard::secret::ContainerSummaryStateEnum, - &ContainerStatus, - ), - ) -> Self { - match input { - bollard::secret::ContainerSummaryStateEnum::DEAD => Self::Dead, - bollard::secret::ContainerSummaryStateEnum::EXITED => Self::Exited, - bollard::secret::ContainerSummaryStateEnum::PAUSED => Self::Paused, - bollard::secret::ContainerSummaryStateEnum::REMOVING => Self::Removing, - bollard::secret::ContainerSummaryStateEnum::RESTARTING => Self::Restarting, - bollard::secret::ContainerSummaryStateEnum::RUNNING => { - if status.unhealthy() { - Self::Running(RunningState::Unhealthy) - } else { - Self::Running(RunningState::Healthy) - } - } - _ => Self::Unknown, - } - } -} - -/// Again, need status, to check if container is unhealthy or not -impl - From<( - Option<&bollard::secret::ContainerSummaryStateEnum>, - &ContainerStatus, - )> for State -{ - fn from( - (input, status): ( - Option<&bollard::secret::ContainerSummaryStateEnum>, - &ContainerStatus, - ), - ) -> Self { - input.map_or(Self::Unknown, |input| Self::from((input, status))) - } -} - -/// Again, need status, to check if container is unhealthy or not -impl From<(Option, &ContainerStatus)> for State { - fn from((input, status): (Option, &ContainerStatus)) -> Self { - input.map_or(Self::Unknown, |input| Self::from((input.as_str(), status))) - } -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let disp = match self { - Self::Dead => "✖ dead", - Self::Exited => "✖ exited", - Self::Paused => "॥ paused", - Self::Removing => "removing", - Self::Restarting => "↻ restarting", - Self::Running(RunningState::Healthy) => "✓ running", - Self::Running(RunningState::Unhealthy) => "! running", - Self::Unknown => "? unknown", - }; - write!(f, "{disp}") - } -} - -/// Items for the container control list -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum DockerCommand { - Pause, - Restart, - Start, - Stop, - Resume, - Delete, -} - -impl DockerCommand { - pub const fn get_color(self, colors: AppColors) -> Color { - match self { - Self::Pause => colors.commands.pause, - Self::Restart => colors.commands.restart, - Self::Start => colors.commands.start, - Self::Stop => colors.commands.stop, - Self::Delete => colors.commands.delete, - Self::Resume => colors.commands.resume, - } - } - - /// Docker commands available depending on the containers state - pub fn gen_vec(state: State) -> Vec { - match state { - State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete], - State::Paused => vec![Self::Resume, Self::Stop, Self::Delete], - State::Restarting => vec![Self::Stop, Self::Delete], - State::Running(_) => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete], - _ => vec![Self::Delete], - } - } -} - -impl fmt::Display for DockerCommand { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let disp = match self { - Self::Pause => "pause", - Self::Delete => "delete", - Self::Restart => "restart", - Self::Start => "start", - Self::Stop => "stop", - Self::Resume => "resume", - }; - write!(f, "{disp}") - } -} - -pub trait Stats { - fn get_value(&self) -> f64; -} - -/// Struct for frequently updated CPU stats -/// So can use custom display formatter -/// Use trait Stats for use as generic in draw_chart function -#[derive(Debug, Default, Clone, Copy)] -pub struct CpuStats(f64); - -impl CpuStats { - pub const fn new(value: f64) -> Self { - Self(value) - } -} - -impl Eq for CpuStats {} - -impl PartialEq for CpuStats { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl PartialOrd for CpuStats { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for CpuStats { - fn cmp(&self, other: &Self) -> Ordering { - if self.0 > other.0 { - Ordering::Greater - } else if (self.0 - other.0).abs() < 0.01 { - Ordering::Equal - } else { - Ordering::Less - } - } -} - -impl Stats for CpuStats { - fn get_value(&self) -> f64 { - self.0 - } -} - -impl fmt::Display for CpuStats { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let disp = format!("{:05.2}%", self.0); - write!(f, "{disp:>x$}", x = f.width().unwrap_or(1)) - } -} - -/// Struct for frequently updated memory usage stats -/// So can use custom display formatter -/// Use trait Stats for use as generic in draw_chart function -#[derive(Debug, Default, Clone, Copy, Eq)] -pub struct ByteStats(u64); - -impl PartialEq for ByteStats { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl PartialOrd for ByteStats { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ByteStats { - fn cmp(&self, other: &Self) -> Ordering { - self.0.cmp(&other.0) - } -} - -impl ByteStats { - pub const fn new(value: u64) -> Self { - Self(value) - } - pub const fn update(&mut self, value: u64) { - self.0 = value; - } -} - -#[allow(clippy::cast_precision_loss)] -impl Stats for ByteStats { - fn get_value(&self) -> f64 { - self.0 as f64 - } -} - -/// convert from bytes to kB, MB, GB etc -impl fmt::Display for ByteStats { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let as_f64 = self.get_value(); - let p = match as_f64 { - x if x >= ONE_GB => format!("{y:.2} GB", y = as_f64 / ONE_GB), - x if x >= ONE_MB => format!("{y:.2} MB", y = as_f64 / ONE_MB), - _ => format!("{y:.2} kB", y = as_f64 / ONE_KB), - }; - write!(f, "{p:>x$}", x = f.width().unwrap_or(1)) - } -} - -#[derive(Debug, Default, Clone, Copy, Eq)] -pub struct BandwidthStat(u64); - -#[cfg(test)] -impl BandwidthStat { - pub fn new(x: u64) -> Self { - Self(x) - } -} - -impl PartialEq for BandwidthStat { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl PartialOrd for BandwidthStat { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for BandwidthStat { - fn cmp(&self, other: &Self) -> Ordering { - self.0.cmp(&other.0) - } -} - -#[allow(clippy::cast_precision_loss)] -impl Stats for BandwidthStat { - fn get_value(&self) -> f64 { - self.0 as f64 - } -} - -/// convert from bytes to per second, using 1000 instead of 1024 -impl fmt::Display for BandwidthStat { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let as_f64 = self.get_value(); - let p = match as_f64 { - x if x >= ONE_GB => format!("{y:.2} GB/s", y = as_f64 / ONE_GB), - x if x >= ONE_MB => format!("{y:.2} Mb/s", y = as_f64 / ONE_MB), - _ => format!("{y:.2} kb/s", y = as_f64 / ONE_KB), - }; - write!(f, "{p:>x$}", x = f.width().unwrap_or(1)) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NetworkBandwidth(VecDeque); - -impl NetworkBandwidth { - pub fn new() -> Self { - Self(VecDeque::with_capacity(60)) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// Find the highest speed recorded in the vecque - pub fn max(&self) -> BandwidthStat { - self.to_vec_f64() - .iter() - .map(|(_, speed)| *speed) - .max_by(|a, b| a.total_cmp(b)) - .map(|m| BandwidthStat(m as u64)) - .unwrap_or(BandwidthStat(0)) - } - - pub fn push(&mut self, x: u64) { - if self.0.len() >= 60 { - self.0.pop_front(); - } - self.0.push_back(BandwidthStat(x)); - } - - /// Get the current total amount of traffic on a given device - pub fn current_total(&self) -> ByteStats { - self.0 - .back() - .map_or(ByteStats::default(), |i| ByteStats::new(i.0)) - } - - /// Convert to f64 for use in the network graph - pub fn to_vec_f64(&self) -> Vec<(f64, f64)> { - self.0 - .iter() - .zip(self.0.iter().skip(1)) - .enumerate() - .map(|(i, (prev, current))| (i as f64, current.0.saturating_sub(prev.0) as f64)) - .collect() - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct ChartsData { - pub memory: ChartSeries, - pub cpu: ChartSeries, - pub rx: ChartSeries, - pub tx: ChartSeries, - pub state: State, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct ChartSeries { - pub dataset: Vec<(f64, f64)>, - pub max: T, - pub current: T, -} - -/// Used to make sure that each log entry, for each container, is unique, -/// will only push a log entry into the logs vec if timestamp of said log entry isn't in the hashset -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct LogsTz(String); - -impl fmt::Display for LogsTz { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl LogsTz { - /// With a given &str, split into a logtz and content, so that we only need to `use split_once()` once - /// The docker log, which should always contain a timestamp, is in the format `2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet` - pub fn splitter(input: &str) -> (Self, String) { - let (tz, content) = input.split_once(' ').unwrap_or_default(); - (Self(tz.to_owned()), content.to_owned()) - } - - /// Display the timestamp in a given format, and if provided, with a timezone offset - pub fn display_with_formatter(&self, tz: Option<&TimeZone>, format: &str) -> Option { - self.0.parse::().map_or(None, |t| { - if let Some(tz) = tz.as_ref() { - let tz = tz.iana_name()?; - let z = t.in_tz(tz).ok()?; - Some(z.strftime(format).to_string()) - } else { - Some(t.strftime(format).to_string()) - } - }) - } -} - -/// Store the logs alongside a HashSet, each log *should* generate a unique timestamp, -/// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the -/// stateful list dependent on whether the timestamp is in the HashSet or not -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Logs { - lines: StatefulList>, - tz: HashSet, - search_results: Vec, - search_term: Option, - offset: usize, - max_offset: usize, - max_log_len: usize, - adjusted_max_width: usize, - adjust_max_width_text_len: usize, -} - -impl Default for Logs { - fn default() -> Self { - let mut lines = StatefulList::new(vec![]); - lines.end(); - Self { - lines, - tz: HashSet::new(), - offset: 0, - max_offset: 0, - search_term: None, - search_results: vec![], - adjusted_max_width: 0, - adjust_max_width_text_len: 0, - max_log_len: 0, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub enum LogsButton { - Both, - Next, - Previous, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)] -pub struct LogSearch { - pub term: Option, - pub result: Option, - pub buttons: Option, -} - -/// LogSearch is used in FrameData -impl From<&Logs> for LogSearch { - fn from(l: &Logs) -> Self { - let buttons = l.lines.state.selected().as_ref().and_then(|x| { - let show_next = l.search_results.iter().any(|n| n > x); - let show_previous = l.search_results.iter().any(|n| n < x); - match (show_next, show_previous) { - (true, true) => Some(LogsButton::Both), - (true, false) => Some(LogsButton::Next), - (false, true) => Some(LogsButton::Previous), - (false, false) => None, - } - }); - Self { - term: l.search_term.clone(), - result: l.get_search_result(), - buttons, - } - } -} - -impl Logs { - pub fn gen_log_search(&self) -> LogSearch { - LogSearch::from(self) - } - - /// Scroll to the next or previous search result, accounts for when currently selected line isn't in the results vec - pub fn search_scroll(&mut self, sd: &ScrollDirection) -> Option<()> { - if let Some(current_selected) = self.lines.state.selected() { - if let Some(current_position) = self - .search_results - .iter() - .position(|i| i == ¤t_selected) - { - if let Some(new_index) = match sd { - ScrollDirection::Down => current_position.checked_add(1), - ScrollDirection::Up => current_position.checked_sub(1), - // TODO set offset - _ => None, - } && let Some(f) = self.search_results.get(new_index) - { - self.lines.state.select(Some(*f)); - return Some(()); - } - } else { - let range = match sd { - ScrollDirection::Up => (0..=current_selected).rev().collect::>(), - ScrollDirection::Down => (current_selected - ..=self - .search_results - .last() - .map_or_else(|| current_selected, |i| *i)) - .collect::>(), - // TODO set offset - _ => vec![], - }; - for i in range { - if self.search_results.contains(&i) { - self.lines.state.select(Some(i)); - return Some(()); - } - } - } - } - None - } - - /// Get a string x/y, where y is total matches found, and x is current ordered selected line - /// WIll be padded by max chars of total matches - fn get_search_result(&self) -> Option { - if self.search_results.is_empty() { - return None; - } - Some(self.lines.state.selected().map_or_else( - || format!("{}", self.search_results.len()), - |current_index| { - self.search_results - .iter() - .position(|i| i == ¤t_index) - .map_or_else( - || format!("{}", self.search_results.len()), - |index| { - let len = format!("{}", self.search_results.len()); - let len_width = len.chars().count(); - format!("{:>len_width$}/{len:>len_width$}", index + 1) - }, - ) - }, - )) - } - - /// Search through the logs for a matching string - pub fn search(&mut self, case_sensitive: bool, scroll: bool) { - if let Some(search_term) = self.search_term.as_ref() { - let term = if case_sensitive { - search_term.to_owned() - } else { - search_term.to_lowercase() - }; - self.search_results = self - .lines - .items - .iter() - .enumerate() - .filter_map(|(index, a)| { - a.lines - .iter() - .any(|b| { - b.spans.iter().any(|c| { - if case_sensitive { - c.content.contains(&term) - } else { - c.content.to_lowercase().contains(&term) - } - }) - }) - .then_some(index) - }) - .collect(); - if !self.search_results.is_empty() && scroll { - self.lines.state.select(self.search_results.last().copied()); - self.offset = 0; - } - } else { - self.search_results.clear(); - } - } - - /// Set a single char into the filter term - pub fn search_term_push(&mut self, c: char, case_sensitive: bool) { - if let Some(term) = self.search_term.as_mut() { - term.push(c); - } else { - self.search_term = Some(format!("{c}")); - } - self.search(case_sensitive, true); - } - - /// Delete the final char of the filter term - pub fn search_term_pop(&mut self, case_sensitive: bool) { - if let Some(term) = self.search_term.as_mut() { - term.pop(); - if term.is_empty() { - self.search_term = None; - } - } - self.search(case_sensitive, true); - } - - /// Remove the filter completely - pub fn search_term_clear(&mut self) { - self.search_term = None; - self.search_results.clear(); - } - - /// Only allow a new log line to be inserted if the log timestamp isn't in the tz HashSet - pub fn insert(&mut self, line: Text<'static>, tz: LogsTz, case_sensitive: bool) { - if self.tz.insert(tz) { - self.max_log_len = self.max_log_len.max(line.width()); - self.lines.items.push(line); - // Maybe - Ideally we'd re-render here - if self.search_term.is_some() { - self.search(case_sensitive, false); - } - } - } - - /// If scrolling horizontally along the logs, display a counter of the position in the in the scroll, `x/y` - pub fn get_scroll_title(&mut self, width: u16) -> Option { - if self.horizontal_scroll_able(width) { - let text_width = self.adjust_max_width_text_len; - let arrow_left = if self.offset > 0 { " ←" } else { " " }; - let arrow_right = if self.offset < self.adjusted_max_width { - "→ " - } else { - " " - }; - Some(format!( - "{left} {offset:>text_width$}/{adjusted_max_width} {right}", - offset = self.offset, - adjusted_max_width = self.adjusted_max_width, - left = arrow_left, - right = arrow_right, - )) - } else { - None - } - } - - /// Format a log lone. Only return screen width amount of chars - /// If offset set, remove `char_offset` number of chars from a Text - /// `text` *should* only be a single line, so just use the .first() method rather than trying to iterate - fn format_log_line(text: &Text<'static>, char_offset: usize, width: u16) -> Text<'static> { - let mut skipped = 0; - text.lines.first().map_or_else(Text::default, |line| { - Text::from(Line::from( - line.spans - .iter() - .filter_map(|span| { - if skipped >= char_offset { - Some(ratatui::text::Span::styled( - span.content.chars().take(width.into()).collect::(), - span.style, - )) - } else { - let span_len = span.content.chars().count(); - if skipped + span_len <= char_offset { - skipped += span_len; - None - } else { - let start_index = char_offset - skipped; - skipped = char_offset; - Some(ratatui::text::Span::styled( - span.content - .chars() - .skip(start_index) - .take(width.into()) - .collect::(), - span.style, - )) - } - } - }) - .collect::>(), - )) - }) - } - - /// Get the logs vec, but instead of cloning to whole vec, only clone items within x of the currently selected index, as well as only the current screen widths number of chars - /// Where x is the abs different of the index plus the panel height & a padding - /// Take into account the char offset, so that can scroll a line - /// The rest can be just empty list items - pub fn get_visible_logs(&self, size: Size, padding: usize) -> Vec> { - let current_index = self.lines.state.selected().unwrap_or_default(); - let height_padding = usize::from(size.height) + padding; - let char_offset = if self.offset > self.max_log_len { - self.max_log_len - } else { - self.offset - }; - - self.lines - .items - .iter() - .enumerate() - .map(|(index, item)| { - if current_index.abs_diff(index) <= height_padding { - Self::format_log_line(item, char_offset, size.width) - } else { - Text::from("") - } - }) - .collect() - } - - /// The rest of the methods are basically forwarding from the underlying StatefulList - pub fn get_state_title(&self) -> String { - self.lines.get_state_title() - } - - /// Return true it currently selected container logs are wide enough to horizontally scroll - pub fn horizontal_scroll_able(&mut self, width: u16) -> bool { - if self.lines.items.is_empty() { - return false; - } - self.adjusted_max_width = self.max_log_len.saturating_sub(width.into()) + 4; - self.adjust_max_width_text_len = self.adjusted_max_width.to_string().chars().count(); - self.max_log_len + 4 > usize::from(width) - } - - /// Add a padding so one char will always be visilbe? - pub fn forward(&mut self, width: u16) { - // Need to set a max_offset, instead of using a width each time - if self.horizontal_scroll_able(width) - && self.adjusted_max_width > 0 - && self.offset < self.adjusted_max_width - { - self.offset = self.offset.saturating_add(1); - } - } - - /// Reduce the char offset - pub const fn back(&mut self) { - self.offset = self.offset.saturating_sub(1); - } - - /// Scroll lines down by one - pub fn next(&mut self) { - self.lines.next(); - } - - /// Scroll lines up by one - pub fn previous(&mut self) { - self.lines.previous(); - } - - /// Go to the end of the lines - pub fn end(&mut self) { - self.lines.end(); - } - - /// Go to the start of the lines - pub fn start(&mut self) { - self.lines.start(); - } - - /// Get total number of log lines - pub const fn len(&self) -> usize { - self.lines.items.len() - } - - pub const fn state(&mut self) -> &mut ListState { - &mut self.lines.state - } -} - -/// Info for each container -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ContainerItem { - pub cpu_stats: VecDeque, - pub created: u64, - pub docker_controls: StatefulList, - pub health: Option, - pub id: ContainerId, - pub image: ContainerImage, - pub is_oxker: bool, - pub last_updated: u64, - pub logs: Logs, - pub mem_limit: ByteStats, - pub mem_stats: VecDeque, - pub name: ContainerName, - pub ports: Vec, - pub rx: NetworkBandwidth, - pub state: State, - pub status: ContainerStatus, - pub tx: NetworkBandwidth, -} - -/// Basic display information, for when running in debug mode -impl fmt::Display for ContainerItem { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}, {}, {}, {}", - self.id.get_short(), - self.name, - self.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)), - self.mem_stats.back().unwrap_or(&ByteStats::new(0)) - ) - } -} - -impl ContainerItem { - #[allow(clippy::too_many_arguments)] - /// Create a new container item - pub fn new( - created: u64, - id: ContainerId, - image: String, - is_oxker: bool, - name: String, - ports: Vec, - state: State, - status: ContainerStatus, - ) -> Self { - let mut docker_controls = StatefulList::new(DockerCommand::gen_vec(state)); - docker_controls.start(); - - Self { - cpu_stats: VecDeque::with_capacity(60), - created, - docker_controls, - health: None, - id, - image: image.into(), - is_oxker, - last_updated: 0, - logs: Logs::default(), - mem_limit: ByteStats::default(), - mem_stats: VecDeque::with_capacity(60), - name: name.into(), - ports, - rx: NetworkBandwidth::new(), - state, - status, - tx: NetworkBandwidth::new(), - } - } - - /// Find the max value in the cpu stats VecDeque - fn max_cpu_stats(&self) -> CpuStats { - self.cpu_stats - .iter() - .max() - .map_or_else(CpuStats::default, |value| *value) - } - - /// Find the max value in the mem stats VecDeque - fn max_mem_stats(&self) -> ByteStats { - self.mem_stats - .iter() - .max() - .map_or_else(ByteStats::default, |value| *value) - } - - /// Convert cpu stats into a vec for the charts function - #[allow(clippy::cast_precision_loss)] - fn get_cpu_dataset(&self) -> Vec<(f64, f64)> { - self.cpu_stats - .iter() - .enumerate() - .map(|(i, v)| (i as f64, v.0)) - .collect::>() - } - - /// Convert mem stats into a Vec for the charts function - #[allow(clippy::cast_precision_loss)] - fn get_mem_dataset(&self) -> Vec<(f64, f64)> { - self.mem_stats - .iter() - .enumerate() - .map(|(i, v)| (i as f64, v.0 as f64)) - .collect::>() - } - - /// Get all cpu chart data - fn get_cpu_chart_data(&self) -> ChartSeries { - ChartSeries { - dataset: self.get_cpu_dataset(), - max: self.max_cpu_stats(), - current: self - .cpu_stats - .back() - .map_or_else(|| CpuStats::new(0.0), |i| *i), - } - } - - /// Get all mem chart data - fn get_mem_chart_data(&self) -> ChartSeries { - ChartSeries { - dataset: self.get_mem_dataset(), - max: self.max_mem_stats(), - current: self - .mem_stats - .back() - .map_or_else(|| ByteStats::new(0), |i| *i), - } - } - - /// Get all mem chart data - /// Don't understand what we are doing here - fn get_bandwidth_chart_tx_data(&self) -> ChartSeries { - let data = self.tx.to_vec_f64(); - ChartSeries { - current: BandwidthStat(data.last().map_or(0, |i| i.1 as u64)), - dataset: data, - max: self.tx.max(), - } - } - - /// Get all mem chart data - fn get_bandwidth_chart_rx_data(&self) -> ChartSeries { - let data = self.rx.to_vec_f64(); - ChartSeries { - current: BandwidthStat(data.last().map_or(0, |i| i.1 as u64)), - dataset: data, - max: self.rx.max(), - } - } - - /// Get chart info for cpu & memory in one function - /// So only need to call .lock() once - pub fn get_chart_data(&self) -> ChartsData { - ChartsData { - memory: self.get_mem_chart_data(), - cpu: self.get_cpu_chart_data(), - rx: self.get_bandwidth_chart_rx_data(), - tx: self.get_bandwidth_chart_tx_data(), - state: self.state, - } - } -} - -/// Container information panel headings + widths, for nice pretty formatting -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Columns { - pub name: (Header, u8), - pub state: (Header, u8), - pub status: (Header, u8), - pub cpu: (Header, u8), - pub mem: (Header, u8, u8), - pub id: (Header, u8), - pub image: (Header, u8), - pub net_rx: (Header, u8), - pub net_tx: (Header, u8), -} - -impl Columns { - /// (Column titles, minimum header string length) - pub const fn new() -> Self { - Self { - name: (Header::Name, 4), - state: (Header::State, 5), - status: (Header::Status, 6), - cpu: (Header::Cpu, 3), - mem: (Header::Memory, 7, 7), - id: (Header::Id, 8), - image: (Header::Image, 5), - net_rx: (Header::Rx, 4), - net_tx: (Header::Tx, 4), - } - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod tests { - - use jiff::tz::TimeZone; - use ratatui::{ - layout::Size, - text::{Line, Text}, - }; - - use crate::{ - app_data::{ContainerImage, LogSearch, Logs, LogsTz, RunningState}, - ui::log_sanitizer, - }; - - use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, State}; - - #[test] - /// Display CpuStats as a string - fn test_container_state_cpustats_to_string() { - let test = |f: f64, s: &str| { - assert_eq!(CpuStats::new(f).to_string(), s); - }; - - test(0.0, "00.00%"); - test(1.5, "01.50%"); - test(15.15, "15.15%"); - test(150.15, "150.15%"); - } - - #[test] - /// Display bytestats as a string, convert into correct data unit (Kb, MB, GB) - fn test_container_state_bytestats_to_string() { - let test = |u: u64, s: &str| { - assert_eq!(ByteStats::new(u).to_string(), s); - }; - - test(0, "0.00 kB"); - test(150, "0.15 kB"); - test(1500, "1.50 kB"); - test(150_000, "150.00 kB"); - test(1_500_000, "1.50 MB"); - test(15_000_000, "15.00 MB"); - test(150_000_000, "150.00 MB"); - test(1_500_000_000, "1.50 GB"); - test(15_000_000_000, "15.00 GB"); - test(150_000_000_000, "150.00 GB"); - } - - #[test] - /// ContainerName as string truncated correctly - fn test_container_state_container_name_to_string() { - let result = ContainerName::from("name_01"); - assert_eq!(result.to_string(), "name_01"); - - let result = ContainerName::from("name_01_name_01_name_01_name_01_"); - assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…"); - - let result = result.get(); - assert_eq!(result, "name_01_name_01_name_01_name_01_"); - } - - #[test] - /// ContainerImage as string truncated correctly - fn test_container_state_container_image() { - let result = ContainerImage::from("name_01"); - assert_eq!(result.to_string(), "name_01"); - - let result = ContainerImage::from("name_01_name_01_name_01_name_01_"); - assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…"); - - let result = result.get(); - assert_eq!(result, "name_01_name_01_name_01_name_01_"); - } - - #[test] - /// LogzTz correctly splits a line by timestamp - fn test_container_state_logz_splitter() { - let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet"; - let log_tz = LogsTz::splitter(input); - - assert_eq!( - log_tz.0, - super::LogsTz("2023-01-14T12:01:20.012345678Z".to_owned()) - ); - assert_eq!(log_tz.1, "Lorem ipsum dolor sit amet"); - } - - #[test] - /// LogsTz display correctly formats with a given timestamp string - fn test_container_state_logz_display() { - let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet"; - let log_tz = LogsTz::splitter(input); - - let result = log_tz - .0 - .display_with_formatter(None, "%Y-%m-%dT%H:%M:%S.%8f"); - assert!(result.is_some()); - let result = result.unwrap(); - assert_eq!(result, "2023-01-14T12:01:20.01234567"); - - let result = log_tz.0.display_with_formatter(None, "%Y-%m-%d %H:%M:%S"); - assert!(result.is_some()); - let result = result.unwrap(); - assert_eq!(result, "2023-01-14 12:01:20"); - - let result = log_tz.0.display_with_formatter(None, "%Y-%j"); - assert!(result.is_some()); - let result = result.unwrap(); - - assert_eq!(result, "2023-014"); - } - - #[test] - /// LogsTz display correctly formats with a given timestamp string & timezone - fn test_container_state_logz_display_with_timezone() { - let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet"; - let log_tz = LogsTz::splitter(input); - - let timezone = Some(TimeZone::get("Asia/Tokyo").unwrap()); - let result = log_tz - .0 - .display_with_formatter(timezone.as_ref(), "%Y-%m-%dT%H:%M:%S.%8f"); - assert!(result.is_some()); - let result = result.unwrap(); - assert_eq!(result, "2023-01-14T21:01:20.01234567"); - - let result = log_tz - .0 - .display_with_formatter(timezone.as_ref(), "%Y-%m-%d %H:%M:%S"); - assert!(result.is_some()); - let result = result.unwrap(); - assert_eq!(result, "2023-01-14 21:01:20"); - - let result = log_tz.0.display_with_formatter(timezone.as_ref(), "%Y-%j"); - assert!(result.is_some()); - let result = result.unwrap(); - assert_eq!(result, "2023-014"); - } - - #[test] - /// Logs can only contain 1 entry per LogzTz - fn test_container_state_logz() { - let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet"; - let (tz, _) = LogsTz::splitter(input); - let mut logs = Logs::default(); - let line = log_sanitizer::remove_ansi(input); - - logs.insert(Text::from(line.clone()), tz.clone(), true); - logs.insert(Text::from(line.clone()), tz.clone(), true); - logs.insert(Text::from(line), tz, true); - - assert_eq!(logs.lines.items.len(), 1); - - let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet"; - let (tz, _) = LogsTz::splitter(input); - let line = log_sanitizer::remove_ansi(input); - - logs.insert(Text::from(line.clone()), tz.clone(), true); - logs.insert(Text::from(line.clone()), tz.clone(), true); - logs.insert(Text::from(line), tz, true); - - assert_eq!(logs.lines.items.len(), 2); - } - - #[test] - /// check ContainerStatus unhealthy state - fn test_container_state_unhealthy() { - let input = ContainerStatus::from("Up 1 hour".to_owned()); - - assert!(!input.unhealthy()); - - let input = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); - - assert!(input.unhealthy()); - } - - #[test] - /// Generate container State from a &str and &ContainerStatus - fn test_container_status_unhealthy() { - let healthy = ContainerStatus::from("Up 1 hour".to_owned()); - let unhealthy = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); - - // Running and healthy - let input = State::from(("running", &healthy)); - assert_eq!(input, State::Running(RunningState::Healthy)); - - // Running and unhealthy - let input = State::from(("running", &unhealthy)); - assert_eq!(input, State::Running(RunningState::Unhealthy)); - - // Dead - let input = State::from(("dead", &healthy)); - assert_eq!(input, State::Dead); - - // Exited - let input = State::from(("exited", &healthy)); - assert_eq!(input, State::Exited); - - // Paused - let input = State::from(("paused", &healthy)); - assert_eq!(input, State::Paused); - - // Removing - let input = State::from(("removing", &healthy)); - assert_eq!(input, State::Removing); - - // Restarting - let input = State::from(("restarting", &healthy)); - assert_eq!(input, State::Restarting); - - // Unknown - let input = State::from(("oxker", &healthy)); - assert_eq!(input, State::Unknown); - } - - #[test] - /// Test the format_log_line methods, should ideally check colours are being correct kept as well - fn test_to_vec() { - let mut logs = Logs::default(); - - let input = "2023-01-14T19:13:30.783138328Z Hello world some long line".to_owned(); - let (tz, _) = LogsTz::splitter(&input); - logs.insert(Text::from(input), tz, true); - - let input = "2023-01-14T19:13:31.783138328Z Hello world some line".to_owned(); - let (tz, _) = LogsTz::splitter(&input); - logs.insert(Text::from(input), tz, true); - - let input = "2023-01-14T19:13:32.783138328Z Hello world".to_owned(); - let (tz, _) = LogsTz::splitter(&input); - logs.insert(Text::from(input), tz, true); - - logs.offset = 43; - let result = logs.get_visible_logs( - Size { - width: 14, - height: 10, - }, - 10, - ); - assert_eq!( - vec![ - Text::from(Line::from("some long line")), - Text::from(Line::from("some line")), - Text::from(Line::default()) - ], - result - ); - } - - #[test] - /// Test the get_scroll_title methods - fn test_scroll_title() { - let mut logs = Logs::default(); - - let result = logs.get_scroll_title(10); - assert!(result.is_none()); - - let input = "short".to_owned(); - let (tz, _) = LogsTz::splitter(&input); - logs.insert(Text::from(input), tz, true); - - let result = logs.get_scroll_title(10); - assert!(result.is_none()); - - let input = "2023-01-14T19:13:30.783138328Z Hello world some long line".to_owned(); - let (tz, _) = LogsTz::splitter(&input); - logs.insert(Text::from(input), tz, true); - - let result = logs.get_scroll_title(10); - assert_eq!(result, Some(" 0/51 → ".to_owned())); - - logs.forward(10); - - let result = logs.get_scroll_title(10); - assert_eq!(result, Some(" ← 1/51 → ".to_owned())); - - for _ in 0..=49 { - logs.forward(10); - } - let result = logs.get_scroll_title(10); - assert_eq!(result, Some(" ← 51/51 ".to_owned())); - } - - #[test] - /// Test the log search - fn test_logsearch() { - let mut logs = Logs::default(); - - for i in 1..=10 { - let input = if i % 2 == 0 { - format!("{i}, hello world some long line {i}") - } else { - format!("{i}, Hello world some long line {i}") - }; - let (tz, _) = LogsTz::splitter(&input); - logs.insert(Text::from(input), tz, true); - } - - logs.search_term_push('H', true); - assert_eq!(logs.search_results, [0, 2, 4, 6, 8]); - logs.search_term_clear(); - logs.search_term_push('H', false); - assert_eq!(logs.search_results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); - } - - #[test] - /// Test the LogSearch::From() methods - fn test_logsearch_from() { - let mut logs = Logs::default(); - - for i in 1..=10 { - let input = format!("{i}, Hello world some long line {i}"); - let (tz, _) = LogsTz::splitter(&input); - logs.insert(Text::from(input), tz, true); - } - - let log_search = LogSearch::from(&logs); - assert_eq!( - log_search, - LogSearch { - term: None, - result: None, - buttons: None - } - ); - - logs.search_term_push('H', true); - let log_search = LogSearch::from(&logs); - assert_eq!( - log_search, - LogSearch { - term: Some("H".to_owned()), - result: Some("10/10".to_owned()), - buttons: Some(crate::app_data::LogsButton::Previous) - } - ); - - logs.previous(); - - let log_search = LogSearch::from(&logs); - assert_eq!( - log_search, - LogSearch { - term: Some("H".to_owned()), - result: Some(" 9/10".to_owned()), - buttons: Some(crate::app_data::LogsButton::Both) - } - ); - - logs.start(); - - let log_search = LogSearch::from(&logs); - assert_eq!( - log_search, - LogSearch { - term: Some("H".to_owned()), - result: Some(" 1/10".to_owned()), - buttons: Some(crate::app_data::LogsButton::Next) - } - ); - - logs.search_term_push('H', true); - let log_search = LogSearch::from(&logs); - assert_eq!( - log_search, - LogSearch { - term: Some("HH".to_owned()), - result: None, - buttons: None - } - ); - - logs.search_term_clear(); - logs.search_term_push('2', true); - let log_search = LogSearch::from(&logs); - assert_eq!(logs.lines.state.selected(), Some(1)); - assert_eq!( - log_search, - LogSearch { - term: Some("2".to_owned()), - result: Some("1/1".to_owned()), - buttons: None - } - ); - - logs.next(); - - let log_search = LogSearch::from(&logs); - assert_eq!( - log_search, - LogSearch { - term: Some("2".to_owned()), - result: Some("1".to_owned()), - buttons: Some(crate::app_data::LogsButton::Previous) - } - ); - } -} diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs deleted file mode 100644 index 9e05986..0000000 --- a/src/app_data/mod.rs +++ /dev/null @@ -1,2571 +0,0 @@ -use bollard::{models::ContainerSummary, secret::ContainerInspectResponse}; -use core::fmt; -use parking_lot::Mutex; -use ratatui::{layout::Size, text::Text, widgets::ListState}; -use std::{ - hash::Hash, - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; - -mod container_state; - -use crate::{ - ENTRY_POINT, - app_error::AppError, - config::Config, - ui::{GuiState, Rerender, Status, log_sanitizer}, -}; -pub use container_state::*; - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum SortedOrder { - Asc, - Desc, -} - -#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] -pub enum Header { - State, - Status, - Cpu, - Memory, - Id, - Name, - Image, - Rx, - Tx, -} - -/// Convert Header enum into strings to display -impl fmt::Display for Header { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let disp = match self { - Self::State => "state", - Self::Status => "status", - Self::Cpu => "cpu", - Self::Memory => "memory/limit", - Self::Id => "id", - Self::Name => "name", - Self::Image => "image", - Self::Rx => "↓ rx", - Self::Tx => "↑ tx", - }; - write!(f, "{disp:>x$}", x = f.width().unwrap_or(1)) - } -} - -#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum FilterBy { - #[default] - Name, - Image, - Status, - All, -} - -/// Convert errors into strings to display -impl fmt::Display for FilterBy { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::Name => "Name", - Self::Image => "Image", - Self::Status => "Status", - Self::All => "All", - } - ) - } -} - -impl FilterBy { - const fn next(self) -> Option { - match self { - Self::Name => Some(Self::Image), - Self::Image => Some(Self::Status), - Self::Status => Some(Self::All), - Self::All => None, - } - } - - const fn prev(self) -> Option { - match self { - Self::Name => None, - Self::Image => Some(Self::Name), - Self::Status => Some(Self::Image), - Self::All => Some(Self::Status), - } - } -} - -#[derive(Debug, Clone)] -pub struct Filter { - pub term: Option, - pub by: FilterBy, -} -impl Filter { - pub fn new() -> Self { - Self { - term: None, - by: FilterBy::default(), - } - } -} - -#[derive(Debug, Clone)] -pub struct InspectData { - pub width: usize, - pub height: usize, - pub as_string: String, - pub name: String, - pub id: ContainerId, // pub as_lines: Vec>, -} - -impl From for InspectData { - fn from(input: ContainerInspectResponse) -> Self { - let as_string = serde_json::to_string_pretty(&input) - .unwrap_or_default() - .lines() - .skip(1) - .collect::>() - .split_last() - .map(|(_, data)| data) - .unwrap_or_default() - .join("\n"); - - let height = as_string.lines().count(); - - let mut width = 0; - for i in as_string.lines() { - width = width.max(i.chars().count()); - } - - Self { - name: input.name.unwrap_or_default(), - // TODO maybe make this an Option? - id: ContainerId::from(input.id.unwrap_or_default().as_str()), - width, - height, - as_string, - } - } -} - -/// Global app_state, stored in an Arc -#[derive(Debug, Clone)] -#[cfg(not(test))] -pub struct AppData { - containers: StatefulList, - error: Option, - filter: Filter, - hidden_containers: Vec, - inspect_data: Option, - rerender: Arc, - sorted_by: Option<(Header, SortedOrder)>, - current_sorted_id: Vec, - pub config: Config, -} - -#[derive(Debug, Clone)] -#[cfg(test)] -pub struct AppData { - pub config: Config, - pub containers: StatefulList, - pub error: Option, - pub filter: Filter, - pub hidden_containers: Vec, - pub inspect_data: Option, - pub current_sorted_id: Vec, - pub rerender: Arc, - pub sorted_by: Option<(Header, SortedOrder)>, -} - -impl AppData { - /// Generate a default app_state - pub fn new(config: Config, redraw: &Arc) -> Self { - Self { - config, - containers: StatefulList::new(vec![]), - current_sorted_id: vec![], - error: None, - filter: Filter::new(), - hidden_containers: vec![], - inspect_data: None, - rerender: Arc::clone(redraw), - sorted_by: None, - } - } - - /// Current time as unix timestamp - #[allow(clippy::expect_used)] - fn get_systemtime() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("In our known reality, this error should never occur") - .as_secs() - } - - pub fn clear_inspect_data(&mut self) { - self.inspect_data = None; - } - - pub fn set_inspect_data(&mut self, data: ContainerInspectResponse) { - self.inspect_data = Some(InspectData::from(data)) - // self.inspect_data = Some(data) - } - - pub fn get_inspect_data(&self) -> Option { - self.inspect_data.clone() - } - /// Filter related methods - /// Get the filterby and filter_term - pub const fn get_filter(&self) -> (FilterBy, Option<&String>) { - (self.filter.by, self.filter.term.as_ref()) - } - - pub fn log_search_scroll(&mut self, np: &ScrollDirection) { - if let Some(i) = self.get_mut_selected_container() - && i.logs.search_scroll(np).is_some() - { - self.rerender.update_draw(); - } - } - - pub fn gen_log_search(&self) -> Option { - self.get_selected_container() - .map(|i| i.logs.gen_log_search()) - } - - /// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by - fn can_insert(&self, container: &ContainerItem) -> bool { - self.filter.term.as_ref().is_none_or(|term| { - let term = term.to_lowercase(); - match self.filter.by { - FilterBy::All => { - container.name.contains(&term) - || container.image.contains(&term) - || container.status.contains(&term) - } - FilterBy::Image => container.image.contains(&term), - FilterBy::Name => container.name.contains(&term), - FilterBy::Status => container.status.contains(&term), - } - }) - } - - /// Remove items from the containers list based on the filter term, and insert into a "hidden" vec - /// sets the state to start if any filtering has occurred - /// Also search in the "hidden" vec for items and insert back into the main containers vec - fn filter_containers(&mut self) { - self.rerender.update_draw(); - let pre_len = self.get_container_len(); - - if !self.hidden_containers.is_empty() { - let (mut new_items, tmp_items): (Vec<_>, Vec<_>) = self - .hidden_containers - .iter() - .cloned() - .partition(|item| self.can_insert(item)); - - while let Some(x) = new_items.pop() { - self.containers.items.push(x); - } - self.hidden_containers = tmp_items; - } - - let (new_items, tmp_items) = self - .containers - .items - .iter() - .cloned() - .partition(|item| self.can_insert(item)); - - self.containers.items = new_items; - self.hidden_containers.extend(tmp_items); - - self.sort_containers(); - if self.get_container_len() != pre_len { - self.containers.start(); - } - } - - pub fn logs_search_clear(&mut self) { - if let Some(selected_container) = self.get_mut_selected_container() { - selected_container.logs.search_term_clear(); - self.rerender.update_draw(); - } - } - - /// Set a single char into the filter term - pub fn log_search_push(&mut self, c: char) { - let cs = self.config.log_search_case_sensitive; - if let Some(selected_container) = self.get_mut_selected_container() { - selected_container.logs.search_term_push(c, cs); - self.rerender.update_draw(); - } - } - - /// Delete the final char of the filter term - pub fn log_search_pop(&mut self) { - let cs = self.config.log_search_case_sensitive; - if let Some(selected_container) = self.get_mut_selected_container() { - selected_container.logs.search_term_pop(cs); - self.rerender.update_draw(); - } - } - - /// Re-filter the containers, used after the filter.by has been changed - fn re_filter(&mut self) { - self.containers.items.append(&mut self.hidden_containers); - self.hidden_containers = vec![]; - self.filter_containers(); - } - - /// Set a single char into the filter term - pub fn filter_term_push(&mut self, c: char) { - if let Some(term) = self.filter.term.as_mut() { - term.push(c); - } else { - self.filter.term = Some(format!("{c}")); - } - self.filter_containers(); - } - - /// Delete the final char of the filter term - pub fn filter_term_pop(&mut self) { - if let Some(term) = self.filter.term.as_mut() { - // should now search for items in the tmp vec, and insert into containers if found - term.pop(); - if term.is_empty() { - self.filter.term = None; - } - } - self.filter_containers(); - } - - /// change the filter_by option - pub fn filter_by_next(&mut self) { - if let Some(by) = self.filter.by.next() { - self.filter.by = by; - self.re_filter(); - } - } - - /// change the filter_by option - pub fn filter_by_prev(&mut self) { - if let Some(by) = self.filter.by.prev() { - self.filter.by = by; - self.re_filter(); - } - } - - /// Remove the filter completely - pub fn filter_term_clear(&mut self) { - self.filter.term = None; - while let Some(i) = self.hidden_containers.pop() { - if self.get_container_by_id(&i.id).is_none() { - self.containers.items.push(i); - } - } - self.sort_containers(); - } - - /// Container sort related methods - /// Change the sorted order, also set the selected container state to match new order - fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) { - self.sorted_by = x; - self.sort_containers(); - self.containers.state.select( - self.containers - .items - .iter() - .position(|i| self.get_selected_container_id().as_ref() == Some(&i.id)), - ); - self.rerender.update_draw(); - } - - /// Remove the sorted header & order, and sort by default - created datetime - pub fn reset_sorted(&mut self) { - self.set_sorted(None); - self.rerender.update_draw(); - } - - /// Sort containers based on a given header, if headings match, and already ascending, remove sorting - pub fn set_sort_by_header(&mut self, selected_header: Header) { - let mut output = Some((selected_header, SortedOrder::Asc)); - if let Some((current_header, order)) = self.get_sorted() - && current_header == selected_header - { - match order { - SortedOrder::Desc => output = None, - SortedOrder::Asc => output = Some((selected_header, SortedOrder::Desc)), - } - } - self.set_sorted(output); - } - - pub const fn get_sorted(&self) -> Option<(Header, SortedOrder)> { - self.sorted_by - } - - /// Get a vec of the containers ID's in the order they are displayed in the containers panel - fn get_current_ids(&self) -> Vec { - self.containers - .items - .iter() - .map(|i| i.id.clone()) - .collect::>() - } - /// Sort the containers vec, based on a heading (and if clash, then by name), either ascending or descending, - /// If not sort set, then sort by created time - pub fn sort_containers(&mut self) { - if let Some((head, ord)) = self.sorted_by { - let pre_order = self.get_current_ids(); - let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering { - let item_ord = match ord { - SortedOrder::Asc => (a, b), - SortedOrder::Desc => (b, a), - }; - match head { - Header::State => item_ord - .0 - .state - .order() - .cmp(&item_ord.1.state.order()) - .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Status => item_ord - .0 - .status - .get() - .cmp(item_ord.1.status.get()) - .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Cpu => item_ord - .0 - .cpu_stats - .back() - .cmp(&item_ord.1.cpu_stats.back()) - .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Memory => item_ord - .0 - .mem_stats - .back() - .cmp(&item_ord.1.mem_stats.back()) - .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Id => item_ord - .0 - .id - .cmp(&item_ord.1.id) - .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Image => item_ord - .0 - .image - .get() - .cmp(item_ord.1.image.get()) - .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Rx => item_ord - .0 - .rx - .current_total() - .cmp(&item_ord.1.rx.current_total()) - .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Tx => item_ord - .0 - .tx - .current_total() - .cmp(&item_ord.1.tx.current_total()) - .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Name => item_ord - .0 - .name - .get() - .cmp(item_ord.1.name.get()) - .then_with(|| item_ord.0.id.cmp(&item_ord.1.id)), - } - }; - - self.containers.items.sort_by(sort_closure); - if pre_order != self.get_current_ids() { - self.rerender.update_draw(); - } - } else if self.current_sorted_id != self.get_current_ids() { - self.containers.items.sort_by(|a, b| { - a.created - .cmp(&b.created) - .then_with(|| a.name.get().cmp(b.name.get())) - }); - self.rerender.update_draw(); - self.current_sorted_id = self.get_current_ids(); - } - } - - /// Container state methods - /// Get the total number of none "hidden" containers - pub const fn get_container_len(&self) -> usize { - self.containers.items.len() - } - - pub fn get_all_id_state(&self) -> Vec<(State, ContainerId)> { - self.containers - .items - .iter() - .map(|i| (i.state, i.id.clone())) - .collect::>() - } - - /// Get all the ContainerItems - /// Thnk this allow block can be removed with the 1.87 release of Clippy - pub fn get_container_items(&self) -> &[ContainerItem] { - &self.containers.items - } - - /// Get title for containers section, add a suffix indicating if the containers are currently under filter - pub fn get_container_title(&self) -> String { - let suffix = if !self.hidden_containers.is_empty() && !self.containers.items.is_empty() { - " - filtered" - } else { - "" - }; - format!("{}{}", self.containers.get_state_title(), suffix) - } - - /// Select the first container - pub fn containers_start(&mut self) { - self.containers.start(); - self.rerender.update_draw(); - } - - /// select the last container - pub fn containers_end(&mut self) { - self.containers.end(); - self.rerender.update_draw(); - } - - pub fn containers_scroll(&mut self, scroll: &ScrollDirection) { - self.containers.scroll(scroll); - self.rerender.update_draw(); - } - - /// Get ListState of containers - pub const fn get_container_state(&mut self) -> &mut ListState { - &mut self.containers.state - } - - /// Get Option of the current selected container - pub fn get_selected_container(&self) -> Option<&ContainerItem> { - self.containers - .state - .selected() - .and_then(|i| self.containers.items.get(i)) - } - - /// Find the longest port when it's transformed into a string, defaults are header lens (ip, private, public) - ///display like this: "│ ip, private, public│", so (5,10,9) are the minimum lengths required - pub fn get_longest_port(&self) -> (usize, usize, usize) { - let mut output = (5, 10, 9); - - for item in [&self.containers.items, &self.hidden_containers] { - for item in item { - output.0 = output.0.max( - item.ports - .iter() - .map(ContainerPorts::len_ip) - .max() - .unwrap_or(output.0), - ); - output.1 = output.1.max( - item.ports - .iter() - .map(ContainerPorts::len_private) - .max() - .unwrap_or(output.1), - ); - output.2 = output.2.max( - item.ports - .iter() - .map(ContainerPorts::len_public) - .max() - .unwrap_or(output.2), - ); - } - } - output - } - - /// Get Option of the current selected container's ports, sorted by private port - pub fn get_selected_ports(&self) -> Option<(Vec, State)> { - if let Some(item) = self.get_selected_container() { - let mut ports = item.ports.clone(); - ports.sort_by(|a, b| a.private.cmp(&b.private)); - return Some((ports, item.state)); - } - None - } - - /// Get mutable Option of the current selected container - fn get_mut_selected_container(&mut self) -> Option<&mut ContainerItem> { - self.containers - .state - .selected() - .and_then(|i| self.containers.items.get_mut(i)) - } - - /// Get a mutable container by given id - #[cfg(not(test))] - fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { - self.containers.items.iter_mut().find(|i| &i.id == id) - } - - /// As above, but make it public to testing - #[cfg(test)] - pub fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { - self.containers.items.iter_mut().find(|i| &i.id == id) - } - - /// Get a mutable container by given id in the tmp_container vec - fn get_hidden_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { - self.hidden_containers.iter_mut().find(|i| &i.id == id) - } - - /// Get the ContainerName of by ID - pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<&ContainerName> { - self.containers - .items - .iter_mut() - .find(|i| &i.id == id) - .map(|i| &i.name) - } - - /// Find the id of the currently selected container. - /// If any containers on system, will always return a ContainerId - /// Only returns None when no containers found. - pub fn get_selected_container_id(&self) -> Option { - self.get_selected_container().map(|i| i.id.clone()) - } - - /// Check if a given ID matches the currently selected container - pub fn is_selected_container(&self, id: &ContainerId) -> bool { - self.get_selected_container().is_some_and(|i| &i.id == id) - } - - /// Get the Id and State for the currently selected container - used by the exec check method - pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> { - self.get_selected_container() - .map(|i| (i.id.clone(), i.state, i.name.get().to_owned())) - } - - /// Selected DockerCommand methods - /// Get the current selected docker command - /// So know which command to execute - pub fn selected_docker_controls(&self) -> Option { - self.get_selected_container().and_then(|i| { - i.docker_controls.state.selected().and_then(|x| { - i.docker_controls - .items - .get(x) - .map(std::borrow::ToOwned::to_owned) - }) - }) - } - - /// Change selected choice of docker commands of selected container - pub fn docker_controls_scroll(&mut self, scroll: &ScrollDirection) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.scroll(scroll); - // i.docker_controls.next(); - self.rerender.update_draw(); - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_controls_start(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.start(); - self.rerender.update_draw(); - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_controls_end(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.end(); - self.rerender.update_draw(); - } - } - - /// Get mutable Option of the currently selected container DockerCommand state - pub fn get_control_state(&mut self) -> Option<&mut ListState> { - self.get_mut_selected_container() - .map(|i| &mut i.docker_controls.state) - } - - /// Get mutable Option of the currently selected container DockerCommand items - pub fn get_control_items(&mut self) -> Option<&mut Vec> { - self.get_mut_selected_container() - .map(|i| &mut i.docker_controls.items) - } - - /// Logs related methods - /// Get the title for log panel for selected container, will be either - /// 1) "logs x/x - container_name - container_image" - /// 2) "logs - container_name - container_image" when no logs found - /// 3) " " no container currently selected - aka no containers on system - pub fn get_log_title(&self) -> String { - self.get_selected_container() - .map_or_else(String::new, |ci| { - let logs_len = ci.logs.get_state_title(); - let prefix = if logs_len.is_empty() { - String::from(" ") - } else { - format!("{logs_len} ") - }; - format!("{}- {} - {}", prefix, ci.name.get(), ci.image.get()) - }) - } - - /// If scrolling horizontally along the logs, display a counter of the position in the in the scroll, `x/y` - pub fn get_scroll_title(&mut self, width: u16) -> Option { - self.get_mut_selected_container() - .and_then(|i| i.logs.get_scroll_title(width)) - } - - pub fn logs_horizontal_scroll(&mut self, sd: &ScrollDirection, width: u16) { - // Change this to set a max_offset, instead of taking in width each time, then can be combined with the log_scroll beneath - match sd { - ScrollDirection::Down => { - if let Some(i) = self.get_mut_selected_container() { - i.logs.forward(width); - self.rerender.update_draw(); - } - } - ScrollDirection::Up => { - if let Some(i) = self.get_mut_selected_container() { - i.logs.back(); - self.rerender.update_draw(); - } - } - // TODO set offset - _ => (), - } - } - - /// select next selected log line - pub fn log_scroll(&mut self, scroll: &ScrollDirection) { - if let Some(i) = self.get_mut_selected_container() { - match scroll { - ScrollDirection::Down => i.logs.next(), - ScrollDirection::Up => i.logs.previous(), - // TODO set offset - _ => (), - } - self.rerender.update_draw(); - } - } - - /// select last selected log line - pub fn log_end(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.logs.end(); - self.rerender.update_draw(); - } - } - - /// select first selected log line - pub fn log_start(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.logs.start(); - self.rerender.update_draw(); - } - } - - /// Get mutable Vec of current containers logs - pub fn get_logs(&self, size: Size, padding: usize) -> Vec> { - self.containers - .state - .selected() - .and_then(|i| self.containers.items.get(i)) - .map_or(vec![], |i| i.logs.get_visible_logs(size, padding)) - } - - /// Get mutable Option of the currently selected container Logs state - pub fn get_log_state(&mut self) -> Option<&mut ListState> { - self.containers - .state - .selected() - .and_then(|i| self.containers.items.get_mut(i)) - .map(|i| i.logs.state()) - } - - /// Chart data related methods - /// Get mutable Option of the currently selected container chart data - pub fn get_chart_data(&self) -> Option { - self.containers - .state - .selected() - .and_then(|i| self.containers.items.get(i)) - .map(container_state::ContainerItem::get_chart_data) - } - - /// Error related methods - /// Get single app_state error - pub fn get_error(&self) -> Option { - self.error.clone() - } - - /// Remove single app_state error - pub fn remove_error(&mut self) { - self.error = None; - self.rerender.update_draw(); - } - - /// Insert single app_state error - pub fn set_error(&mut self, error: AppError, gui_state: &Arc>, status: Status) { - gui_state.lock().status_push(status); - self.error = Some(error); - self.rerender.update_draw(); - } - - /// Check if the selected container is a dockerised version of oxker - /// So that can disallow commands to be send - /// Is a shabby way of implementing this - pub fn is_oxker(&self) -> bool { - self.get_selected_container().is_some_and(|i| i.is_oxker) - } - - /// Check if selected container is oxker and also that oxker is being run in a container - pub fn is_oxker_in_container(&self) -> bool { - self.get_selected_container() - .is_some_and(|i| i.is_oxker && self.config.in_container) - } - - /// Find the widths for the strings in the containers panel. - /// So can display nicely and evenly - /// Searches in both contains & hidden_containers - pub fn get_width(&self) -> Columns { - let mut columns = Columns::new(); - let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12); - - for container in [&self.containers.items, &self.hidden_containers] { - for container in container { - // TODO refactor these - let cpu_count = container.cpu_stats.back().map_or_else( - || count(&CpuStats::default().to_string()), - |i| count(&i.to_string()), - ); - - let mem_current_count = container.mem_stats.back().map_or_else( - || count(&ByteStats::default().to_string()), - |i| count(&i.to_string()), - ); - columns.cpu.1 = columns.cpu.1.max(cpu_count); - columns.image.1 = columns.image.1.max(count(&container.image.to_string())); - columns.mem.1 = columns.mem.1.max(mem_current_count); - columns.mem.2 = columns.mem.2.max(count(&container.mem_limit.to_string())); - columns.name.1 = columns.name.1.max(count(&container.name.to_string())); - columns.net_rx.1 = columns - .net_rx - .1 - .max(count(&container.rx.current_total().to_string())); - columns.net_tx.1 = columns - .net_tx - .1 - .max(count(&container.tx.current_total().to_string())); - columns.state.1 = columns.state.1.max(count(&container.state.to_string())); - columns.status.1 = columns.status.1.max(count(container.status.get())); - } - } - columns - } - - /// Update related methods - /// Get mutable reference to a container in the containers vec & the hidden_containers vec - fn get_any_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { - if self.get_hidden_container_by_id(id).is_some() { - self.get_hidden_container_by_id(id) - } else { - self.get_container_by_id(id) - } - } - - /// Update container mem, cpu, & network stats, in single function so only need to call .lock() once - /// Will also, if a sort is set, sort the containers - pub fn update_stats_by_id( - &mut self, - id: &ContainerId, - cpu_stat: Option, - mem_stat: Option, - mem_limit: u64, - rx: u64, - tx: u64, - ) { - if let Some(container) = self.get_any_container_by_id(id) { - if container.cpu_stats.len() >= 60 { - container.cpu_stats.pop_front(); - } - if container.mem_stats.len() >= 60 { - container.mem_stats.pop_front(); - } - - if let Some(cpu) = cpu_stat { - container.cpu_stats.push_back(CpuStats::new(cpu)); - } - if let Some(mem) = mem_stat { - container.mem_stats.push_back(ByteStats::new(mem)); - } - - // Only insert if alive, or if is empty, need two to create an entry in the bandwidth chart, so instead this fills in the RX/TX total columns - if container.rx.is_empty() || container.state.is_alive() { - container.rx.push(rx); - container.tx.push(tx); - } - - container.mem_limit.update(mem_limit); - } - if self.is_selected_container(id) { - self.rerender.update_draw(); - } - self.sort_containers(); - } - - /// Update, or insert, containers - pub fn update_containers(&mut self, mut all_containers: Vec) { - let all_ids = self - .containers - .items - .iter() - .map(|i| i.id.clone()) - .collect::>(); - - // Only sort it no containers currently set, as afterwards the order is fixed - if self.containers.items.is_empty() { - all_containers.sort_by(|a, b| a.created.cmp(&b.created)); - } - - if !all_containers.is_empty() && self.containers.state.selected().is_none() { - self.containers.start(); - } - - for (index, id) in all_ids.iter().enumerate() { - if !all_containers - .iter() - .filter_map(|i| i.id.as_ref()) - .any(|x| x == id.get()) - { - // If removed container is currently selected, then change selected to previous - // This will default to 0 in any edge cases - if self.containers.state.selected().is_some() { - self.containers.scroll(&ScrollDirection::Up); - } - // Check is some, else can cause out of bounds error, if containers get removed before a docker update - if self.containers.items.get(index).is_some() { - self.containers.items.remove(index); - if self.is_selected_container(id) { - self.rerender.update_draw(); - } - } - } - } - - for mut i in all_containers { - if let Some(id) = i.id.as_ref() { - let name = i.names.as_mut().map_or(String::new(), |names| { - names.first_mut().map_or(String::new(), |f| { - if f.starts_with('/') { - f.remove(0); - } - (*f).clone() - }) - }); - - let ports = i.ports.map_or(vec![], |i| { - i.into_iter().map(ContainerPorts::from).collect::>() - }); - - let id = ContainerId::from(id.as_str()); - - let is_oxker = i - .command - .as_ref() - .is_some_and(|i| i.starts_with(ENTRY_POINT)); - - let status = ContainerStatus::from( - i.status - .as_ref() - .map_or(String::new(), std::clone::Clone::clone), - ); - let state = State::from(( - i.state - .as_ref() - .map_or(&bollard::secret::ContainerSummaryStateEnum::DEAD, |z| z), - &status, - )); - let image = i - .image - .as_ref() - .map_or(String::new(), std::clone::Clone::clone); - - let created = i - .created - .map_or(0, |i| u64::try_from(i).unwrap_or_default()); - - if let Some(item) = self.get_any_container_by_id(&id) { - if item.name.get() != name { - item.name.set(name); - } - if item.status != status { - item.status = status; - } - if item.state != state { - item.docker_controls.items = DockerCommand::gen_vec(state); - // Update the list state, needs to be None if the gen_vec returns an empty vec - match state { - State::Removing | State::Restarting | State::Unknown => { - item.docker_controls.state.select(None); - } - _ => item.docker_controls.start(), - } - item.state = state; - } - - item.ports = ports; - - if item.image.get() != image { - item.image.set(image); - } - } else { - // container not known, so make new ContainerItem and push into containers Ve - let container = ContainerItem::new( - created, id, image, is_oxker, name, ports, state, status, - ); - let can_insert = self.can_insert(&container); - if can_insert { - self.containers.items.push(container); - } else { - self.hidden_containers.push(container); - } - } - } - // self.redraw.set_true("update_containers"); - } - } - - /// Update logs of a given container, based on id - pub fn update_log_by_id(&mut self, logs: Vec, id: &ContainerId) { - let color = self.config.color_logs; - let raw = self.config.raw_logs; - let format = self.config.timestamp_format.clone(); - let config_tz = self.config.timezone.clone(); - - let cs = self.config.log_search_case_sensitive; - - let show_timestamp = self.config.show_timestamp; - - if let Some(container) = self.get_any_container_by_id(id) { - if !container.is_oxker { - container.last_updated = Self::get_systemtime(); - let current_len = container.logs.len(); - for mut i in logs { - let (log_tz, log_content) = LogsTz::splitter(i.as_str()); - if show_timestamp { - i = format!( - "{} {}", - log_tz - .display_with_formatter(config_tz.as_ref(), &format) - .unwrap_or_else(|| log_tz.to_string()), - log_content - ); - } else { - i = log_content; - } - let lines = if color { - log_sanitizer::colorize_logs(&i) - } else if raw { - log_sanitizer::raw(&i) - } else { - log_sanitizer::remove_ansi(&i) - }; - container.logs.insert(Text::from(lines), log_tz, cs); - } - - // Set the logs selected row for each container - // Either when no long currently selected, or currently selected (before updated) is already at end - if container.logs.state().selected().is_none() - || container.logs.state().selected().map_or(1, |f| f + 1) == current_len - { - container.logs.end(); - } - } - if self.is_selected_container(id) { - self.rerender.update_draw(); - } - } - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod tests { - - use super::*; - use crate::tests::{gen_appdata, gen_container_summary, gen_containers}; - use std::collections::VecDeque; - - // ******* // - // Sort by // - // ******* // - - #[test] - /// Sort by header: name - fn test_app_data_set_sort_by_header_name() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - // descending - app_data.set_sorted(Some((Header::Name, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("3")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("1")); - - // ascending - app_data.set_sorted(Some((Header::Name, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("1")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("3")); - } - - #[test] - /// Sort by header: state - fn test_app_data_set_sort_by_header_state() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { - i.state = State::Exited; - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - i.state = State::Running(RunningState::Healthy); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { - i.state = State::Paused; - } - - // descending - app_data.set_sorted(Some((Header::State, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("1")); - assert_eq!(b.id, ContainerId::from("3")); - assert_eq!(c.id, ContainerId::from("2")); - - // ascending - app_data.set_sorted(Some((Header::State, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("2")); - assert_eq!(b.id, ContainerId::from("3")); - assert_eq!(c.id, ContainerId::from("1")); - } - - #[test] - /// Sort by header: status - fn test_app_data_set_sort_by_header_status() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - ContainerStatus::from("Exited (0) 10 minutes ago".to_owned()).clone_into(&mut i.status); - } - - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { - // "Up 2 hours (Paused)".clone_into(&mut i.status); - ContainerStatus::from("Up 2 hours (Paused)".to_owned()).clone_into(&mut i.status); - } - - // Sort by status - // descending - app_data.set_sorted(Some((Header::Status, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("3")); - assert_eq!(b.id, ContainerId::from("1")); - assert_eq!(c.id, ContainerId::from("2")); - - // ascending - app_data.set_sorted(Some((Header::Status, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("2")); - assert_eq!(b.id, ContainerId::from("1")); - assert_eq!(c.id, ContainerId::from("3")); - } - - #[test] - /// Sort by header: cpu - fn test_app_data_set_sort_by_header_cpu() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { - i.cpu_stats = VecDeque::from([CpuStats::new(10.1)]); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - i.cpu_stats = VecDeque::from([CpuStats::new(8.1)]); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { - i.cpu_stats = VecDeque::from([CpuStats::new(20.3)]); - } - - // descending - app_data.set_sorted(Some((Header::Cpu, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("3")); - assert_eq!(b.id, ContainerId::from("1")); - assert_eq!(c.id, ContainerId::from("2")); - - // ascending - app_data.set_sorted(Some((Header::Cpu, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("2")); - assert_eq!(b.id, ContainerId::from("1")); - assert_eq!(c.id, ContainerId::from("3")); - } - - #[test] - /// Sort by header: memory - fn test_app_data_set_sort_by_header_mem() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { - i.mem_stats = VecDeque::from([ByteStats::new(40)]); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - i.mem_stats = VecDeque::from([ByteStats::new(80)]); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { - i.mem_stats = VecDeque::from([ByteStats::new(2)]); - } - - // descending - app_data.set_sorted(Some((Header::Memory, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("2")); - assert_eq!(b.id, ContainerId::from("1")); - assert_eq!(c.id, ContainerId::from("3")); - - // ascending - app_data.set_sorted(Some((Header::Memory, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("3")); - assert_eq!(b.id, ContainerId::from("1")); - assert_eq!(c.id, ContainerId::from("2")); - } - - #[test] - /// Sort by header: id - fn test_app_data_set_sort_by_header_id() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - // descending - app_data.set_sorted(Some((Header::Id, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("3")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("1")); - - // ascending - app_data.set_sorted(Some((Header::Id, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("1")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("3")); - } - - #[test] - /// Sort by header: image - fn test_app_data_set_sort_by_header_image() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - // descending - app_data.set_sorted(Some((Header::Image, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("3")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("1")); - - // ascending - app_data.set_sorted(Some((Header::Image, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("1")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("3")); - } - - #[test] - /// Sort by header: rx - fn test_app_data_set_sort_by_header_rx() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(40); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(80); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(2); - } - - // descending - app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("2")); - assert_eq!(b.id, ContainerId::from("1")); - assert_eq!(c.id, ContainerId::from("3")); - - // ascending - app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("3")); - assert_eq!(b.id, ContainerId::from("1")); - assert_eq!(c.id, ContainerId::from("2")); - } - - #[test] - /// Sort by header: tx - fn test_app_data_set_sort_by_header_tx() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(400); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(80); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(83); - } - - // descending - app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("1")); - assert_eq!(b.id, ContainerId::from("3")); - assert_eq!(c.id, ContainerId::from("2")); - - // ascending - app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("2")); - assert_eq!(b.id, ContainerId::from("3")); - assert_eq!(c.id, ContainerId::from("1")); - } - - #[test] - /// Sort by header when selected headers match - fn test_app_data_set_sort_by_header_match() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - // descending - app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("3")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("1")); - - // ascending - app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("1")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("3")); - } - - #[test] - /// reset sorted - fn test_app_data_reset_sorted() { - let (_ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result, &containers); - - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(400); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(80); - } - if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { - i.rx = NetworkBandwidth::new(); - i.rx.push(83); - } - - app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("2")); - assert_eq!(b.id, ContainerId::from("3")); - assert_eq!(c.id, ContainerId::from("1")); - - app_data.set_sorted(None); - let result = app_data.get_container_items(); - let (a, b, c) = (&result[0], &result[1], &result[2]); - assert_eq!(a.id, ContainerId::from("1")); - assert_eq!(b.id, ContainerId::from("2")); - assert_eq!(c.id, ContainerId::from("3")); - } - - // **************** // - // Container state // - // **************** // - - #[test] - /// Get len of current containers vec - fn test_app_data_get_container_len() { - let (_ids, containers) = gen_containers(); - let app_data = gen_appdata(&containers); - assert_eq!(app_data.get_container_len(), 3); - } - - #[test] - /// Select the first container - fn test_app_data_containers_start() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - // No container selected - let result = app_data.get_container_state(); - assert_eq!(result.selected(), None); - assert_eq!(result.offset(), 0); - - // First container selected - app_data.containers_start(); - let result = app_data.get_container_state(); - assert_eq!(result.selected(), Some(0)); - assert_eq!(result.offset(), 0); - - let result = app_data.get_selected_container_id(); - assert_eq!(result, Some(ContainerId::from("1"))); - let result = app_data.get_selected_container_id_state_name(); - assert_eq!( - result, - Some(( - ContainerId::from("1"), - State::Running(RunningState::Healthy), - "container_1".to_owned() - )) - ); - - // Calling previous when at start has no effect - app_data.containers_scroll(&ScrollDirection::Up); - let result = app_data.get_selected_container_id(); - assert_eq!(result, Some(ContainerId::from("1"))); - let result = app_data.get_selected_container_id_state_name(); - assert_eq!( - result, - Some(( - ContainerId::from("1"), - State::Running(RunningState::Healthy), - "container_1".to_owned() - )) - ); - } - - #[test] - /// advance container list state by one - /// get get_selected_container_id() & get_selected_container_id_state_name() return valid Some data - fn test_app_data_containers_next() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - // Advance list state by 1 - app_data.containers_start(); - app_data.containers.scroll(&ScrollDirection::Down); - - let result = app_data.get_container_state(); - assert_eq!(result.selected(), Some(1)); - assert_eq!(result.offset(), 0); - - let result = app_data.get_selected_container_id(); - assert_eq!(result, Some(ContainerId::from("2"))); - let result = app_data.get_selected_container_id_state_name(); - assert_eq!( - result, - Some(( - ContainerId::from("2"), - State::Running(RunningState::Healthy), - "container_2".to_owned() - )) - ); - } - - #[test] - /// advance container list state to the end - /// get get_selected_container_id() & get_selected_container_id_state_name() return valid Some data - fn test_app_data_containers_end() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - app_data.containers_end(); - let result = app_data.get_container_state(); - assert_eq!(result.selected(), Some(2)); - assert_eq!(result.offset(), 0); - - let result = app_data.get_selected_container_id(); - assert_eq!(result, Some(ContainerId::from("3"))); - let result = app_data.get_selected_container_id_state_name(); - assert_eq!( - result, - Some(( - ContainerId::from("3"), - State::Running(RunningState::Healthy), - "container_3".to_owned() - )) - ); - - // Calling previous when at end has no effect - app_data.containers.scroll(&ScrollDirection::Down); - let result = app_data.get_selected_container_id(); - assert_eq!(result, Some(ContainerId::from("3"))); - let result = app_data.get_selected_container_id_state_name(); - assert_eq!( - result, - Some(( - ContainerId::from("3"), - State::Running(RunningState::Healthy), - "container_3".to_owned() - )) - ); - } - - #[test] - /// go to previous container - fn test_app_data_containers_prev() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - app_data.containers_end(); - app_data.containers.scroll(&ScrollDirection::Up); - let result = app_data.get_container_state(); - assert_eq!(result.selected(), Some(1)); - assert_eq!(result.offset(), 0); - } - - #[test] - /// Get the currently selected container - fn test_app_data_get_selected_container() { - let (_ids, mut containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_selected_container(); - assert_eq!(result, None); - - app_data.containers.start(); - app_data.containers.scroll(&ScrollDirection::Down); - - let result = app_data.get_selected_container(); - assert_eq!(result, Some(&containers[1])); - - // As above, but now as mut - let result = app_data.get_mut_selected_container(); - assert_eq!(result, Some(&mut containers[1])); - } - - #[test] - /// Get mut container by id - fn test_app_data_get_container_by_id() { - let (_ids, mut containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_by_id(&ContainerId::from("2")); - assert_eq!(result, Some(&mut containers[1])); - } - - #[test] - /// Get just the containers name by id - fn test_app_data_get_container_name_by_id() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_name_by_id(&ContainerId::from("2")); - assert_eq!(result, Some(&ContainerName::from("container_2"))); - } - - #[test] - /// Get the id of the currently selected container - fn test_app_data_get_selected_container_id() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - app_data.containers_end(); - - let result = app_data.get_selected_container_id(); - assert_eq!(result, Some(ContainerId::from("3"))); - } - - #[test] - fn test_app_data_get_selected_container_id_state_name() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - app_data.containers_end(); - - let result = app_data.get_selected_container_id_state_name(); - assert_eq!( - result, - Some(( - ContainerId::from("3"), - State::Running(RunningState::Healthy), - "container_3".to_owned() - )) - ); - } - - // ************** // - // DockerControls // - // ************** // - - #[test] - /// Docker commands returned correctly - fn test_app_data_selected_docker_command() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - // No commands when no container selected - let result = app_data.selected_docker_controls(); - assert!(result.is_none()); - - // Correct commands returned - app_data.containers_start(); - app_data.docker_controls_start(); - - let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerCommand::Pause)); - } - - #[test] - /// Docker command next works - fn test_app_data_selected_docker_command_next() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - app_data.containers_start(); - app_data.docker_controls_start(); - app_data.docker_controls_scroll(&ScrollDirection::Down); - - let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerCommand::Restart)); - } - - #[test] - /// Dockercommand end works, and next has no effect when at end - fn test_app_data_selected_docker_command_end() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - app_data.containers_start(); - app_data.docker_controls_end(); - - let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerCommand::Delete)); - - // Next has no effect when at end - app_data.docker_controls_scroll(&ScrollDirection::Down); - let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerCommand::Delete)); - } - - #[test] - /// Docker commands previous works, and has no effect when at start - fn test_app_data_selected_docker_command_previous() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - app_data.containers_start(); - app_data.docker_controls_end(); - app_data.docker_controls_scroll(&ScrollDirection::Up); - - let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerCommand::Stop)); - - // previous has no effect when at start - app_data.docker_controls_start(); - app_data.docker_controls_scroll(&ScrollDirection::Up); - let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerCommand::Pause)); - } - - #[test] - /// DockerCommands get correct controls dependant on container state - fn test_app_data_get_control_items() { - let test_state = |state: State, expected: &mut Vec| { - let gen_item_state = |state: State| { - ContainerItem::new( - 1, - ContainerId::from("1"), - "image_1".to_owned(), - false, - "container_1".to_owned(), - vec![], - state, - ContainerStatus::from("Up 1 hour".to_owned()), - ) - }; - let mut app_data = gen_appdata(&[gen_item_state(state)]); - app_data.containers_start(); - app_data.docker_controls_start(); - - let result = app_data.get_control_items(); - assert_eq!(result, Some(expected)); - }; - - test_state( - State::Dead, - &mut vec![ - DockerCommand::Start, - DockerCommand::Restart, - DockerCommand::Delete, - ], - ); - test_state( - State::Exited, - &mut vec![ - DockerCommand::Start, - DockerCommand::Restart, - DockerCommand::Delete, - ], - ); - test_state( - State::Paused, - &mut vec![ - DockerCommand::Resume, - DockerCommand::Stop, - DockerCommand::Delete, - ], - ); - test_state(State::Removing, &mut vec![DockerCommand::Delete]); - test_state( - State::Restarting, - &mut vec![DockerCommand::Stop, DockerCommand::Delete], - ); - test_state( - State::Running(RunningState::Healthy), - &mut vec![ - DockerCommand::Pause, - DockerCommand::Restart, - DockerCommand::Stop, - DockerCommand::Delete, - ], - ); - test_state(State::Unknown, &mut vec![DockerCommand::Delete]); - } - - // ****** // - // Filter // - // ****** // - - #[test] - /// Data is filtered correctly by name - fn test_app_data_filter_by_name() { - let (_, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - assert!(app_data.get_filter().1.is_none()); - - let pre_len = app_data.containers.items.len(); - app_data.filter_term_push('_'); - app_data.filter_term_push('2'); - - assert_eq!(app_data.get_filter().1, Some(&"_2".to_string())); - - app_data.filter_containers(); - let post_len = app_data.containers.items.len(); - assert!(pre_len != post_len); - assert_eq!(post_len, 1); - - // Can insert checks against the current filter term - assert!(app_data.can_insert(&containers[1])); - assert!(!app_data.can_insert(&containers[0])); - assert!(!app_data.can_insert(&containers[2])); - } - - #[test] - /// Data is filtered correctly by image - fn test_app_data_filter_by_image() { - let (_, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - assert!(app_data.get_filter().1.is_none()); - - let pre_len = app_data.containers.items.len(); - for c in ['i', 'm', 'a', 'g', 'e', '_', '2'] { - app_data.filter_term_push(c); - } - // app_data.filter_term_push('2'); - app_data.filter_by_next(); - - assert_eq!( - app_data.get_filter(), - (FilterBy::Image, Some(&"image_2".to_string())) - ); - - app_data.filter_containers(); - let post_len = app_data.containers.items.len(); - assert!(pre_len != post_len); - assert_eq!(post_len, 1); - - assert!(!app_data.can_insert(&containers[0])); - assert!(app_data.can_insert(&containers[1])); - assert!(!app_data.can_insert(&containers[2])); - } - - #[test] - /// Data is filtered correctly by status - fn test_app_data_filter_by_status() { - let (_, mut containers) = gen_containers(); - ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status); - let mut app_data = gen_appdata(&containers); - - assert!(app_data.get_filter().1.is_none()); - - let pre_len = app_data.containers.items.len(); - app_data.filter_term_push('x'); - - app_data.filter_by_next(); - app_data.filter_by_next(); - - assert_eq!( - app_data.get_filter(), - (FilterBy::Status, Some(&"x".to_string())) - ); - - app_data.filter_containers(); - let post_len = app_data.containers.items.len(); - assert!(pre_len != post_len); - assert_eq!(post_len, 1); - - assert!(app_data.can_insert(&containers[0])); - assert!(!app_data.can_insert(&containers[1])); - assert!(!app_data.can_insert(&containers[2])); - } - - #[test] - /// Data is filtered correctly by all - fn test_app_data_filter_by_all() { - let (_, mut containers) = gen_containers(); - ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status); - let mut app_data = gen_appdata(&containers); - - assert!(app_data.get_filter().1.is_none()); - - let pre_len = app_data.containers.items.len(); - app_data.filter_term_push('x'); - - app_data.filter_by_next(); - app_data.filter_by_next(); - app_data.filter_by_next(); - - assert_eq!( - app_data.get_filter(), - (FilterBy::All, Some(&"x".to_string())) - ); - - app_data.filter_containers(); - let post_len = app_data.containers.items.len(); - assert!(pre_len != post_len); - assert_eq!(post_len, 1); - - assert!(app_data.can_insert(&containers[0])); - assert!(!app_data.can_insert(&containers[1])); - assert!(!app_data.can_insert(&containers[2])); - } - - #[test] - /// Data is filtered correctly after various next() and previous() commands - fn test_app_data_filter_prev() { - let (_, mut containers) = gen_containers(); - ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status); - let mut app_data = gen_appdata(&containers); - - assert!(app_data.get_filter().1.is_none()); - - let pre_len = app_data.containers.items.len(); - app_data.filter_term_push('x'); - - app_data.filter_by_next(); - app_data.filter_by_next(); - - assert_eq!( - app_data.get_filter(), - (FilterBy::Status, Some(&"x".to_string())) - ); - - app_data.filter_containers(); - let post_len = app_data.containers.items.len(); - assert!(pre_len != post_len); - assert_eq!(post_len, 1); - - assert!(app_data.can_insert(&containers[0])); - assert!(!app_data.can_insert(&containers[1])); - assert!(!app_data.can_insert(&containers[2])); - - app_data.filter_by_prev(); - assert_eq!( - app_data.get_filter(), - (FilterBy::Image, Some(&"x".to_string())) - ); - - app_data.filter_containers(); - let post_len = app_data.containers.items.len(); - assert!(pre_len != post_len); - assert_eq!(post_len, 0); - - assert!(!app_data.can_insert(&containers[0])); - assert!(!app_data.can_insert(&containers[1])); - assert!(!app_data.can_insert(&containers[2])); - } - - // **** // - // Logs // - // **** // - - #[test] - /// log title string generated correctly - fn test_app_data_get_log_title() { - let (ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - // No container selected select - let result = app_data.get_log_title(); - assert_eq!(result, ""); - - // No logs - app_data.containers.start(); - let result = app_data.get_log_title(); - assert_eq!(result, " - container_1 - image_1"); - - // On last line of logs - let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); - app_data.update_log_by_id(logs, &ids[0]); - let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1 - image_1"); - - // Change log state to no longer be at the end - app_data.log_scroll(&ScrollDirection::Up); - let result = app_data.get_log_title(); - assert_eq!(result, " 2/3 - container_1 - image_1"); - } - - #[test] - /// log title string generated correctly after container change - fn test_app_data_get_log_title_after_container_change() { - let (ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - // No container selected select - let result = app_data.get_log_title(); - assert_eq!(result, ""); - - app_data.containers_start(); - - let result = app_data.get_log_title(); - assert_eq!(result, " - container_1 - image_1"); - - // change container - app_data.containers_scroll(&ScrollDirection::Down); - let result = app_data.get_log_title(); - assert_eq!(result, " - container_2 - image_2"); - - // On last line of logs - let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); - app_data.update_log_by_id(logs, &ids[1]); - let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_2 - image_2"); - - // Change log state to no longer be at the end - app_data.log_scroll(&ScrollDirection::Up); - let result = app_data.get_log_title(); - assert_eq!(result, " 2/3 - container_2 - image_2"); - } - - #[test] - /// update logs by id works - fn test_app_data_update_log_by_id() { - let (ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - // No container selected select - let result = app_data.get_log_title(); - assert_eq!(result, ""); - - app_data.containers_start(); - let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); - - app_data.update_log_by_id(logs, &ids[0]); - - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(2)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_logs( - Size { - width: 20, - height: 4, - }, - 1, - ); - assert_eq!(result.len(), 3); - - let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1 - image_1"); - } - - #[test] - /// logs state reset to start - fn test_app_data_logs_start() { - let (ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); - app_data.containers_start(); - app_data.update_log_by_id(logs, &ids[0]); - - app_data.log_start(); - - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(0)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1 - image_1"); - } - - #[test] - /// logs state end goes to the end of the logs list - fn test_app_data_logs_end() { - let (ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); - app_data.containers_start(); - app_data.update_log_by_id(logs, &ids[0]); - - app_data.log_start(); - - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(0)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1 - image_1"); - - app_data.log_end(); - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(2)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1 - image_1"); - } - - #[test] - /// logs state next works - /// At end has no effect - fn test_app_data_logs_next() { - let (ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); - app_data.containers_start(); - app_data.update_log_by_id(logs, &ids[0]); - - app_data.log_start(); - - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(0)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1 - image_1"); - - app_data.log_scroll(&ScrollDirection::Down); - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(1)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_log_title(); - assert_eq!(result, " 2/3 - container_1 - image_1"); - - app_data.log_scroll(&ScrollDirection::Down); - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(2)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1 - image_1"); - app_data.log_scroll(&ScrollDirection::Down); - - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(2)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1 - image_1"); - } - - #[test] - /// logs state previous works - /// previous at start has no effect - fn test_app_data_logs_previous() { - let (ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); - app_data.containers_start(); - app_data.update_log_by_id(logs, &ids[0]); - - app_data.log_end(); - - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(2)); - assert_eq!(result.unwrap().offset(), 0); - - let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1 - image_1"); - - app_data.log_scroll(&ScrollDirection::Up); - - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(1)); - assert_eq!(result.unwrap().offset(), 0); - let result = app_data.get_log_title(); - assert_eq!(result, " 2/3 - container_1 - image_1"); - - app_data.log_scroll(&ScrollDirection::Up); - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(0)); - assert_eq!(result.unwrap().offset(), 0); - let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1 - image_1"); - - app_data.log_scroll(&ScrollDirection::Up); - let result = app_data.get_log_state(); - assert!(result.is_some()); - assert_eq!(result.as_ref().unwrap().selected(), Some(0)); - assert_eq!(result.unwrap().offset(), 0); - let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1 - image_1"); - } - - // ********** // - // Chart data // - // ********** // - - #[test] - /// Chart data returned correctly - fn test_app_data_get_chart_data() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_chart_data(); - assert!(result.is_none()); - - app_data.containers_start(); - - let mut rx = NetworkBandwidth::new(); - rx.push(200); - rx.push(100); - rx.push(200); - - let mut tx = NetworkBandwidth::new(); - tx.push(300); - tx.push(600); - tx.push(900); - - if let Some(item) = app_data.get_container_by_id(&ContainerId::from("1")) { - item.cpu_stats = VecDeque::from([CpuStats::new(1.2), CpuStats::new(1.2)]); - item.mem_stats = VecDeque::from([ByteStats::new(1), ByteStats::new(2)]); - item.rx = rx; - item.tx = tx; - } - - let result = app_data.get_chart_data(); - assert_eq!( - result, - Some(ChartsData { - memory: ChartSeries { - dataset: vec![(0.0, 1.0), (1.0, 2.0)], - max: ByteStats::new(2), - current: ByteStats::new(2) - }, - cpu: ChartSeries { - dataset: vec![(0.0, 1.2), (1.0, 1.2)], - max: CpuStats::new(1.2), - current: CpuStats::new(1.2) - }, - rx: ChartSeries { - dataset: vec![(0.0, 0.0), (1.0, 100.0)], - max: BandwidthStat::new(100), - current: BandwidthStat::new(100) - }, - tx: ChartSeries { - dataset: vec![(0.0, 300.0), (1.0, 300.0)], - max: BandwidthStat::new(300), - current: BandwidthStat::new(300) - }, - state: State::Running(RunningState::Healthy) - }) - ); - } - - // ************* // - // Header Widths // - // ************* // - - #[test] - /// Header widths return correctly - fn test_app_data_get_width() { - let (_ids, containers) = gen_containers(); - let app_data = gen_appdata(&containers); - - let result = app_data.get_width(); - let expected = Columns { - name: (Header::Name, 11), - state: (Header::State, 9), - status: (Header::Status, 9), - cpu: (Header::Cpu, 6), - mem: (Header::Memory, 7, 7), - id: (Header::Id, 8), - image: (Header::Image, 7), - net_rx: (Header::Rx, 7), - net_tx: (Header::Tx, 7), - }; - assert_eq!(result, expected); - } - - #[test] - /// Header widths return correctly when some containers hidden - fn test_app_data_get_width_filtered() { - let (_ids, mut containers) = gen_containers(); - containers[0].name = ContainerName::from("some_longer_name_with_filter"); - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_width(); - let expected = Columns { - name: (Header::Name, 28), - state: (Header::State, 9), - status: (Header::Status, 9), - cpu: (Header::Cpu, 6), - mem: (Header::Memory, 7, 7), - id: (Header::Id, 8), - image: (Header::Image, 7), - net_rx: (Header::Rx, 7), - net_tx: (Header::Tx, 7), - }; - - assert_eq!(result, expected); - app_data.filter_term_push('c'); - app_data.filter_containers(); - assert_eq!(result, expected); - } - - // ***** // - // Ports // - // ***** // - - #[test] - /// Returns selected containers ports ordered by private ip - fn test_app_data_get_selected_ports() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - - app_data.containers.items[0].ports.push(ContainerPorts { - ip: None, - private: 10, - public: Some(1), - }); - app_data.containers.items[0].ports.push(ContainerPorts { - ip: None, - private: 11, - public: Some(3), - }); - app_data.containers.items[0].ports.push(ContainerPorts { - ip: None, - private: 4, - public: Some(2), - }); - - // No containers selected - let result = app_data.get_selected_ports(); - assert!(result.is_none()); - - // Selected container & ports - app_data.containers_start(); - let result = app_data.get_selected_ports(); - - assert_eq!( - result, - Some(( - vec![ - ContainerPorts { - ip: None, - private: 4, - public: Some(2) - }, - ContainerPorts { - ip: None, - private: 10, - public: Some(1) - }, - ContainerPorts { - ip: None, - private: 11, - public: Some(3) - }, - ContainerPorts { - ip: None, - private: 8001, - public: None - } - ], - State::Running(RunningState::Healthy), - )) - ); - - // Selected container & no ports - app_data.containers_start(); - app_data.containers.items[0].ports = vec![]; - let result = app_data.get_selected_ports(); - - assert_eq!( - result, - Some((vec![], State::Running(RunningState::Healthy))) - ); - } - - // ************** // - // Update mtehods // - // ************** // - - #[test] - /// Update stats functioning - fn test_app_data_update_stats() { - let (ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - let result = app_data.get_container_items(); - assert_eq!(result[0], containers[0]); - - app_data.update_stats_by_id(&ids[0], Some(10.0), Some(10), 10, 10, 10); - - let result = app_data.get_container_items(); - assert_ne!(result[0], containers[0]); - assert_eq!(result[0].cpu_stats, VecDeque::from([CpuStats::new(10.0)])); - assert_eq!(result[0].mem_stats, VecDeque::from([ByteStats::new(10)])); - assert_eq!(result[0].mem_limit, ByteStats::new(10)); - - let mut rx = NetworkBandwidth::new(); - rx.push(10); - let mut tx = NetworkBandwidth::new(); - tx.push(10); - assert_eq!(result[0].rx, rx); - // VecDeque::from([ByteStats::new(10)])); - assert_eq!(result[0].tx, tx); - // VecDeque::from([ByteStats::new(10)])); - } - - #[test] - /// Update stats functioning - fn test_app_data_update_containers() { - let (_ids, containers) = gen_containers(); - let mut app_data = gen_appdata(&containers); - let result_pre = app_data.get_container_items().to_owned(); - let input = vec![ - gen_container_summary(1, "paused"), - gen_container_summary(2, "dead"), - ]; - - app_data.update_containers(input); - let result_post = app_data.get_container_items().to_owned(); - assert_ne!(result_pre, result_post); - assert_eq!(result_post[0].state, State::Paused); - assert_eq!(result_post[1].state, State::Dead); - } - - #[test] - /// Update logs don't work if container is_oxker: true - fn test_app_data_update_log_by_id_is_oxker() { - let (ids, mut containers) = gen_containers(); - containers[0].is_oxker = true; - let mut app_data = gen_appdata(&containers); - let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); - - app_data.update_log_by_id(logs, &ids[0]); - app_data.log_start(); - - let result = app_data.get_log_state(); - assert!(result.is_none()); - } - - // *************** // - // Get logs method // - // *************** // - - #[test] - /// get_logs() returns vec of item, but the items are empty unless they are in the *visible" zone, based on height, index, and padding - fn test_app_data_update_get_logs() { - let (ids, containers) = gen_containers(); - - let mut app_data = gen_appdata(&containers); - - app_data.containers_start(); - let logs = (0..=999).map(|i| format!("{i} {i}")).collect::>(); - - app_data.update_log_by_id(logs, &ids[0]); - - let result = app_data.get_logs( - Size { - width: 20, - height: 10, - }, - 10, - ); - for (index, item) in result.iter().enumerate() { - if index < 979 { - assert_eq!(item, &Text::from("")); - } else { - assert_eq!(item, &Text::from(format!("{index}"))); - } - } - - let result = app_data.get_logs( - Size { - width: 20, - height: 100, - }, - 20, - ); - for (index, item) in result.iter().enumerate() { - if index < 879 { - assert_eq!(item, &Text::from("")); - } else { - assert_eq!(item, &Text::from(format!("{index}"))); - } - } - - app_data.log_start(); - - let result = app_data.get_logs( - Size { - width: 20, - height: 10, - }, - 10, - ); - for (index, item) in result.iter().enumerate() { - if index > 20 { - assert_eq!(item, &Text::from("")); - } else { - assert_eq!(item, &Text::from(format!("{index}"))); - } - } - - for _ in 0..=500 { - app_data.log_scroll(&ScrollDirection::Down); - } - let result = app_data.get_logs( - Size { - width: 20, - height: 10, - }, - 10, - ); - for (index, item) in result.iter().enumerate() { - if (481..=521).contains(&index) { - assert_eq!(item, &Text::from(format!("{index}"))); - } else { - assert_eq!(item, &Text::from("")); - } - } - } -} diff --git a/src/app_error.rs b/src/app_error.rs deleted file mode 100644 index ce4b5f6..0000000 --- a/src/app_error.rs +++ /dev/null @@ -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"), - } - } -} diff --git a/src/config/color_parser.rs b/src/config/color_parser.rs deleted file mode 100644 index 8afded3..0000000 --- a/src/config/color_parser.rs +++ /dev/null @@ -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, 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, - )* - } - )* - }; -} - -/// 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::().map(|i| *setter = i)); - } -} - -impl From> for AppColors { - #[allow(clippy::too_many_lines)] - fn from(value: Option) -> 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, - chart_cpu: Option, - chart_memory: Option, - chart_bandwidth: Option, - chart_ports: Option, - commands: Option, - container_state: Option, - containers: Option, - filter: Option, - log_search: Option, - headers_bar: Option, - logs: Option, - popup_delete: Option, - popup_error: Option, - popup_help: Option, - popup_info: Option, -} - -/// 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(), - } - } -} diff --git a/src/config/config.toml b/src/config/config.toml deleted file mode 100644 index f70c116..0000000 --- a/src/config/config.toml +++ /dev/null @@ -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" diff --git a/src/config/keymap_parser.rs b/src/config/keymap_parser.rs deleted file mode 100644 index 34fde03..0000000 --- a/src/config/keymap_parser.rs +++ /dev/null @@ -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, 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>, - )* - pub scroll_many: Option>, - } - )* - }; -} - -/// 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), - )* - 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> for Keymap { - /// Probably a better way to do this, but for now it works - fn from(value: Option) -> Self { - let mut keymap = Self::new(); - - let mut clash = HashSet::new(); - let mut counter = 0; - - let mut update_keymap = - |vec_str: Option>, - keymap_field: &mut (KeyCode, Option), - keymap_clash: &mut HashSet| { - 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>) -> Option { - 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> { - 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); - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index db79281..0000000 --- a/src/config/mod.rs +++ /dev/null @@ -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, - pub in_container: bool, - pub keymap: Keymap, - pub log_search_case_sensitive: bool, - pub raw_logs: bool, - pub dir_config: Option, - pub dir_save: Option, - pub show_logs: bool, - pub show_self: bool, - pub show_std_err: bool, - pub show_timestamp: bool, - pub timestamp_format: String, - pub timezone: Option, - 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)> for Config { - fn from((config_file, dir): (ConfigFile, Option)) -> 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 { - 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) -> Option { - 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 { - 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 { - 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); - } - } -} diff --git a/src/config/parse_args.rs b/src/config/parse_args.rs deleted file mode 100644 index c79434f..0000000 --- a/src/config/parse_args.rs +++ /dev/null @@ -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, - - /// 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, - - /// Directory for saving exported logs, defaults to `$HOME` - #[clap(long="save-dir", short = None)] - pub save_dir: Option, - - /// Path to a config file, readable as TOML, JSONC, or JSON - #[clap(long="config-file", short = None)] - pub config_file: Option, - - /// 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, - } - } -} diff --git a/src/config/parse_config_file.rs b/src/config/parse_config_file.rs deleted file mode 100644 index e685d57..0000000 --- a/src/config/parse_config_file.rs +++ /dev/null @@ -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 { - 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 { - 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, - pub colors: Option, - pub docker_interval: Option, - pub gui: Option, - pub host: Option, - pub keymap: Option, - pub log_search_case_sensitive: Option, - pub raw_logs: Option, - pub save_dir: Option, - pub show_logs: Option, - pub show_self: Option, - pub show_std_err: Option, - pub show_timestamp: Option, - pub timestamp_format: Option, - pub timezone: Option, - pub use_cli: Option, -} - -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 { - match file_format { - ConfigFileFormat::Json => { - serde_json::from_str::(input).map_err(|i| AppError::Parse(i.to_string())) - } - ConfigFileFormat::Jsonc | ConfigFileFormat::JsoncAsJson => { - serde_jsonc::from_str::(input).map_err(|i| AppError::Parse(i.to_string())) - } - ConfigFileFormat::Toml => { - toml::from_str::(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 { - 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 { - 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); - } -} diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs deleted file mode 100644 index 6030e29..0000000 --- a/src/docker_data/message.rs +++ /dev/null @@ -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>), - Inspect(ContainerId), - Update, -} diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs deleted file mode 100644 index 038dc6b..0000000 --- a/src/docker_data/mod.rs +++ /dev/null @@ -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>, - binate: Binate, - config: Config, - docker: Arc, - gui_state: Arc>, - receiver: Receiver, - spawns: Arc>>, -} - -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>, - docker: Arc, - state: State, - spawn_id: SpawnId, - spawns: Arc>>, - ) { - 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::>(); - 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>, - docker: Arc, - id: ContainerId, - since: u64, - spawns: Arc>>, - 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 { - let init = Arc::new(AtomicUsize::new(0)); - for (_, id) in all_ids { - let app_data: Arc> = - 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>, - error: DockerCommand, - gui_state: &Arc>, - ) { - 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::) - .await - } - DockerCommand::Resume => docker.unpause_container(id.get()).await, - DockerCommand::Start => { - docker - .start_container(id.get(), None::) - .await - } - DockerCommand::Stop => { - docker - .stop_container(id.get(), None::) - .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) { - 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>, - docker: Docker, - docker_rx: Receiver, - docker_tx: Sender, - gui_state: Arc>, - ) { - 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); - } -} diff --git a/src/exec.rs b/src/exec.rs deleted file mode 100644 index bc4444e..0000000 --- a/src/exec.rs +++ /dev/null @@ -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, -} - -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) { - 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 { - 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>> for HWU16 { -// type Error = None; -// fn try_from(terminal: &Terminal>) -> Option { -// terminal.size().map_or(None, |i| { -// Some(Self { -// width: i.width, -// height: i.height, -// }) -// }) -// } - -// } - -// impl TerminalSize { -// pub fn new(terminal: &Terminal>) -> Option { -// 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, Arc)), - // use the external `docker-cli` - External(Arc), -} - -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>, docker: &Arc) -> Option { - 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, - terminal_size: Option, - ) -> 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) -> Result<(), AppError> { - match self { - Self::External(id) => { - Self::exec_external(id); - Ok(()) - } - - Self::Internal((id, docker)) => self.exec_internal(id, docker, tty_size).await, - } - } -} diff --git a/src/input_handler/message.rs b/src/input_handler/message.rs deleted file mode 100644 index 1d5ab4a..0000000 --- a/src/input_handler/message.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crossterm::event::{KeyCode, KeyModifiers, MouseEvent}; - -#[derive(Debug, Clone, Copy)] -pub enum InputMessages { - ButtonPress((KeyCode, KeyModifiers)), - MouseEvent((MouseEvent, KeyModifiers)), -} diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs deleted file mode 100644 index 4dd5ae3..0000000 --- a/src/input_handler/mod.rs +++ /dev/null @@ -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>, - docker_tx: Sender, - keymap: config::Keymap, - gui_state: Arc>, - is_running: Arc, - mouse_capture: bool, - rx: Receiver, -} - -impl InputHandler { - /// Initialize self, and running the message handling loop - pub async fn start( - app_data: Arc>, - docker_tx: Sender, - gui_state: Arc>, - is_running: Arc, - rx: Receiver, - ) { - 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> { - 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::(), - ); - } - } - 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), - } - } - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 8ad73f5..0000000 --- a/src/main.rs +++ /dev/null @@ -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 { - 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>, - docker_rx: Receiver, - docker_tx: Sender, - gui_state: &Arc>, -) { - 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>, - docker_sx: &Sender, - gui_state: &Arc>, - input_rx: Receiver, - is_running: &Arc, -) { - 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::>(); - - 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, Vec) { - let ids = (1..=3) - .map(|i| ContainerId::from(format!("{i}").as_str())) - .collect::>(); - let containers = ids - .iter() - .enumerate() - .map(|(index, id)| gen_item(id, index + 1)) - .collect::>(); - (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, - } - } -} diff --git a/src/ui/color_match.rs b/src/ui/color_match.rs deleted file mode 100644 index b54d63a..0000000 --- a/src/ui/color_match.rs +++ /dev/null @@ -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> { - 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::>(), - )] - } - - /// Remove all ansi formatting from a given string and create ratatui Lines - pub fn remove_ansi<'a>(input: &str) -> Vec> { - vec![Line::from( - categorise_text(input) - .into_iter() - .map(|i| i.text) - .collect::() - .trim() - .to_owned(), - )] - } - - /// create ratatui Lines that exactly match the given strings - pub fn raw<'a>(input: &str) -> Vec> { - vec![Line::from(input.escape_debug().collect::())] - } - - /// 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); - } -} diff --git a/src/ui/draw_blocks/chart_bandwidth.rs b/src/ui/draw_blocks/chart_bandwidth.rs deleted file mode 100644 index 79ffd48..0000000 --- a/src/ui/draw_blocks/chart_bandwidth.rs +++ /dev/null @@ -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>, - 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); - } - } - } - } - } -} diff --git a/src/ui/draw_blocks/chart_cpu_mem.rs b/src/ui/draw_blocks/chart_cpu_mem.rs deleted file mode 100644 index 1cd9952..0000000 --- a/src/ui/draw_blocks/chart_cpu_mem.rs +++ /dev/null @@ -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>, - 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); - } - } - } - } - } -} diff --git a/src/ui/draw_blocks/commands.rs b/src/ui/draw_blocks/commands.rs deleted file mode 100644 index 2ca6221..0000000 --- a/src/ui/draw_blocks/commands.rs +++ /dev/null @@ -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>, - area: Rect, - colors: AppColors, - f: &mut Frame, - fd: &FrameData, - gui_state: &Arc>, -) { - 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::>() - }); - - 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); - } - _ => (), - } - } - } - } -} diff --git a/src/ui/draw_blocks/containers.rs b/src/ui/draw_blocks/containers.rs deleted file mode 100644 index 7078d1b..0000000 --- a/src/ui/draw_blocks/containers.rs +++ /dev/null @@ -1,1017 +0,0 @@ -use std::sync::Arc; - -use super::MARGIN; -use parking_lot::Mutex; -use ratatui::{ - Frame, - layout::{Alignment, Rect}, - style::{Modifier, Style, Stylize}, - text::{Line, Span}, - widgets::{List, ListItem, Paragraph}, -}; - -use crate::{ - app_data::{AppData, ByteStats, Columns, ContainerItem, CpuStats}, - config::AppColors, - ui::{FrameData, GuiState, SelectablePanel}, -}; - -use super::{CIRCLE, generate_block}; - -/// Format the container data to display nicely on the screen -fn format_containers<'a>(colors: AppColors, i: &ContainerItem, widths: &Columns) -> Line<'a> { - let state_style = Style::default().fg(i.state.get_color(colors)); - - Line::from(vec![ - Span::styled( - format!( - "{:width$}{MARGIN}", - i.cpu_stats.back().map_or_else(CpuStats::default, |f| *f), - width = &widths.cpu.1.into() - ), - state_style, - ), - Span::styled( - format!( - "{:>width_current$} / {:>width_limit$}{MARGIN}", - i.mem_stats.back().map_or_else(ByteStats::default, |f| *f), - i.mem_limit, - width_current = &widths.mem.1.into(), - width_limit = &widths.mem.2.into() - ), - state_style, - ), - Span::styled( - format!( - "{:>width$}{MARGIN}", - i.id.get_short(), - width = &widths.id.1.into() - ), - colors.containers.text, - ), - Span::styled( - format!( - "{:width$}{MARGIN}", - i.rx.current_total(), - width = widths.net_rx.1.into() - ), - Style::default().fg(colors.containers.text_rx), - ), - Span::styled( - format!( - "{:>width$}{MARGIN}", - i.tx.current_total(), - width = widths.net_tx.1.into() - ), - Style::default().fg(colors.containers.text_tx), - ), - ]) -} - -/// Draw the containers panel -pub fn draw( - app_data: &Arc>, - area: Rect, - colors: AppColors, - f: &mut Frame, - fd: &FrameData, - gui_state: &Arc>, -) { - let block = generate_block(area, colors, fd, gui_state, SelectablePanel::Containers) - .bg(colors.containers.background); - - let items = app_data - .lock() - .get_container_items() - .iter() - .map(|i| ListItem::new(format_containers(colors, i, &fd.columns))) - .collect::>(); - - if items.is_empty() { - let text = if fd.filter_term.is_some() { - "no containers match filter" - } else if fd.is_loading { - &format!("loading {}", fd.loading_icon) - } else { - "no containers running" - }; - - let paragraph = Paragraph::new(text) - .block(block) - .alignment(Alignment::Center); - f.render_widget(paragraph, area); - } else { - let items = List::new(items) - .block(block) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)) - .highlight_symbol(CIRCLE); - f.render_stateful_widget(items, area, app_data.lock().get_container_state()); - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod tests { - use insta::assert_snapshot; - use ratatui::style::{Color, Modifier}; - - use crate::{ - app_data::{ContainerImage, ContainerName, ContainerStatus, State, StatefulList}, - config::AppColors, - ui::{ - FrameData, - draw_blocks::tests::{ - BORDER_CHARS, COLOR_ORANGE, COLOR_RX, COLOR_TX, TuiTestSetup, get_result, - test_setup, - }, - }, - }; - - #[test] - /// No containers, panel unselected, then selected, border color changes correctly - fn test_draw_blocks_containers_none() { - let mut setup = test_setup(40, 6, true, true); - setup.app_data.lock().containers = StatefulList::new(vec![]); - - setup - .gui_state - .lock() - .selectable_panel_next(&setup.app_data); - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - - setup - .terminal - .draw(|f| { - super::draw( - &setup.app_data, - setup.area, - colors, - f, - &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); - } - } - } - - setup - .gui_state - .lock() - .selectable_panel_previous(&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 (_, 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] - /// Containers panel drawn, selected line is bold, border is blue - fn test_draw_blocks_containers_selected_bold() { - let mut setup = test_setup(130, 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() { - if BORDER_CHARS.contains(&result_cell.symbol()) { - assert_eq!(result_cell.fg, Color::LightCyan); - } - - let not_bold = || assert!(result_cell.modifier.is_empty()); - if row_index == 1 { - match result_cell_index { - 0 | 2 | 129 => { - not_bold(); - } - _ => { - assert_eq!(result_cell.modifier, Modifier::BOLD); - } - } - } else { - not_bold(); - } - } - } - - // Change selected panel, border is now no longer blue - 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 (_, 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); - } - } - } - } - - #[test] - /// Columns on all rows are coloured correctly - fn test_draw_blocks_containers_colors() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - - 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() { - match (row_index, result_cell_index) { - //border - (0 | 5, _) | (1..=4, 0 | 129) => { - assert_eq!(result_cell.fg, Color::LightCyan); - } - // name, id, image column - (1..=3, 4..=17 | 71..=91) => { - assert_eq!(result_cell.fg, Color::Blue); - } - // state, status, cpu, memory column - (1..=3, 18..=70) => { - assert_eq!(result_cell.fg, Color::Green); - } - // rx column - (1..=3, 92..=101) => { - assert_eq!(result_cell.fg, COLOR_RX); - } - // tx column - (1..=3, 102..=111) => { - assert_eq!(result_cell.fg, COLOR_TX); - } - _ => assert_eq!(result_cell.fg, Color::Reset), - } - } - } - } - - #[test] - /// Long container + image name is truncated correctly - fn test_draw_blocks_containers_long_name_image() { - let mut setup = test_setup(170, 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"); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - setup.app_data.lock().containers.items[0].state = State::Paused; - - setup - .terminal - .draw(|f| { - super::draw( - &setup.app_data, - setup.area, - colors, - f, - &fd, - &setup.gui_state, - ); - }) - .unwrap(); - assert_snapshot!(setup.terminal.backend()); - } - - // Check that the correct colour is applied to the state/status/cpu/memory section - - fn check_colour(setup: &TuiTestSetup, color: Color) { - 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 - (0 | 5, _) | (1..=4, 0 | 129) => { - assert_eq!(result_cell.fg, Color::LightCyan); - } - // name, id, image column - (1..=3, 4..=17 | 71..=91) => { - assert_eq!(result_cell.fg, Color::Blue); - } - // state, status, cpu, memory column of the first row - (1, 18..=70) => { - assert_eq!(result_cell.fg, color); - } - // state, status, cpu, memory column - (2..=3, 4..=77) => { - assert_eq!(result_cell.fg, Color::Green); - } - // rx column - (1..=3, 92..=101) => { - assert_eq!(result_cell.fg, COLOR_RX); - } - // tx column - (1..=3, 102..=111) => { - assert_eq!(result_cell.fg, COLOR_TX); - } - _ => assert_eq!(result_cell.fg, Color::Reset), - } - } - } - } - - #[test] - /// When container is paused, correct colors displayed - fn test_draw_blocks_containers_paused() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - setup.app_data.lock().containers.items[0].state = State::Paused; - - setup - .terminal - .draw(|f| { - super::draw( - &setup.app_data, - setup.area, - colors, - f, - &fd, - &setup.gui_state, - ); - }) - .unwrap(); - - check_colour(&setup, Color::Yellow); - assert_snapshot!(setup.terminal.backend()); - } - - #[test] - /// When container is dead, correct colors displayed - fn test_draw_blocks_containers_dead() { - let mut setup = test_setup(130, 6, true, true); - setup.app_data.lock().containers.items[0].state = State::Dead; - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - - setup - .terminal - .draw(|f| { - super::draw( - &setup.app_data, - setup.area, - colors, - f, - &fd, - &setup.gui_state, - ); - }) - .unwrap(); - - check_colour(&setup, Color::Red); - assert_snapshot!(setup.terminal.backend()); - } - - #[test] - /// When container is exited, correct colors displayed - fn test_draw_blocks_containers_exited() { - let mut setup = test_setup(130, 6, true, true); - - setup.app_data.lock().containers.items[0].state = State::Exited; - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - - setup - .terminal - .draw(|f| { - super::draw( - &setup.app_data, - setup.area, - colors, - f, - &fd, - &setup.gui_state, - ); - }) - .unwrap(); - - check_colour(&setup, Color::Red); - assert_snapshot!(setup.terminal.backend()); - } - #[test] - /// When container is paused, correct colors displayed - fn test_draw_blocks_containers_removing() { - let mut setup = test_setup(130, 6, true, true); - - setup.app_data.lock().containers.items[0].state = State::Removing; - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - - setup - .terminal - .draw(|f| { - super::draw( - &setup.app_data, - setup.area, - colors, - f, - &fd, - &setup.gui_state, - ); - }) - .unwrap(); - - check_colour(&setup, Color::LightRed); - assert_snapshot!(setup.terminal.backend()); - } - - #[test] - /// When container state is restarting, correct colors displayed - fn test_draw_blocks_containers_restarting() { - let mut setup = test_setup(130, 6, true, true); - - setup.app_data.lock().containers.items[0].state = State::Restarting; - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - - 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() { - match (row_index, result_cell_index) { - // border - (0 | 5, _) | (1..=4, 0 | 129) => { - assert_eq!(result_cell.fg, Color::LightCyan); - } - // name, id, image column - (1..=3, 4..=17 | 74..=94) => { - assert_eq!(result_cell.fg, Color::Blue); - } - // state, status, cpu, memory column of the first row - (1, 18..=73) => { - assert_eq!(result_cell.fg, Color::LightGreen); - } - // state, status, cpu, memory column - (2..=3, 18..=73) => { - assert_eq!(result_cell.fg, Color::Green); - } - // rx column - (1..=3, 95..=104) => { - assert_eq!(result_cell.fg, COLOR_RX); - } - // tx column - (1..=3, 105..=114) => { - assert_eq!(result_cell.fg, COLOR_TX); - } - _ => { - assert_eq!(result_cell.fg, Color::Reset); - } - } - } - } - } - - #[test] - /// When container state is unhealthy, correct colors displayed - fn test_draw_blocks_containers_unhealthy() { - let mut setup = test_setup(130, 6, true, true); - - let status = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); - setup.app_data.lock().containers.items[0].state = State::from(("running", &status)); - setup.app_data.lock().containers.items[0].status = status; - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - - 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() { - match (row_index, result_cell_index) { - // border - (0 | 5, _) | (1..=4, 0 | 129) => { - assert_eq!(result_cell.fg, Color::LightCyan); - } - // name, id, image column - (1..=3, 4..=17 | 83..=103) => { - assert_eq!(result_cell.fg, Color::Blue); - } - // state, status, cpu, memory column of the first row - (1, 18..=82) => { - assert_eq!(result_cell.fg, COLOR_ORANGE); - } - // state, status, cpu, memory column - (2..=3, 18..=82) => { - assert_eq!(result_cell.fg, Color::Green); - } - // rx column - (1..=3, 104..=113) => { - assert_eq!(result_cell.fg, COLOR_RX); - } - // tx column - (1..=3, 114..=123) => { - assert_eq!(result_cell.fg, COLOR_TX); - } - _ => assert_eq!(result_cell.fg, Color::Reset), - } - } - } - } - - #[test] - /// When container state is unknown, correct colors displayed - fn test_draw_blocks_containers_unknown() { - let mut setup = test_setup(130, 6, true, true); - - setup.app_data.lock().containers.items[0].state = State::Unknown; - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let colors = setup.app_data.lock().config.app_colors; - - setup - .terminal - .draw(|f| { - super::draw( - &setup.app_data, - setup.area, - colors, - f, - &fd, - &setup.gui_state, - ); - }) - .unwrap(); - - check_colour(&setup, Color::Red); - assert_snapshot!(setup.terminal.backend()); - } - - #[test] - /// Custom colors applied correctly - fn test_draw_blocks_containers_custom_colors() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - let mut colors = AppColors::new(); - colors.borders.selected = Color::Green; - colors.containers.background = Color::Black; - colors.containers.text = Color::Yellow; - colors.containers.text_rx = Color::Red; - colors.containers.text_tx = Color::Blue; - - colors.container_state.running_healthy = Color::Magenta; - - 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() { - // The highlight symbol can't correctly be colored - if (row_index, result_cell_index) != (1, 2) { - assert_eq!(result_cell.bg, Color::Black); - } - match (row_index, result_cell_index) { - //border - (0 | 5, _) | (1..=4, 0 | 129) => { - assert_eq!(result_cell.fg, Color::Green); - } - // name, id, image column - (1..=3, 4..=17 | 71..=91) => { - assert_eq!(result_cell.fg, Color::Yellow); - } - // state, status, cpu, memory column - (1..=3, 18..=70) => { - assert_eq!(result_cell.fg, Color::Magenta); - } - // rx column - (1..=3, 92..=101) => { - assert_eq!(result_cell.fg, Color::Red); - } - // tx column - (1..=3, 102..=111) => { - assert_eq!(result_cell.fg, Color::Blue); - } - _ => assert_eq!(result_cell.fg, Color::Reset), - } - } - } - } - - #[test] - /// Make sure that the state has the correctly color applied to it - fn test_draw_blocks_containers_custom_colors_state_healthy() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - - // Healthy - let mut colors = AppColors::new(); - colors.container_state.running_healthy = Color::Magenta; - - 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() { - if let (1..=3, 18..=70) = (row_index, result_cell_index) { - assert_eq!(result_cell.fg, Color::Magenta); - } - } - } - } - #[test] - /// Make sure that the state has the correctly color applied to it - fn test_draw_blocks_containers_custom_colors_state_unhealthy() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - - let mut colors = AppColors::new(); - colors.container_state.running_unhealthy = Color::Red; - let status = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); - setup.app_data.lock().containers.items[0].state = State::from(("running", &status)); - setup.app_data.lock().containers.items[0].status = status; - - 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() { - if let (1, 18..=70) = (row_index, result_cell_index) { - assert_eq!(result_cell.fg, Color::Red); - } - } - } - } - - #[test] - /// Make sure that the state has the correctly color applied to it - fn test_draw_blocks_containers_custom_colors_state_dead() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - - let mut colors = AppColors::new(); - colors.container_state.dead = Color::Magenta; - setup.app_data.lock().containers.items[0].state = State::Dead; - - 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() { - if let (1, 18..=70) = (row_index, result_cell_index) { - assert_eq!(result_cell.fg, Color::Magenta); - } - } - } - } - - #[test] - /// Make sure that the state has the correctly color applied to it - fn test_draw_blocks_containers_custom_colors_state_exited() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - - let mut colors = AppColors::new(); - colors.container_state.exited = Color::Gray; - setup.app_data.lock().containers.items[0].state = State::Exited; - - 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() { - if let (1, 18..=70) = (row_index, result_cell_index) { - assert_eq!(result_cell.fg, Color::Gray); - } - } - } - } - - #[test] - /// Make sure that the state has the correctly color applied to it - fn test_draw_blocks_containers_custom_colors_state_paused() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - - let mut colors = AppColors::new(); - colors.container_state.paused = Color::Cyan; - setup.app_data.lock().containers.items[0].state = State::Paused; - - 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() { - if let (1, 18..=70) = (row_index, result_cell_index) { - assert_eq!(result_cell.fg, Color::Cyan); - } - } - } - } - - #[test] - /// Make sure that the state has the correctly color applied to it - fn test_draw_blocks_containers_custom_colors_state_removing() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - - let mut colors = AppColors::new(); - colors.container_state.removing = Color::White; - setup.app_data.lock().containers.items[0].state = State::Removing; - - 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() { - if let (1, 18..=70) = (row_index, result_cell_index) { - assert_eq!(result_cell.fg, Color::White); - } - } - } - } - - #[test] - /// Make sure that the state has the correctly color applied to it - fn test_draw_blocks_containers_custom_colors_state_restarting() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - - let mut colors = AppColors::new(); - colors.container_state.restarting = Color::LightYellow; - setup.app_data.lock().containers.items[0].state = State::Restarting; - - 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() { - if let (1, 18..=70) = (row_index, result_cell_index) { - assert_eq!(result_cell.fg, Color::LightYellow); - } - } - } - } - - #[test] - /// Make sure that the state has the correctly color applied to it - fn test_draw_blocks_containers_custom_colors_state_unknown() { - let mut setup = test_setup(130, 6, true, true); - - let fd = FrameData::from((&setup.app_data, &setup.gui_state)); - - let mut colors = AppColors::new(); - colors.container_state.unknown = COLOR_ORANGE; - setup.app_data.lock().containers.items[0].state = State::Unknown; - - 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() { - if let (1, 18..=70) = (row_index, result_cell_index) { - assert_eq!(result_cell.fg, COLOR_ORANGE); - } - } - } - } -} diff --git a/src/ui/draw_blocks/delete_confirm.rs b/src/ui/draw_blocks/delete_confirm.rs deleted file mode 100644 index d64fb7d..0000000 --- a/src/ui/draw_blocks/delete_confirm.rs +++ /dev/null @@ -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>, - 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()); - } -} diff --git a/src/ui/draw_blocks/error.rs b/src/ui/draw_blocks/error.rs deleted file mode 100644 index ddb85ef..0000000 --- a/src/ui/draw_blocks/error.rs +++ /dev/null @@ -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, - keymap: &Keymap, - seconds: Option, -) { - 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()); - } -} diff --git a/src/ui/draw_blocks/filter.rs b/src/ui/draw_blocks/filter.rs deleted file mode 100644 index 1091cee..0000000 --- a/src/ui/draw_blocks/filter.rs +++ /dev/null @@ -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); - } - } - } - } - } -} diff --git a/src/ui/draw_blocks/headers.rs b/src/ui/draw_blocks/headers.rs deleted file mode 100644 index 01a0e7d..0000000 --- a/src/ui/draw_blocks/headers.rs +++ /dev/null @@ -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:(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, 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>, - 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>, - 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::>(); - - 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>, - keymap: &Keymap, -) { - let gen_style = |bg: Option, 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) { - 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 - ); -} diff --git a/src/ui/draw_blocks/help.rs b/src/ui/draw_blocks/help.rs deleted file mode 100644 index 1c1fd76..0000000 --- a/src/ui/draw_blocks/help.rs +++ /dev/null @@ -1,1300 +0,0 @@ -use std::sync::LazyLock; - -use ratatui::{ - Frame, - layout::{Alignment, Constraint, Direction, Layout, Rect, Size}, - style::{Color, Style, Stylize}, - text::{Line, Span}, - widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph}, -}; - -use crate::{ - config::{AppColors, Config, Keymap}, - ui::gui_state::BoxLocation, -}; - -use super::{DESCRIPTION, NAME_TEXT, REPO, VERSION, popup}; - -macro_rules! to_u16 { - ($value:expr) => { - u16::try_from($value).unwrap_or_default() - }; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum KeyDescriptions { - Clear, - Command, - Exec, - FilterMode, - Help, - InspectMode, - LogHeight, - LogVisibility, - MouseCapture, - Panel, - Quit, - Redraw, - Save, - ScrollEnd, - ScrollH, - ScrollSpeed, - ScrollStart, - ScrollV, - SearchMode, - SortCpu, - SortHeader, - SortId, - SortImage, - SortMem, - SortName, - SortRX, - SortState, - SortStatus, - SortStop, - SortTX, -} - -type Column = Vec<(Vec>, KeyDescriptions)>; - -#[derive(Debug, Clone, Hash)] -struct KeymapColumns { - left: Column, - right: Column, -} - -impl KeymapColumns { - fn default(keymap: &Keymap) -> Self { - Self { - left: vec![ - ( - vec![ - Some(keymap.quit.0.to_string()), - keymap.quit.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Quit, - ), - ( - vec![ - Some(keymap.scroll_down.0.to_string()), - Some(keymap.scroll_up.0.to_string()), - keymap.scroll_down.1.as_ref().map(|i| i.to_string()), - keymap.scroll_up.1.as_ref().map(|i| i.to_string()), - Some(keymap.scroll_start.0.to_string()), - Some(keymap.scroll_end.0.to_string()), - keymap.scroll_start.1.as_ref().map(|i| i.to_string()), - keymap.scroll_end.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::ScrollV, - ), - ( - vec![Some(keymap.scroll_many.to_string())], - KeyDescriptions::ScrollSpeed, - ), - ( - vec![ - Some(keymap.exec.0.to_string()), - keymap.exec.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Exec, - ), - ( - vec![ - Some(keymap.filter_mode.0.to_string()), - keymap.filter_mode.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::FilterMode, - ), - ( - vec![ - Some(keymap.toggle_help.0.to_string()), - keymap.toggle_help.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Help, - ), - ( - vec![ - Some(keymap.log_section_height_decrease.0.to_string()), - Some(keymap.log_section_height_increase.0.to_string()), - keymap - .log_section_height_decrease - .1 - .as_ref() - .map(|i| i.to_string()), - keymap - .log_section_height_increase - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::LogHeight, - ), - (vec![Some("1 ~ 9".to_owned())], KeyDescriptions::SortHeader), - ( - vec![ - Some(keymap.select_next_panel.0.to_string()), - Some(keymap.select_previous_panel.0.to_string()), - keymap - .select_previous_panel - .1 - .as_ref() - .map(|i| i.to_string()), - keymap.select_next_panel.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Panel, - ), - ( - vec![ - Some(keymap.save_logs.0.to_string()), - keymap.save_logs.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Save, - ), - ], - right: vec![ - ( - vec![ - Some(keymap.clear.0.to_string()), - keymap.clear.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Clear, - ), - ( - vec![ - Some(keymap.scroll_back.0.to_string()), - Some(keymap.scroll_forward.0.to_string()), - keymap.scroll_back.1.as_ref().map(|i| i.to_string()), - keymap.scroll_forward.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::ScrollH, - ), - (vec![Some(String::from("Enter"))], KeyDescriptions::Command), - ( - vec![ - Some(keymap.inspect.0.to_string()), - keymap.inspect.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::InspectMode, - ), - ( - vec![ - Some(keymap.log_search_mode.0.to_string()), - keymap.log_search_mode.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::SearchMode, - ), - ( - vec![ - Some(keymap.force_redraw.0.to_string()), - keymap.force_redraw.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Redraw, - ), - ( - vec![ - Some(keymap.log_section_toggle.0.to_string()), - keymap.log_section_toggle.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::LogVisibility, - ), - (vec![Some("0".to_owned())], KeyDescriptions::SortStop), - ( - vec![ - Some(keymap.toggle_mouse_capture.0.to_string()), - keymap - .toggle_mouse_capture - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::MouseCapture, - ), - ], - } - } - - fn custom(config: &Config) -> Self { - Self { - left: vec![ - ( - vec![ - Some(config.keymap.quit.0.to_string()), - config.keymap.quit.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Quit, - ), - ( - vec![ - Some(config.keymap.scroll_down.0.to_string()), - Some(config.keymap.scroll_up.0.to_string()), - config.keymap.scroll_down.1.as_ref().map(|i| i.to_string()), - config.keymap.scroll_up.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::ScrollV, - ), - ( - vec![ - Some(config.keymap.scroll_start.0.to_string()), - config.keymap.scroll_start.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::ScrollStart, - ), - ( - vec![Some(config.keymap.scroll_many.to_string())], - KeyDescriptions::ScrollSpeed, - ), - ( - vec![ - Some(config.keymap.exec.0.to_string()), - config.keymap.exec.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Exec, - ), - ( - vec![ - Some(config.keymap.filter_mode.0.to_string()), - config.keymap.filter_mode.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::FilterMode, - ), - ( - vec![ - Some(config.keymap.toggle_help.0.to_string()), - config.keymap.toggle_help.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Help, - ), - ( - vec![ - Some(config.keymap.log_section_height_decrease.0.to_string()), - Some(config.keymap.log_section_height_increase.0.to_string()), - config - .keymap - .log_section_height_decrease - .1 - .as_ref() - .map(|i| i.to_string()), - config - .keymap - .log_section_height_increase - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::LogHeight, - ), - ( - vec![ - Some(config.keymap.sort_by_name.0.to_string()), - config.keymap.sort_by_name.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::SortName, - ), - ( - vec![ - Some(config.keymap.sort_by_status.0.to_string()), - config - .keymap - .sort_by_status - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::SortStatus, - ), - ( - vec![ - Some(config.keymap.sort_by_memory.0.to_string()), - config - .keymap - .sort_by_memory - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::SortMem, - ), - ( - vec![ - Some(config.keymap.sort_by_image.0.to_string()), - config - .keymap - .sort_by_image - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::SortImage, - ), - ( - vec![ - Some(config.keymap.sort_by_tx.0.to_string()), - config.keymap.sort_by_tx.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::SortTX, - ), - ( - vec![ - Some(config.keymap.select_next_panel.0.to_string()), - Some(config.keymap.select_previous_panel.0.to_string()), - config - .keymap - .select_previous_panel - .1 - .as_ref() - .map(|i| i.to_string()), - config - .keymap - .select_next_panel - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::Panel, - ), - ( - vec![ - Some(config.keymap.save_logs.0.to_string()), - config.keymap.save_logs.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Save, - ), - ], - - right: vec![ - ( - vec![ - Some(config.keymap.clear.0.to_string()), - config.keymap.clear.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Clear, - ), - ( - vec![ - Some(config.keymap.scroll_back.0.to_string()), - Some(config.keymap.scroll_forward.0.to_string()), - config.keymap.scroll_back.1.as_ref().map(|i| i.to_string()), - config - .keymap - .scroll_forward - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::ScrollH, - ), - ( - vec![ - Some(config.keymap.scroll_end.0.to_string()), - config.keymap.scroll_end.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::ScrollEnd, - ), - (vec![Some(String::from("Enter"))], KeyDescriptions::Command), - ( - vec![ - Some(config.keymap.inspect.0.to_string()), - config.keymap.inspect.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::InspectMode, - ), - ( - vec![ - Some(config.keymap.log_search_mode.0.to_string()), - config - .keymap - .log_search_mode - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::SearchMode, - ), - ( - vec![ - Some(config.keymap.force_redraw.0.to_string()), - config.keymap.force_redraw.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::Redraw, - ), - ( - vec![ - Some(config.keymap.log_section_toggle.0.to_string()), - config - .keymap - .log_section_toggle - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::LogVisibility, - ), - ( - vec![ - Some(config.keymap.sort_by_state.0.to_string()), - config - .keymap - .sort_by_state - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::SortState, - ), - ( - vec![ - Some(config.keymap.sort_by_cpu.0.to_string()), - config.keymap.sort_by_cpu.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::SortCpu, - ), - ( - vec![ - Some(config.keymap.sort_by_id.0.to_string()), - config.keymap.sort_by_id.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::SortId, - ), - ( - vec![ - Some(config.keymap.sort_by_rx.0.to_string()), - config.keymap.sort_by_rx.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::SortRX, - ), - ( - vec![ - Some(config.keymap.sort_reset.0.to_string()), - config.keymap.sort_reset.1.as_ref().map(|i| i.to_string()), - ], - KeyDescriptions::SortStop, - ), - ( - vec![ - Some(config.keymap.toggle_mouse_capture.0.to_string()), - config - .keymap - .toggle_mouse_capture - .1 - .as_ref() - .map(|i| i.to_string()), - ], - KeyDescriptions::MouseCapture, - ), - ], - } - } - - /// Add 1 to allow spacing between the key and the definition - fn longest_line(column: &Column) -> usize { - column - .iter() - .map(|(keys, _)| { - keys.iter() - .filter_map(|k| k.as_deref()) - .collect::>() - .join(" ") - .len() - }) - .max() - .unwrap_or(0) - .saturating_add(1) - } - - fn create_button_line(column: &Column, colors: &AppColors) -> Vec> { - let longest_button = Self::longest_line(column); - column - .iter() - .map(|(keys, desc)| HelpInfo::create_button_line(keys, desc, *colors, longest_button)) - .collect::>() - } - - fn to_helpinfo(&self, config: &Config) -> (HelpInfo, HelpInfo) { - let left = Self::create_button_line(&self.left, &config.app_colors); - let right = Self::create_button_line(&self.right, &config.app_colors); - - let size_left = HelpInfo::calc_size(&left); - let size_right = HelpInfo::calc_size(&right); - ( - HelpInfo { - lines: left, - size: size_left, - }, - HelpInfo { - lines: right, - size: size_right, - }, - ) - } -} - -impl KeyDescriptions { - fn as_str(&self) -> &'static str { - match self { - Self::Clear => "close dialog", - Self::Command => "send docker command", - Self::Exec => "exec into a container", - Self::FilterMode => "filter mode", - Self::Help => "toggle this panel", - Self::InspectMode => "container inspect mode", - Self::LogHeight => "change log section height", - Self::LogVisibility => "toggle of section visibility", - Self::MouseCapture => "toggle mouse capture - allows text selection", - Self::Panel => "change panel", - Self::Quit => "quit", - Self::Redraw => "force clear screen and redraw", - Self::Save => "save logs to file", - Self::ScrollH => "scroll horizontally", - Self::ScrollStart => "scroll to start", - Self::ScrollEnd => "scroll to end", - Self::ScrollSpeed => "increase scroll speed", - Self::ScrollV => "scroll vertically", - Self::SearchMode => "log search mode", - Self::SortHeader => "sort by header - or click header", - Self::SortStop => "stop sort", - Self::SortCpu => "sort by CPU", - Self::SortId => "sort by ID", - Self::SortImage => "sort by Image", - Self::SortMem => "sort by memory", - Self::SortName => "sort by name", - Self::SortRX => "sort by RX", - Self::SortState => "sort by state", - Self::SortStatus => "sort by status", - Self::SortTX => "sort by TX", - } - } -} - -/// Help popup box needs these three pieces of information -/// Change this to a trait -#[derive(Debug, Clone, Hash)] -struct HelpInfo { - lines: Vec>, - size: Size, -} - -static DEFAULT_NAME: LazyLock = LazyLock::new(|| { - let colors = AppColors::new(); - HelpInfo::gen_name_description(colors) -}); - -static DEFAULT_COLUMNS: LazyLock = - LazyLock::new(|| KeymapColumns::default(&Keymap::new())); - -impl HelpInfo { - /// Find the height and width of an array of lines - fn calc_size(lines: &[Line]) -> Size { - Size { - width: to_u16!( - lines - .iter() - .map(ratatui::prelude::Line::width) - .max() - .unwrap_or(1) - ), - height: to_u16!(lines.len()), - } - } - /// Just an empty span, i.e. a new line - fn empty_line<'a>() -> Line<'a> { - Line::from(String::new()) - } - - /// generate a span, of given &str and given color - fn span<'a>(input: String, color: Color) -> Span<'a> { - Span::styled(input, Style::default().fg(color)) - } - - /// &str to black text span - fn text_span<'a>(input: String, color: AppColors) -> Span<'a> { - Self::span(input, color.popup_help.text) - } - - /// &str to white text span - fn highlighted_text_span<'a>(input: String, color: AppColors) -> Span<'a> { - Self::span(input, color.popup_help.text_highlight) - } - - /// Generate the `oxker` name section - fn gen_name_description(colors: AppColors) -> Self { - let mut lines = NAME_TEXT - .lines() - .map(|i| Line::from(Self::highlighted_text_span(i.to_owned(), colors))) - .collect::>(); - lines.extend([ - Self::empty_line(), - Line::from(Self::highlighted_text_span(DESCRIPTION.to_owned(), colors)).centered(), - ]); - let size = Self::calc_size(&lines); - Self { lines, size } - } - - fn create_button<'a>( - input: &[Option], // Use a slice for better flexibility - color: AppColors, - spacing: usize, - ) -> Span<'a> { - let label = input - .iter() - .flatten() - .map(|s| s.as_str()) - .collect::>() - .join(" "); - - let padded_text = format!("{label:( - keys: &[Option], - desc: &KeyDescriptions, - app_colors: AppColors, - longest_button: usize, - ) -> Line<'a> { - Line::from(vec![ - Self::create_button(keys, app_colors, longest_button), - Span::from(desc.as_str()), - ]) - } - - fn gen_keymap_title(style: Style) -> Self { - let lines = vec![ - Self::empty_line(), - Line::from(Span::from("Keymap")) - .style(style) - .centered() - .underlined(), - ]; - let size = Self::calc_size(&lines); - Self { lines, size } - } - - fn gen_keymap(config: &Config) -> (Self, Self) { - let columns = if config.keymap == Keymap::new() { - DEFAULT_COLUMNS.clone() - } else { - KeymapColumns::custom(config) - }; - columns.to_helpinfo(config) - } - - fn gen_locations(config: &Config) -> Self { - let mut entries = Vec::new(); - - if let Some(path) = &config.dir_config { - entries.push(("config location: ", path.display().to_string())); - } - if let Some(path) = &config.dir_save { - entries.push(("export location: ", path.display().to_string())); - } - if config.show_timestamp { - let tz = config - .timezone - .as_ref() - .and_then(|t| t.iana_name()) - .unwrap_or("Etc/UTC"); - entries.push((" logs timezone: ", tz.to_string())); - } - - let max_len = entries - .iter() - .map(|(_, val)| val.chars().count()) - .max() - .unwrap_or_default(); - - // 3. Map entries to Lines - let mut lines = entries - .into_iter() - .map(|(label, val)| { - let spacing = " ".repeat(max_len.saturating_sub(val.chars().count())); - Line::from(vec![ - Self::text_span(label.to_owned(), config.app_colors), - Self::highlighted_text_span(format!("{spacing}{val}"), config.app_colors), - ]) - .right_aligned() - }) - .collect::>(); - - lines.extend([ - Self::empty_line(), - Line::from(Self::text_span( - "a work in progress, all and any input appreciated".to_owned(), - config.app_colors, - )), - Self::highlighted_text_span(REPO.to_owned(), config.app_colors) - .underlined() - .into_centered_line(), - ]); - - let size = Self::calc_size(&lines); - Self { lines, size } - } -} - -// Draw the oxker name on one half, other half shoe logs location, save location, timezone -fn draw_top_section( - f: &mut Frame, - area: Rect, - colors: AppColors, - style: Style, - name: HelpInfo, - config_dir: HelpInfo, -) { - let horizontal_split = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(area); - - let name_paragraph = Paragraph::new(name.lines) - .style( - Style::default() - .bg(colors.popup_help.background) - .fg(colors.popup_help.text_highlight), - ) - .alignment(Alignment::Center); - - let location_top_padding = name.size.height.saturating_sub(config_dir.size.height); - - let right_vertical_split = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Max(location_top_padding), - Constraint::Max(config_dir.size.height), - ]) - .split(horizontal_split[1]); - - let config_paragraph = Paragraph::new(config_dir.lines).style(style); - - f.render_widget(name_paragraph, horizontal_split[0]); - f.render_widget(config_paragraph, right_vertical_split[1]); -} - -fn draw_keymap_title(f: &mut Frame, area: Rect, title: HelpInfo) { - f.render_widget(Paragraph::new(title.lines), area); -} - -fn draw_keymap(f: &mut Frame, area: Rect, style: Style, columns: (HelpInfo, HelpInfo)) { - // Calculate some padding - let horizontal_padding = area - .width - .saturating_sub(columns.0.size.width) - .saturating_sub(columns.1.size.width) - .saturating_div(2) - .saturating_sub(1); - - let horizontal_split = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Max(horizontal_padding), - Constraint::Max(columns.0.size.width), - Constraint::Max(2), - Constraint::Max(columns.1.size.width), - Constraint::Max(horizontal_padding), - ]) - .split(area); - - let left_column = Paragraph::new(columns.0.lines).style(style).left_aligned(); - let right_column = Paragraph::new(columns.1.lines).style(style).left_aligned(); - f.render_widget(left_column, horizontal_split[1]); - f.render_widget(right_column, horizontal_split[3]); -} - -pub fn draw(config: &Config, f: &mut Frame) { - let default_colors = config.app_colors == AppColors::new(); - let title = format!(" {VERSION} "); - let style = Style::default() - .bg(config.app_colors.popup_help.background) - .fg(config.app_colors.popup_help.text); - - let name_info = if default_colors { - DEFAULT_NAME.clone() - } else { - HelpInfo::gen_name_description(config.app_colors) - }; - let locations = HelpInfo::gen_locations(config); - let keymap_title = HelpInfo::gen_keymap_title(style); - let keymap_columns = HelpInfo::gen_keymap(config); - - let total_width = name_info - .size - .width - // Account for spacing between the two sections - .saturating_add(locations.size.width) - .saturating_add(2) - .max( - keymap_columns - .0 - .size - .width - .saturating_add(keymap_columns.1.size.width) - // Account for the spacing spacing between each column - .saturating_add(2), - ); - let top_height = name_info.size.height.max(locations.size.height); - let keymap_height = keymap_columns - .0 - .size - .height - .max(keymap_columns.1.size.height); - let total_height = top_height - .saturating_add(keymap_title.size.height) - .saturating_add(keymap_height); - - let block = Block::default() - .title(title) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(style) - .style(style) - .padding(Padding::horizontal(1)); - - let area = popup::draw( - (total_height + 2).into(), - (total_width + 4).into(), - f.area(), - BoxLocation::MiddleCentre, - ); - - let inner_area = block.inner(area); - - let vertical_split = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(top_height), - Constraint::Length(keymap_title.size.height), - Constraint::Length(keymap_height), - ]) - .split(inner_area); - - f.render_widget(Clear, area); - f.render_widget(block, area); - draw_top_section( - f, - vertical_split[0], - config.app_colors, - style, - name_info, - locations, - ); - draw_keymap_title(f, vertical_split[1], keymap_title); - draw_keymap(f, vertical_split[2], style, keymap_columns); -} - -#[cfg(test)] -#[allow(clippy::unwrap_used, clippy::too_many_lines)] -mod tests { - use std::path::PathBuf; - - use crate::config::{AppColors, Keymap}; - use crossterm::event::{KeyCode, KeyModifiers}; - use insta::assert_snapshot; - use ratatui::style::Color; - - use crate::ui::draw_blocks::tests::{get_result, test_setup}; - - #[test] - /// This test is incredibly annoying - /// println!("{} {} {} {} {}", row_index, result_cell_index, result_cell.symbol(), result_cell.bg, result_cell.fg); - fn test_draw_blocks_help() { - let mut setup = test_setup(118, 25, true, true); - setup.app_data.lock().config.dir_save = Some(PathBuf::from("/test_dir")); - setup.app_data.lock().config.dir_config = - Some(PathBuf::from("/home/user/.config/oxker/config.toml")); - setup.app_data.lock().config.show_timestamp = true; - - setup - .terminal - .draw(|f| { - super::draw(&setup.app_data.lock().config, f); - }) - .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) { - // The space around the popup - (0|24, _) | (_, 0|117) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Reset, Color::Reset)), - // The borders - (1|23, 1..=23) | (_, 1|116) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::Black)), - // The oxker logo - // The description - (2..=10, 3..=58)| - // Config location - (5, 79..=114) | - // Export location - (6, 79..=114) | - // Timezone - (7, 79..=114) | - //url - (10, 69..=104) | - // Left column - (13..=22, 4..=24) | - // Right Column - (13..=21,59..=69) - => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::White)), - _ => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::Black)), - }; - } - } - } - - #[test] - fn test_draw_blocks_help_no_config() { - let mut setup = test_setup(116, 25, true, true); - setup.app_data.lock().config.dir_save = Some(PathBuf::from("/test_dir")); - setup.app_data.lock().config.show_timestamp = true; - - setup - .terminal - .draw(|f| { - super::draw(&setup.app_data.lock().config, f); - }) - .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) { - // The space around the popup - (0|24, _) | (_, 0|115) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Reset, Color::Reset)), - // The borders - (1|23, 1..=23) | (_, 1|114) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::Black)), - // The oxker logo - // The description - (2..=10, 3..=57)| - // Export location - (6, 104..=112) | - // Timezone - (7, 104..=112) | - //url - (10, 67..=102) | - // Left column - (13..=22, 3..=23) | - // Right Column - (13..=21,58..=68) - => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::White)), - _ => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::Black)), - }; - } - } - } - - #[test] - fn test_draw_blocks_help_no_save() { - let mut setup = test_setup(118, 25, true, true); - setup.app_data.lock().config.dir_config = - Some(PathBuf::from("/home/user/.config/oxker/config.toml")); - setup.app_data.lock().config.show_timestamp = true; - - setup - .terminal - .draw(|f| { - super::draw(&setup.app_data.lock().config, f); - }) - .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) { - // The space around the popup - (0|24, _) | (_, 0|117) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Reset, Color::Reset)), - // The borders - (1|23, 1..=23) | (_, 1|116) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::Black)), - // The oxker logo - // The description - (2..=10, 3..=58)| - // Config location - (6, 79..=114) | - // Timezone - (7, 79..=114) | - //url - (10, 69..=104) | - // Left column - (13..=22, 4..=24) | - // Right Column - (13..=21,59..=69) - => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::White)), - _ => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::Black)), - }; - } - } - } - - #[test] - fn test_draw_blocks_help_no_timezone() { - let mut setup = test_setup(118, 25, true, true); - setup.app_data.lock().config.dir_save = Some(PathBuf::from("/test_dir")); - setup.app_data.lock().config.dir_config = - Some(PathBuf::from("/home/user/.config/oxker/config.toml")); - - setup - .terminal - .draw(|f| { - super::draw(&setup.app_data.lock().config, f); - }) - .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) { - // The space around the popup - (0|24, _) | (_, 0|117) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Reset, Color::Reset)), - // The borders - (1|23, 1..=23) | (_, 1|116) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::Black)), - // The oxker logo - // The description - (2..=10, 3..=58)| - // Config location - (6, 79..=114) | - // Export location - (7, 79..=114) | - //url - (10, 69..=104) | - // Left column - (13..=22, 4..=24) | - // Right Column - (13..=21,59..=69) - => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::White)), - _ => assert_eq!((result_cell.bg, result_cell.fg), (Color::Magenta, Color::Black)), - }; - } - } - } - - #[test] - fn test_draw_blocks_help_custom_color() { - let mut setup = test_setup(118, 25, true, true); - setup.app_data.lock().config.dir_save = Some(PathBuf::from("/test_dir")); - setup.app_data.lock().config.dir_config = - Some(PathBuf::from("/home/user/.config/oxker/config.toml")); - setup.app_data.lock().config.show_timestamp = true; - - let mut colors = AppColors::new(); - colors.popup_help.background = Color::Black; - colors.popup_help.text = Color::Red; - colors.popup_help.text_highlight = Color::Yellow; - - setup.app_data.lock().config.app_colors = colors; - - setup - .terminal - .draw(|f| { - super::draw(&setup.app_data.lock().config, f); - }) - .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) { - // The space around the popup - (0|24, _) | (_, 0|117) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Reset, Color::Reset)), - // The borders - (1|23, 1..=23) | (_, 1|116) => assert_eq!((result_cell.bg, result_cell.fg), (Color::Black, Color::Red)), - // The oxker logo - // The description - (2..=10, 3..=58)| - // Config location - (5, 79..=114) | - // Export location - (6, 79..=114) | - // Timezone - (7, 79..=114) | - //url - (10, 69..=104) | - // Left column - (13..=22, 4..=24) | - // Right Column - (13..=21,59..=69) - => assert_eq!((result_cell.bg, result_cell.fg), (Color::Black, Color::Yellow)), - _ => assert_eq!((result_cell.bg, result_cell.fg), (Color::Black, Color::Red)), - }; - } - } - } - - #[test] - /// Help panel will show custom keymap if in use, with one definition for each entry - fn test_draw_blocks_help_custom_keymap_one_definition() { - let mut setup = test_setup(118, 25, true, true); - - setup.app_data.lock().config.dir_save = Some(PathBuf::from("/test_dir")); - setup.app_data.lock().config.dir_config = - Some(PathBuf::from("/home/user/.config/oxker/config.toml")); - setup.app_data.lock().config.show_timestamp = true; - - let keymap = Keymap { - clear: (KeyCode::Char('a'), None), - delete_confirm: (KeyCode::Char('b'), None), - delete_deny: (KeyCode::Char('c'), None), - exec: (KeyCode::Char('d'), None), - inspect: (KeyCode::Char('e'), None), - filter_mode: (KeyCode::Char('f'), None), - log_search_mode: (KeyCode::Char('g'), None), - force_redraw: (KeyCode::Char('h'), None), - scroll_back: (KeyCode::Char('i'), None), - scroll_forward: (KeyCode::Char('j'), None), - log_section_height_decrease: (KeyCode::Char('k'), None), - log_section_height_increase: (KeyCode::Char('l'), None), - log_section_toggle: (KeyCode::Char('m'), None), - quit: (KeyCode::Char('n'), None), - save_logs: (KeyCode::Char('o'), None), - scroll_down: (KeyCode::Char('p'), None), - scroll_end: (KeyCode::Char('q'), None), - scroll_many: KeyModifiers::ALT, - scroll_start: (KeyCode::Char('r'), None), - scroll_up: (KeyCode::Char('s'), None), - select_next_panel: (KeyCode::Char('t'), None), - select_previous_panel: (KeyCode::Char('u'), None), - sort_by_cpu: (KeyCode::Char('v'), None), - sort_by_id: (KeyCode::Char('w'), None), - sort_by_image: (KeyCode::Char('x'), None), - sort_by_memory: (KeyCode::Char('y'), None), - sort_by_name: (KeyCode::Char('z'), None), - sort_by_rx: (KeyCode::Char('0'), None), - sort_by_state: (KeyCode::Char('1'), None), - sort_by_status: (KeyCode::Char('2'), None), - sort_by_tx: (KeyCode::Char('3'), None), - sort_reset: (KeyCode::Char('4'), None), - toggle_help: (KeyCode::Char('5'), None), - toggle_mouse_capture: (KeyCode::Char('6'), None), - }; - - setup.app_data.lock().config.keymap = keymap; - - setup - .terminal - .draw(|f| { - super::draw(&setup.app_data.lock().config, f); - }) - .unwrap(); - - assert_snapshot!(setup.terminal.backend()); - } - - #[test] - /// Help panel will show custom keymap if in use, with two definitions for each entry - fn test_draw_blocks_help_custom_keymap_two_definition() { - let mut setup = test_setup(124, 30, true, true); - - setup.app_data.lock().config.dir_save = Some(PathBuf::from("/test_dir")); - setup.app_data.lock().config.dir_config = - Some(PathBuf::from("/home/user/.config/oxker/config.toml")); - setup.app_data.lock().config.show_timestamp = true; - - let keymap = 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'))), - inspect: (KeyCode::Char('i'), Some(KeyCode::Char('j'))), - filter_mode: (KeyCode::Char('k'), Some(KeyCode::Char('l'))), - log_search_mode: (KeyCode::Char('m'), Some(KeyCode::Char('n'))), - force_redraw: (KeyCode::Char('o'), Some(KeyCode::Char('p'))), - scroll_back: (KeyCode::Char('q'), Some(KeyCode::Char('r'))), - scroll_forward: (KeyCode::Char('s'), Some(KeyCode::Char('t'))), - log_section_height_decrease: (KeyCode::Char('u'), Some(KeyCode::Char('v'))), - log_section_height_increase: (KeyCode::Char('w'), Some(KeyCode::Char('x'))), - log_section_toggle: (KeyCode::Char('y'), Some(KeyCode::Char('z'))), - quit: (KeyCode::Char('0'), Some(KeyCode::Char('1'))), - save_logs: (KeyCode::Char('2'), Some(KeyCode::Char('3'))), - scroll_down: (KeyCode::Char('4'), Some(KeyCode::Char('5'))), - scroll_end: (KeyCode::Char('6'), Some(KeyCode::Char('7'))), - scroll_many: KeyModifiers::ALT, - scroll_start: (KeyCode::Char('8'), Some(KeyCode::Char('9'))), - scroll_up: (KeyCode::CapsLock, Some(KeyCode::ScrollLock)), - select_next_panel: (KeyCode::PrintScreen, Some(KeyCode::Right)), - select_previous_panel: (KeyCode::Left, Some(KeyCode::Up)), - sort_by_cpu: (KeyCode::Down, Some(KeyCode::Delete)), - sort_by_id: (KeyCode::BackTab, Some(KeyCode::Backspace)), - sort_by_image: (KeyCode::End, Some(KeyCode::Esc)), - sort_by_memory: (KeyCode::Home, Some(KeyCode::Insert)), - sort_by_name: (KeyCode::KeypadBegin, Some(KeyCode::Menu)), - sort_by_rx: (KeyCode::NumLock, Some(KeyCode::PageDown)), - sort_by_state: (KeyCode::PageUp, Some(KeyCode::Pause)), - sort_by_status: (KeyCode::PrintScreen, Some(KeyCode::Tab)), - sort_by_tx: (KeyCode::F(1), Some(KeyCode::F(2))), - sort_reset: (KeyCode::F(3), Some(KeyCode::F(4))), - toggle_help: (KeyCode::F(5), Some(KeyCode::F(6))), - toggle_mouse_capture: (KeyCode::F(7), Some(KeyCode::F(8))), - }; - - setup.app_data.lock().config.keymap = keymap; - - setup - .terminal - .draw(|f| { - super::draw(&setup.app_data.lock().config, f); - }) - .unwrap(); - - assert_snapshot!(setup.terminal.backend()); - } - - #[test] - /// Help panel will show custom keymap if in use, with one or two definitions for each entry - fn test_draw_blocks_help_custom_keymap_one_two_definition() { - let mut setup = test_setup(124, 30, true, true); - - setup.app_data.lock().config.dir_save = Some(PathBuf::from("/test_dir")); - setup.app_data.lock().config.dir_config = - Some(PathBuf::from("/home/user/.config/oxker/config.toml")); - setup.app_data.lock().config.show_timestamp = true; - - let keymap = Keymap { - clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))), - delete_confirm: (KeyCode::Char('c'), None), - delete_deny: (KeyCode::Char('e'), Some(KeyCode::Char('f'))), - exec: (KeyCode::Char('g'), None), - inspect: (KeyCode::Char('i'), Some(KeyCode::Char('j'))), - filter_mode: (KeyCode::Char('k'), None), - log_search_mode: (KeyCode::Char('m'), Some(KeyCode::Char('n'))), - force_redraw: (KeyCode::Char('o'), None), - scroll_back: (KeyCode::Char('q'), Some(KeyCode::Char('r'))), - scroll_forward: (KeyCode::Char('s'), None), - log_section_height_decrease: (KeyCode::Char('u'), Some(KeyCode::Char('v'))), - log_section_height_increase: (KeyCode::Char('w'), None), - log_section_toggle: (KeyCode::Char('y'), Some(KeyCode::Char('z'))), - quit: (KeyCode::Char('0'), None), - save_logs: (KeyCode::Char('2'), Some(KeyCode::Char('3'))), - scroll_down: (KeyCode::Char('4'), None), - scroll_end: (KeyCode::Char('6'), Some(KeyCode::Char('7'))), - scroll_many: KeyModifiers::ALT, - scroll_start: (KeyCode::Char('8'), None), - scroll_up: (KeyCode::CapsLock, Some(KeyCode::ScrollLock)), - select_next_panel: (KeyCode::PrintScreen, None), - select_previous_panel: (KeyCode::Left, Some(KeyCode::Up)), - sort_by_cpu: (KeyCode::Down, None), - sort_by_id: (KeyCode::BackTab, None), - sort_by_image: (KeyCode::End, Some(KeyCode::Esc)), - sort_by_memory: (KeyCode::Home, None), - sort_by_name: (KeyCode::KeypadBegin, Some(KeyCode::Menu)), - sort_by_rx: (KeyCode::NumLock, None), - sort_by_state: (KeyCode::PageUp, Some(KeyCode::Pause)), - sort_by_status: (KeyCode::PrintScreen, None), - sort_by_tx: (KeyCode::F(1), Some(KeyCode::F(2))), - sort_reset: (KeyCode::F(3), None), - toggle_help: (KeyCode::F(5), Some(KeyCode::F(6))), - toggle_mouse_capture: (KeyCode::F(7), None), - }; - - setup.app_data.lock().config.keymap = keymap; - - setup - .terminal - .draw(|f| { - super::draw(&setup.app_data.lock().config, f); - }) - .unwrap(); - - assert_snapshot!(setup.terminal.backend()); - } -} diff --git a/src/ui/draw_blocks/info.rs b/src/ui/draw_blocks/info.rs deleted file mode 100644 index 89094d6..0000000 --- a/src/ui/draw_blocks/info.rs +++ /dev/null @@ -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>, - 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); - } - } - } -} diff --git a/src/ui/draw_blocks/inspect.rs b/src/ui/draw_blocks/inspect.rs deleted file mode 100644 index 3c5917d..0000000 --- a/src/ui/draw_blocks/inspect.rs +++ /dev/null @@ -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> { - 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::(), - ) - }) - .collect() -} - -/// Draw the InspectContainer widget to the entire screen -pub fn draw( - f: &mut Frame, - colors: AppColors, - data: InspectData, - gui_state: &Arc>, - 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 = - 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()]), - }, - )])), - }), -} - } -} diff --git a/src/ui/draw_blocks/logs.rs b/src/ui/draw_blocks/logs.rs deleted file mode 100644 index 98b643f..0000000 --- a/src/ui/draw_blocks/logs.rs +++ /dev/null @@ -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>, - area: Rect, - colors: AppColors, - f: &mut Frame, - fd: &FrameData, - gui_state: &Arc>, -) { - 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()); - } - } - } - } - } -} diff --git a/src/ui/draw_blocks/mod.rs b/src/ui/draw_blocks/mod.rs deleted file mode 100644 index 147e166..0000000 --- a/src/ui/draw_blocks/mod.rs +++ /dev/null @@ -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>, - 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>, - pub gui_state: Arc>, - pub fd: FrameData, - pub area: Rect, - pub terminal: Terminal, - pub ids: Vec, - } - - 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's, instead of from UI - impl From<(&Arc>, &Arc>)> for FrameData { - fn from(data: (&Arc>, &Arc>)) -> 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> { - 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::>(); - 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()); - } -} diff --git a/src/ui/draw_blocks/popup.rs b/src/ui/draw_blocks/popup.rs deleted file mode 100644 index 1b9329e..0000000 --- a/src/ui/draw_blocks/popup.rs +++ /dev/null @@ -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] -} diff --git a/src/ui/draw_blocks/ports.rs b/src/ui/draw_blocks/ports.rs deleted file mode 100644 index 44fd16d..0000000 --- a/src/ui/draw_blocks/ports.rs +++ /dev/null @@ -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); - } - } - } - } - } -} diff --git a/src/ui/draw_blocks/search_logs.rs b/src/ui/draw_blocks/search_logs.rs deleted file mode 100644 index 335aa31..0000000 --- a/src/ui/draw_blocks/search_logs.rs +++ /dev/null @@ -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); - } - } - } - } - } -} diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_custom_colors.snap deleted file mode 100644 index 699967c..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_custom_colors.snap +++ /dev/null @@ -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 │ •• │" -"│ │•• ••• │" -"│ │•••••• │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_dead.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_dead.snap deleted file mode 100644 index 64b250a..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_dead.snap +++ /dev/null @@ -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│ │" -"│ │ │" -"│ │ │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_paused.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_paused.snap deleted file mode 100644 index 64b250a..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_paused.snap +++ /dev/null @@ -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│ │" -"│ │ │" -"│ │ │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_none.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_none.snap deleted file mode 100644 index 64b250a..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_none.snap +++ /dev/null @@ -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│ │" -"│ │ │" -"│ │ │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data.snap deleted file mode 100644 index f2451e2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data.snap +++ /dev/null @@ -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 │ •• │" -"│ │•• │" -"│ │ │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_rx.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_rx.snap deleted file mode 100644 index bff8635..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_rx.snap +++ /dev/null @@ -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 │ •• │" -"│ │•• │" -"│ │• │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_tx.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_tx.snap deleted file mode 100644 index f2451e2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_tx.snap +++ /dev/null @@ -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 │ •• │" -"│ │•• │" -"│ │ │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_tx_and_rx.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_tx_and_rx.snap deleted file mode 100644 index 699967c..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_bandwidth__tests__draw_blocks_charts_running_with_data_tx_and_rx.snap +++ /dev/null @@ -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 │ •• │" -"│ │•• ••• │" -"│ │•••••• │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_custom_colors.snap deleted file mode 100644 index 8a75efc..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_custom_colors.snap +++ /dev/null @@ -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│ • │" -"│ │ •• ││ │ •• │" -"│ │ • • ││ │ •• │" -"│ │ • • ││ │ • • │" -"│ │ • • ││ │ •• • │" -"│ │ • •• ││ │ • • │" -"│ │•• •• ││ │• • │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_dead.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_dead.snap deleted file mode 100644 index 8a75efc..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_dead.snap +++ /dev/null @@ -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│ • │" -"│ │ •• ││ │ •• │" -"│ │ • • ││ │ •• │" -"│ │ • • ││ │ • • │" -"│ │ • • ││ │ •• • │" -"│ │ • •• ││ │ • • │" -"│ │•• •• ││ │• • │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_paused.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_paused.snap deleted file mode 100644 index 8a75efc..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_paused.snap +++ /dev/null @@ -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│ • │" -"│ │ •• ││ │ •• │" -"│ │ • • ││ │ •• │" -"│ │ • • ││ │ • • │" -"│ │ • • ││ │ •• • │" -"│ │ • •• ││ │ • • │" -"│ │•• •• ││ │• • │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_running_none.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_running_none.snap deleted file mode 100644 index 8632071..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_running_none.snap +++ /dev/null @@ -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│ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_running_some.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_running_some.snap deleted file mode 100644 index 8a75efc..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__chart_cpu_mem__tests__draw_blocks_charts_running_some.snap +++ /dev/null @@ -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│ • │" -"│ │ •• ││ │ •• │" -"│ │ • • ││ │ •• │" -"│ │ • • ││ │ • • │" -"│ │ • • ││ │ •• • │" -"│ │ • •• ││ │ • • │" -"│ │•• •• ││ │• • │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_custom_colors.snap deleted file mode 100644 index 90160a2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_custom_colors.snap +++ /dev/null @@ -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│ • │" -"│ │ •• ││ │ •• │" -"│ │ • • ││ │ •• │" -"│ │ • • ││ │ • • │" -"│ │ • • ││ │ •• • │" -"│ │ • •• ││ │ • • │" -"│ │•• •• ││ │• • │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_dead.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_dead.snap deleted file mode 100644 index 90160a2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_dead.snap +++ /dev/null @@ -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│ • │" -"│ │ •• ││ │ •• │" -"│ │ • • ││ │ •• │" -"│ │ • • ││ │ • • │" -"│ │ • • ││ │ •• • │" -"│ │ • •• ││ │ • • │" -"│ │•• •• ││ │• • │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_paused.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_paused.snap deleted file mode 100644 index 90160a2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_paused.snap +++ /dev/null @@ -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│ • │" -"│ │ •• ││ │ •• │" -"│ │ • • ││ │ •• │" -"│ │ • • ││ │ • • │" -"│ │ • • ││ │ •• • │" -"│ │ • •• ││ │ • • │" -"│ │•• •• ││ │• • │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_running_none.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_running_none.snap deleted file mode 100644 index af71149..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_running_none.snap +++ /dev/null @@ -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│ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_running_some.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_running_some.snap deleted file mode 100644 index 90160a2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__charts__tests__draw_blocks_charts_running_some.snap +++ /dev/null @@ -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│ • │" -"│ │ •• ││ │ •• │" -"│ │ • • ││ │ •• │" -"│ │ • • ││ │ • • │" -"│ │ • • ││ │ •• • │" -"│ │ • •• ││ │ • • │" -"│ │•• •• ││ │• • │" -"│ │ ││ │ │" -"╰──────────────────────────────────────╯╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_custom_colors_paused.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_custom_colors_paused.snap deleted file mode 100644 index 8aaca4d..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_custom_colors_paused.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/commands.rs -expression: setup.terminal.backend() ---- -"╭──────────╮" -"│ resume │" -"│▶ stop │" -"│ delete │" -"│ │" -"╰──────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_custom_colors_running.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_custom_colors_running.snap deleted file mode 100644 index a4f3838..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_custom_colors_running.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/commands.rs -expression: setup.terminal.backend() ---- -"╭──────────╮" -"│▶ pause │" -"│ restart │" -"│ stop │" -"│ delete │" -"╰──────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_none.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_none.snap deleted file mode 100644 index 9af74a1..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_none.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/commands.rs -expression: setup.terminal.backend() ---- -"╭──────────╮" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_panel_selected_color.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_panel_selected_color.snap deleted file mode 100644 index a4f3838..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_panel_selected_color.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/commands.rs -expression: setup.terminal.backend() ---- -"╭──────────╮" -"│▶ pause │" -"│ restart │" -"│ stop │" -"│ delete │" -"╰──────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_some.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_some.snap deleted file mode 100644 index a4f3838..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_some.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/commands.rs -expression: setup.terminal.backend() ---- -"╭──────────╮" -"│▶ pause │" -"│ restart │" -"│ stop │" -"│ delete │" -"╰──────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_some_paused.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_some_paused.snap deleted file mode 100644 index 8aaca4d..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__commands__tests__draw_blocks_commands_some_paused.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/commands.rs -expression: setup.terminal.backend() ---- -"╭──────────╮" -"│ resume │" -"│▶ stop │" -"│ delete │" -"│ │" -"╰──────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_colors.snap deleted file mode 100644 index b7b75a5..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_colors.snap +++ /dev/null @@ -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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors.snap deleted file mode 100644 index b7b75a5..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors.snap +++ /dev/null @@ -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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_dead.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_dead.snap deleted file mode 100644 index 5e8c7d2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_dead.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ✖ dead 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_exited.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_exited.snap deleted file mode 100644 index 15afd7c..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_exited.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ✖ exited 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_healthy.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_healthy.snap deleted file mode 100644 index b7b75a5..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_healthy.snap +++ /dev/null @@ -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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_paused.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_paused.snap deleted file mode 100644 index 9d95084..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_paused.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ॥ paused 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_removing.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_removing.snap deleted file mode 100644 index 3412602..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_removing.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 removing 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_restarting.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_restarting.snap deleted file mode 100644 index 22c8635..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_restarting.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ↻ restarting 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_unhealthy.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_unhealthy.snap deleted file mode 100644 index 611c8dd..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_unhealthy.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ! running Up 1 hour (unhealthy) 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_unknown.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_unknown.snap deleted file mode 100644 index a86d2b2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_custom_colors_state_unknown.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ? unknown 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_dead.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_dead.snap deleted file mode 100644 index 5e8c7d2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_dead.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ✖ dead 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_exited.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_exited.snap deleted file mode 100644 index 15afd7c..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_exited.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ✖ exited 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_long_name_image.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_long_name_image.snap deleted file mode 100644 index ef1be6d..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_long_name_image.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ a_long_container_name_for_the… ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 a_long_image_name_for_the_pur… 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_none.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_none.snap deleted file mode 100644 index 55702b6..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_none.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers ──────────────────────────╮" -"│ no containers running │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_paused.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_paused.snap deleted file mode 100644 index 9d95084..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_paused.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ॥ paused 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_removing.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_removing.snap deleted file mode 100644 index 3412602..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_removing.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 removing 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_restarting.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_restarting.snap deleted file mode 100644 index 332bc75..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_restarting.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ↻ restarting 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_selected_bold.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_selected_bold.snap deleted file mode 100644 index b7b75a5..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_selected_bold.snap +++ /dev/null @@ -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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_unhealthy.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_unhealthy.snap deleted file mode 100644 index 264dc2b..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_unhealthy.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ! running Up 1 hour (unhealthy) 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_unknown.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_unknown.snap deleted file mode 100644 index a86d2b2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__containers__tests__draw_blocks_containers_unknown.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/containers.rs -expression: setup.terminal.backend() ---- -"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│⚪ container_1 ? unknown 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 │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete.snap deleted file mode 100644 index 9a63a7f..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/ui/draw_blocks/delete_confirm.rs -expression: setup.terminal.backend() ---- -" " -" ╭──────────────────────── Confirm Delete ────────────────────────╮ " -" │ │ " -" │ Are you sure you want to delete container: container_1 │ " -" │ │ " -" │ ╭─────────────────────╮ ╭─────────────────────╮ │ " -" │ │ ( n ) no │ │ ( y ) yes │ │ " -" │ ╰─────────────────────╯ ╰─────────────────────╯ │ " -" ╰────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_colors.snap deleted file mode 100644 index 9a63a7f..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_colors.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/ui/draw_blocks/delete_confirm.rs -expression: setup.terminal.backend() ---- -" " -" ╭──────────────────────── Confirm Delete ────────────────────────╮ " -" │ │ " -" │ Are you sure you want to delete container: container_1 │ " -" │ │ " -" │ ╭─────────────────────╮ ╭─────────────────────╮ │ " -" │ │ ( n ) no │ │ ( y ) yes │ │ " -" │ ╰─────────────────────╯ ╰─────────────────────╯ │ " -" ╰────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_one_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_one_definition.snap deleted file mode 100644 index 8574f25..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_one_definition.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/ui/draw_blocks/delete_confirm.rs -expression: setup.terminal.backend() ---- -" " -" ╭──────────────────────── Confirm Delete ────────────────────────╮ " -" │ │ " -" │ Are you sure you want to delete container: container_1 │ " -" │ │ " -" │ ╭─────────────────────╮ ╭─────────────────────╮ │ " -" │ │ ( End ) no │ │ ( F10 ) yes │ │ " -" │ ╰─────────────────────╯ ╰─────────────────────╯ │ " -" ╰────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_one_two_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_one_two_definition.snap deleted file mode 100644 index 85cdc89..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_one_two_definition.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/ui/draw_blocks/delete_confirm.rs -expression: setup.terminal.backend() ---- -" " -" ╭──────────────────────── Confirm Delete ────────────────────────╮ " -" │ │ " -" │ Are you sure you want to delete container: container_1 │ " -" │ │ " -" │ ╭─────────────────────╮ ╭─────────────────────╮ │ " -" │ │ ( End | Up ) no │ │ ( F10 ) yes │ │ " -" │ ╰─────────────────────╯ ╰─────────────────────╯ │ " -" ╰────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_two_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_two_definition.snap deleted file mode 100644 index 67f83d1..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_custom_keymap_two_definition.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/ui/draw_blocks/delete_confirm.rs -expression: setup.terminal.backend() ---- -" " -" ╭──────────────────────── Confirm Delete ────────────────────────╮ " -" │ │ " -" │ Are you sure you want to delete container: container_1 │ " -" │ │ " -" │ ╭─────────────────────╮ ╭─────────────────────╮ │ " -" │ │ ( End | Up ) no │ │ ( F10 | L ) yes │ │ " -" │ ╰─────────────────────╯ ╰─────────────────────╯ │ " -" ╰────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_long_name.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_long_name.snap deleted file mode 100644 index ed1e07c..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__delete_confirm__tests__draw_blocks_delete_long_name.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/ui/draw_blocks/delete_confirm.rs -expression: setup.terminal.backend() ---- -" " -" ╭──────────────────────────────────── Confirm Delete ────────────────────────────────────╮ " -" │ │ " -" │ Are you sure you want to delete container: container_1_container_1_container_1 │ " -" │ │ " -" │ ╭──────────────────────────────╮ ╭─────────────────────────────╮ │ " -" │ │ ( n ) no │ │ ( y ) yes │ │ " -" │ ╰──────────────────────────────╯ ╰─────────────────────────────╯ │ " -" ╰────────────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_clearable_error.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_clearable_error.snap deleted file mode 100644 index 57550a3..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_clearable_error.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: src/ui/draw_blocks/error.rs -expression: setup.terminal.backend() ---- -" " -" ╭────────────── Error ──────────────╮ " -" │ │ " -" │ Unable to exec into container │ " -" │ │ " -" │ ( c ) clear error │ " -" │ │ " -" │ ( q ) quit oxker │ " -" │ │ " -" ╰───────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_colors.snap deleted file mode 100644 index 57550a3..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_colors.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: src/ui/draw_blocks/error.rs -expression: setup.terminal.backend() ---- -" " -" ╭────────────── Error ──────────────╮ " -" │ │ " -" │ Unable to exec into container │ " -" │ │ " -" │ ( c ) clear error │ " -" │ │ " -" │ ( q ) quit oxker │ " -" │ │ " -" ╰───────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap.snap deleted file mode 100644 index 26eab89..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: src/ui/draw_blocks/error.rs -expression: setup.terminal.backend() ---- -" " -" ╭────────────── Error ──────────────╮ " -" │ │ " -" │ Unable to exec into container │ " -" │ │ " -" │ ( Back Tab ) clear error │ " -" │ │ " -" │ ( F4 ) quit oxker │ " -" │ │ " -" ╰───────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap_one_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap_one_two_definitions.snap deleted file mode 100644 index d00b144..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap_one_two_definitions.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: src/ui/draw_blocks/error.rs -expression: setup.terminal.backend() ---- -" " -" ╭────────────── Error ──────────────╮ " -" │ │ " -" │ Unable to exec into container │ " -" │ │ " -" │ ( c ) clear error │ " -" │ │ " -" │ ( F4 | End ) quit oxker │ " -" │ │ " -" ╰───────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap_two_definitions.snap deleted file mode 100644 index d238e61..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_custom_keymap_two_definitions.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: src/ui/draw_blocks/error.rs -expression: setup.terminal.backend() ---- -" " -" ╭────────────── Error ──────────────╮ " -" │ │ " -" │ Unable to exec into container │ " -" │ │ " -" │ ( Back Tab | m ) clear error │ " -" │ │ " -" │ ( F4 | End ) quit oxker │ " -" │ │ " -" ╰───────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_docker_connect_error.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_docker_connect_error.snap deleted file mode 100644 index 6bd72ae..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_docker_connect_error.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: src/ui/draw_blocks/error.rs -expression: setup.terminal.backend() ---- -" " -" ╭─────────────────── Error ────────────────────╮ " -" │ │ " -" │ Unable to access docker daemon │ " -" │ │ " -" │ oxker::v0.00.000 closing in 04 seconds │ " -" │ │ " -" │ ( q ) quit oxker │ " -" │ │ " -" ╰──────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_docker_connect_error_custom_host.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_docker_connect_error_custom_host.snap deleted file mode 100644 index 9dabfef..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__error__tests__draw_blocks_error_docker_connect_error_custom_host.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: src/ui/draw_blocks/error.rs -expression: setup.terminal.backend() ---- -" " -" ╭──────────────────────── Error ─────────────────────────╮ " -" │ │ " -" │ Unable to access docker daemon @ "/test/host.sock" │ " -" │ │ " -" │ oxker::v0.00.000 closing in 04 seconds │ " -" │ │ " -" │ ( q ) quit oxker │ " -" │ │ " -" ╰────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row.snap deleted file mode 100644 index f7b9563..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/filter.rs -expression: setup.terminal.backend() ---- -" Esc clear ← by → Name Image Status All filter term: " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_custom_colors.snap deleted file mode 100644 index fbf7f0f..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_custom_colors.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/filter.rs -expression: setup.terminal.backend() ---- -" Esc clear ← by → Name Image Status All filter term: cd " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_filter_by.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_filter_by.snap deleted file mode 100644 index f7b9563..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_filter_by.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/filter.rs -expression: setup.terminal.backend() ---- -" Esc clear ← by → Name Image Status All filter term: " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_s.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_s.snap deleted file mode 100644 index 9f5b920..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_s.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/filter.rs -expression: setup.terminal.backend() ---- -" Esc clear ← by → Name Image Status All term: " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_text.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_text.snap deleted file mode 100644 index fbf7f0f..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__filter__tests__draw_blocks_filter_row_text.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/filter.rs -expression: setup.terminal.backend() ---- -" Esc clear ← by → Name Image Status All filter term: cd " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_animation.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_animation.snap deleted file mode 100644 index 3663ed8..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_animation.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_colors.snap deleted file mode 100644 index 3663ed8..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_colors.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_one_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_one_definition.snap deleted file mode 100644 index 89a0392..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_one_definition.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( T ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_teo_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_teo_definitions.snap deleted file mode 100644 index 5770658..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_teo_definitions.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( T | Tab ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_two_definitions.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_two_definitions.snap deleted file mode 100644 index 5770658..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_custom_keymap_two_definitions.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( T | Tab ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_exit_help.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_exit_help.snap deleted file mode 100644 index 24104eb..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_exit_help.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" ( h ) exit help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_hide_help.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_hide_help.snap deleted file mode 100644 index 24104eb..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_hide_help.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" ( h ) exit help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_show_help.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_show_help.snap deleted file mode 100644 index d1dd0af..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_no_containers_show_help.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_some_containers.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_some_containers.snap deleted file mode 100644 index d33195b..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_some_containers.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_some_containers_reduced_width.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_some_containers_reduced_width.snap deleted file mode 100644 index ecbc8c6..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_some_containers_reduced_width.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_cpu_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_cpu_asc.snap deleted file mode 100644 index 84362a8..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_cpu_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_cpu_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_cpu_desc.snap deleted file mode 100644 index 251edd2..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_cpu_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_id_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_id_asc.snap deleted file mode 100644 index a48ca82..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_id_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_id_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_id_desc.snap deleted file mode 100644 index 4f0347b..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_id_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_image_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_image_asc.snap deleted file mode 100644 index 3eb0637..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_image_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_image_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_image_desc.snap deleted file mode 100644 index 32e5410..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_image_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_memory_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_memory_asc.snap deleted file mode 100644 index 1f70235..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_memory_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_memory_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_memory_desc.snap deleted file mode 100644 index 82da172..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_memory_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_name_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_name_asc.snap deleted file mode 100644 index 13c19a7..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_name_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_name_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_name_desc.snap deleted file mode 100644 index 7607d82..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_name_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_rx_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_rx_asc.snap deleted file mode 100644 index fc567d1..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_rx_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_rx_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_rx_desc.snap deleted file mode 100644 index c63b5f9..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_rx_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_state_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_state_asc.snap deleted file mode 100644 index 3a24db0..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_state_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_state_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_state_desc.snap deleted file mode 100644 index 5e2becf..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_state_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_status_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_status_asc.snap deleted file mode 100644 index 7846ba4..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_status_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_status_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_status_desc.snap deleted file mode 100644 index 5de7ea7..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_status_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_tx_asc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_tx_asc.snap deleted file mode 100644 index 17666e9..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_tx_asc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ▲ ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_tx_desc.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_tx_desc.snap deleted file mode 100644 index 5941028..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__headers__tests__draw_blocks_headers_sort_containers_tx_desc.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/headers.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ▼ ( h ) show help " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap deleted file mode 100644 index a68dbd3..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: src/ui/draw_blocks/help.rs -expression: setup.terminal.backend() ---- -" " -" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y config location: /home/user/.config/oxker/config.toml │ " -" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ a work in progress, all and any input appreciated │ " -" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ " -" │ │ " -" │ Keymap │ " -" │ q quit c Esc close dialog │ " -" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ " -" │ Control increase scroll speed Enter send docker command │ " -" │ e exec into a container i container inspect mode │ " -" │ / F1 filter mode # log search mode │ " -" │ h toggle this panel f force clear screen and redraw │ " -" │ - = change log section height \ toggle of section visibility │ " -" │ 1 ~ 9 sort by header - or click header 0 stop sort │ " -" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ " -" │ s save logs to file │ " -" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_color.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_color.snap deleted file mode 100644 index a68dbd3..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_color.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: src/ui/draw_blocks/help.rs -expression: setup.terminal.backend() ---- -" " -" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y config location: /home/user/.config/oxker/config.toml │ " -" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ a work in progress, all and any input appreciated │ " -" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ " -" │ │ " -" │ Keymap │ " -" │ q quit c Esc close dialog │ " -" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ " -" │ Control increase scroll speed Enter send docker command │ " -" │ e exec into a container i container inspect mode │ " -" │ / F1 filter mode # log search mode │ " -" │ h toggle this panel f force clear screen and redraw │ " -" │ - = change log section height \ toggle of section visibility │ " -" │ 1 ~ 9 sort by header - or click header 0 stop sort │ " -" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ " -" │ s save logs to file │ " -" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap deleted file mode 100644 index a7b1daf..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_definition.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: src/ui/draw_blocks/help.rs -expression: setup.terminal.backend() ---- -" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y config location: /home/user/.config/oxker/config.toml │ " -" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ " -" │ │ " -" │ Keymap │ " -" │ n quit a close dialog │ " -" │ p s scroll vertically i j scroll horizontally │ " -" │ r scroll to start q scroll to end │ " -" │ Alt increase scroll speed Enter send docker command │ " -" │ d exec into a container e container inspect mode │ " -" │ f filter mode g log search mode │ " -" │ 5 toggle this panel h force clear screen and redraw │ " -" │ k l change log section height m toggle of section visibility │ " -" │ z sort by name 1 sort by state │ " -" │ 2 sort by status v sort by CPU │ " -" │ y sort by memory w sort by ID │ " -" │ x sort by Image 0 sort by RX │ " -" │ 3 sort by TX 4 stop sort │ " -" │ t u change panel 6 toggle mouse capture - allows text selection │ " -" │ o save logs to file │ " -" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_two_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_two_definition.snap deleted file mode 100644 index ed14a24..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_one_two_definition.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/help.rs -expression: setup.terminal.backend() ---- -" " -" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y config location: /home/user/.config/oxker/config.toml │ " -" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ a work in progress, all and any input appreciated │ " -" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ " -" │ │ " -" │ Keymap │ " -" │ 0 quit a b close dialog │ " -" │ 4 Caps Lock Scroll Lock scroll vertically q s r scroll horizontally │ " -" │ 8 scroll to start 6 7 scroll to end │ " -" │ Alt increase scroll speed Enter send docker command │ " -" │ g exec into a container i j container inspect mode │ " -" │ k filter mode m n log search mode │ " -" │ F5 F6 toggle this panel o force clear screen and redraw │ " -" │ u w v change log section height y z toggle of section visibility │ " -" │ Begin Menu sort by name Page Up Pause sort by state │ " -" │ Print Screen sort by status Down sort by CPU │ " -" │ Home sort by memory Back Tab sort by ID │ " -" │ End Esc sort by Image Num Lock sort by RX │ " -" │ F1 F2 sort by TX F3 stop sort │ " -" │ Print Screen Left Up change panel F7 toggle mouse capture - allows text selection │ " -" │ 2 3 save logs to file │ " -" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definition.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definition.snap deleted file mode 100644 index 1aecc71..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_custom_keymap_two_definition.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/help.rs -expression: setup.terminal.backend() ---- -" " -" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 config location: /home/user/.config/oxker/config.toml │ " -" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ a work in progress, all and any input appreciated │ " -" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ " -" │ │ " -" │ Keymap │ " -" │ 0 1 quit a b close dialog │ " -" │ 4 Caps Lock 5 Scroll Lock scroll vertically q s r t scroll horizontally │ " -" │ 8 9 scroll to start 6 7 scroll to end │ " -" │ Alt increase scroll speed Enter send docker command │ " -" │ g h exec into a container i j container inspect mode │ " -" │ k l filter mode m n log search mode │ " -" │ F5 F6 toggle this panel o p force clear screen and redraw │ " -" │ u w v x change log section height y z toggle of section visibility │ " -" │ Begin Menu sort by name Page Up Pause sort by state │ " -" │ Print Screen Tab sort by status Down Del sort by CPU │ " -" │ Home Insert sort by memory Back Tab Backspace sort by ID │ " -" │ End Esc sort by Image Num Lock Page Down sort by RX │ " -" │ F1 F2 sort by TX F3 F4 stop sort │ " -" │ Print Screen Left Up Right change panel F7 F8 toggle mouse capture - allows text selection │ " -" │ 2 3 save logs to file │ " -" ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_config.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_config.snap deleted file mode 100644 index b6ae220..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_config.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: src/ui/draw_blocks/help.rs -expression: setup.terminal.backend() ---- -" " -" ╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYb │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' " │ " -" │ 8b d8 )888( 8888( 8PP""""""" 88 export location: /test_dir │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ a work in progress, all and any input appreciated │ " -" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ " -" │ │ " -" │ Keymap │ " -" │ q quit c Esc close dialog │ " -" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ " -" │ Control increase scroll speed Enter send docker command │ " -" │ e exec into a container i container inspect mode │ " -" │ / F1 filter mode # log search mode │ " -" │ h toggle this panel f force clear screen and redraw │ " -" │ - = change log section height \ toggle of section visibility │ " -" │ 1 ~ 9 sort by header - or click header 0 stop sort │ " -" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ " -" │ s save logs to file │ " -" ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_save.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_save.snap deleted file mode 100644 index a1240bf..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_save.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: src/ui/draw_blocks/help.rs -expression: setup.terminal.backend() ---- -" " -" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y │ " -" │ 8b d8 )888( 8888( 8PP""""""" 88 config location: /home/user/.config/oxker/config.toml │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 logs timezone: Etc/UTC │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ a work in progress, all and any input appreciated │ " -" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ " -" │ │ " -" │ Keymap │ " -" │ q quit c Esc close dialog │ " -" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ " -" │ Control increase scroll speed Enter send docker command │ " -" │ e exec into a container i container inspect mode │ " -" │ / F1 filter mode # log search mode │ " -" │ h toggle this panel f force clear screen and redraw │ " -" │ - = change log section height \ toggle of section visibility │ " -" │ 1 ~ 9 sort by header - or click header 0 stop sort │ " -" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ " -" │ s save logs to file │ " -" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_timezone.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_timezone.snap deleted file mode 100644 index 01aa561..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__help__tests__draw_blocks_help_no_timezone.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: src/ui/draw_blocks/help.rs -expression: setup.terminal.backend() ---- -" " -" ╭ 0.00.000 ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ " -" │ 88 │ " -" │ 88 │ " -" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba │ " -" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y │ " -" │ 8b d8 )888( 8888( 8PP""""""" 88 config location: /home/user/.config/oxker/config.toml │ " -" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 export location: /test_dir │ " -" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ " -" │ a work in progress, all and any input appreciated │ " -" │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ " -" │ │ " -" │ Keymap │ " -" │ q quit c Esc close dialog │ " -" │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ " -" │ Control increase scroll speed Enter send docker command │ " -" │ e exec into a container i container inspect mode │ " -" │ / F1 filter mode # log search mode │ " -" │ h toggle this panel f force clear screen and redraw │ " -" │ - = change log section height \ toggle of section visibility │ " -" │ 1 ~ 9 sort by header - or click header 0 stop sort │ " -" │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ " -" │ s save logs to file │ " -" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__info__tests__draw_blocks_info.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__info__tests__draw_blocks_info.snap deleted file mode 100644 index 1f84da6..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__info__tests__draw_blocks_info.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/ui/draw_blocks/info.rs -expression: setup.terminal.backend() ---- -" " -" " -" " -" " -" " -" " -" " -" test " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__info__tests__draw_blocks_info_custom_color.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__info__tests__draw_blocks_info_custom_color.snap deleted file mode 100644 index 1f84da6..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__info__tests__draw_blocks_info_custom_color.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/ui/draw_blocks/info.rs -expression: setup.terminal.backend() ---- -" " -" " -" " -" " -" " -" " -" " -" test " -" " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_color.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_color.snap deleted file mode 100644 index 0c5cf65..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_color.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or i to exit ─────────────────────╮" -"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │" -"│ "Created": "2026-01-23T22:20:19.927967311Z", │" -"│ "Path": "docker-entrypoint.sh", │" -"│ "Args": [ │" -"│ "postgres" │" -"│ ], │" -"│ "State": { │" -"│ "Status": "running", │" -"│ "Running": true, │" -"│ "Paused": false, │" -"│ "Restarting": false, │" -"│ "OOMKilled": false, │" -"│ "Dead": false, │" -"│ "Pid": 782, │" -"│ "ExitCode": 0, │" -"│ "Error": "", │" -"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │" -"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │" -"│ }, │" -"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │" -"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│" -"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│" -"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│" -"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│" -"│ "Name": "/postgres", │" -"│ "RestartCount": 0, │" -"│ "Driver": "overlay2", │" -"│ "Platform": "linux", │" -"│ "MountLabel": "", │" -"│ "ProcessLabel": "", │" -"│ "AppArmorProfile": "", │" -"│ "HostConfig": { │" -"│ "CpuShares": 0, │" -"│ "Memory": 1073741824, │" -"│ "CgroupParent": "", │" -"│ "BlkioWeight": 0, │" -"│ "CpuPeriod": 0, │" -"│ "CpuQuota": 0, │" -"│ "CpuRealtimePeriod": 0, │" -"│ "CpuRealtimeRuntime": 0, │" -"│ "CpusetCpus": "", │" -"│ "CpusetMems": "", │" -"│ "MemoryReservation": 0, │" -"│ "MemorySwap": 2147483648, │" -"│ "NanoCpus": 0, │" -"│ "OomKillDisable": false, │" -"│ "CpuCount": 0, │" -"│ "CpuPercent": 0, │" -"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_all.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_all.snap deleted file mode 100644 index 9ab52b8..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_all.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭─────────────────── inspecting: postgres 0bdea642 - F or Z or 4 or 5 to exit ────────────────────╮" -"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │" -"│ "Created": "2026-01-23T22:20:19.927967311Z", │" -"│ "Path": "docker-entrypoint.sh", │" -"│ "Args": [ │" -"│ "postgres" │" -"│ ], │" -"│ "State": { │" -"│ "Status": "running", │" -"│ "Running": true, │" -"│ "Paused": false, │" -"│ "Restarting": false, │" -"│ "OOMKilled": false, │" -"│ "Dead": false, │" -"│ "Pid": 782, │" -"│ "ExitCode": 0, │" -"│ "Error": "", │" -"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │" -"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │" -"│ }, │" -"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │" -"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│" -"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│" -"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│" -"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│" -"│ "Name": "/postgres", │" -"│ "RestartCount": 0, │" -"│ "Driver": "overlay2", │" -"│ "Platform": "linux", │" -"│ "MountLabel": "", │" -"│ "ProcessLabel": "", │" -"│ "AppArmorProfile": "", │" -"│ "HostConfig": { │" -"│ "CpuShares": 0, │" -"│ "Memory": 1073741824, │" -"│ "CgroupParent": "", │" -"│ "BlkioWeight": 0, │" -"│ "CpuPeriod": 0, │" -"│ "CpuQuota": 0, │" -"│ "CpuRealtimePeriod": 0, │" -"│ "CpuRealtimeRuntime": 0, │" -"│ "CpusetCpus": "", │" -"│ "CpusetMems": "", │" -"│ "MemoryReservation": 0, │" -"│ "MemorySwap": 2147483648, │" -"│ "NanoCpus": 0, │" -"│ "OomKillDisable": false, │" -"│ "CpuCount": 0, │" -"│ "CpuPercent": 0, │" -"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_clear_one.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_clear_one.snap deleted file mode 100644 index ed18a89..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_clear_one.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭───────────────────── inspecting: postgres 0bdea642 - F or Esc or i to exit ─────────────────────╮" -"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │" -"│ "Created": "2026-01-23T22:20:19.927967311Z", │" -"│ "Path": "docker-entrypoint.sh", │" -"│ "Args": [ │" -"│ "postgres" │" -"│ ], │" -"│ "State": { │" -"│ "Status": "running", │" -"│ "Running": true, │" -"│ "Paused": false, │" -"│ "Restarting": false, │" -"│ "OOMKilled": false, │" -"│ "Dead": false, │" -"│ "Pid": 782, │" -"│ "ExitCode": 0, │" -"│ "Error": "", │" -"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │" -"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │" -"│ }, │" -"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │" -"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│" -"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│" -"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│" -"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│" -"│ "Name": "/postgres", │" -"│ "RestartCount": 0, │" -"│ "Driver": "overlay2", │" -"│ "Platform": "linux", │" -"│ "MountLabel": "", │" -"│ "ProcessLabel": "", │" -"│ "AppArmorProfile": "", │" -"│ "HostConfig": { │" -"│ "CpuShares": 0, │" -"│ "Memory": 1073741824, │" -"│ "CgroupParent": "", │" -"│ "BlkioWeight": 0, │" -"│ "CpuPeriod": 0, │" -"│ "CpuQuota": 0, │" -"│ "CpuRealtimePeriod": 0, │" -"│ "CpuRealtimeRuntime": 0, │" -"│ "CpusetCpus": "", │" -"│ "CpusetMems": "", │" -"│ "MemoryReservation": 0, │" -"│ "MemorySwap": 2147483648, │" -"│ "NanoCpus": 0, │" -"│ "OomKillDisable": false, │" -"│ "CpuCount": 0, │" -"│ "CpuPercent": 0, │" -"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_clear_two.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_clear_two.snap deleted file mode 100644 index c4b4d46..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_clear_two.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭────────────────────── inspecting: postgres 0bdea642 - F or Z or i to exit ──────────────────────╮" -"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │" -"│ "Created": "2026-01-23T22:20:19.927967311Z", │" -"│ "Path": "docker-entrypoint.sh", │" -"│ "Args": [ │" -"│ "postgres" │" -"│ ], │" -"│ "State": { │" -"│ "Status": "running", │" -"│ "Running": true, │" -"│ "Paused": false, │" -"│ "Restarting": false, │" -"│ "OOMKilled": false, │" -"│ "Dead": false, │" -"│ "Pid": 782, │" -"│ "ExitCode": 0, │" -"│ "Error": "", │" -"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │" -"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │" -"│ }, │" -"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │" -"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│" -"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│" -"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│" -"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│" -"│ "Name": "/postgres", │" -"│ "RestartCount": 0, │" -"│ "Driver": "overlay2", │" -"│ "Platform": "linux", │" -"│ "MountLabel": "", │" -"│ "ProcessLabel": "", │" -"│ "AppArmorProfile": "", │" -"│ "HostConfig": { │" -"│ "CpuShares": 0, │" -"│ "Memory": 1073741824, │" -"│ "CgroupParent": "", │" -"│ "BlkioWeight": 0, │" -"│ "CpuPeriod": 0, │" -"│ "CpuQuota": 0, │" -"│ "CpuRealtimePeriod": 0, │" -"│ "CpuRealtimeRuntime": 0, │" -"│ "CpusetCpus": "", │" -"│ "CpusetMems": "", │" -"│ "MemoryReservation": 0, │" -"│ "MemorySwap": 2147483648, │" -"│ "NanoCpus": 0, │" -"│ "OomKillDisable": false, │" -"│ "CpuCount": 0, │" -"│ "CpuPercent": 0, │" -"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_inspect_one.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_inspect_one.snap deleted file mode 100644 index f1144ec..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_inspect_one.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or 4 to exit ─────────────────────╮" -"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │" -"│ "Created": "2026-01-23T22:20:19.927967311Z", │" -"│ "Path": "docker-entrypoint.sh", │" -"│ "Args": [ │" -"│ "postgres" │" -"│ ], │" -"│ "State": { │" -"│ "Status": "running", │" -"│ "Running": true, │" -"│ "Paused": false, │" -"│ "Restarting": false, │" -"│ "OOMKilled": false, │" -"│ "Dead": false, │" -"│ "Pid": 782, │" -"│ "ExitCode": 0, │" -"│ "Error": "", │" -"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │" -"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │" -"│ }, │" -"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │" -"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│" -"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│" -"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│" -"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│" -"│ "Name": "/postgres", │" -"│ "RestartCount": 0, │" -"│ "Driver": "overlay2", │" -"│ "Platform": "linux", │" -"│ "MountLabel": "", │" -"│ "ProcessLabel": "", │" -"│ "AppArmorProfile": "", │" -"│ "HostConfig": { │" -"│ "CpuShares": 0, │" -"│ "Memory": 1073741824, │" -"│ "CgroupParent": "", │" -"│ "BlkioWeight": 0, │" -"│ "CpuPeriod": 0, │" -"│ "CpuQuota": 0, │" -"│ "CpuRealtimePeriod": 0, │" -"│ "CpuRealtimeRuntime": 0, │" -"│ "CpusetCpus": "", │" -"│ "CpusetMems": "", │" -"│ "MemoryReservation": 0, │" -"│ "MemorySwap": 2147483648, │" -"│ "NanoCpus": 0, │" -"│ "OomKillDisable": false, │" -"│ "CpuCount": 0, │" -"│ "CpuPercent": 0, │" -"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_inspect_two.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_inspect_two.snap deleted file mode 100644 index e2eb4bc..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_custom_keymap_inspect_two.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭────────────────── inspecting: postgres 0bdea642 - c or Esc or 4 or 5 to exit ───────────────────╮" -"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │" -"│ "Created": "2026-01-23T22:20:19.927967311Z", │" -"│ "Path": "docker-entrypoint.sh", │" -"│ "Args": [ │" -"│ "postgres" │" -"│ ], │" -"│ "State": { │" -"│ "Status": "running", │" -"│ "Running": true, │" -"│ "Paused": false, │" -"│ "Restarting": false, │" -"│ "OOMKilled": false, │" -"│ "Dead": false, │" -"│ "Pid": 782, │" -"│ "ExitCode": 0, │" -"│ "Error": "", │" -"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │" -"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │" -"│ }, │" -"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │" -"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│" -"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│" -"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│" -"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│" -"│ "Name": "/postgres", │" -"│ "RestartCount": 0, │" -"│ "Driver": "overlay2", │" -"│ "Platform": "linux", │" -"│ "MountLabel": "", │" -"│ "ProcessLabel": "", │" -"│ "AppArmorProfile": "", │" -"│ "HostConfig": { │" -"│ "CpuShares": 0, │" -"│ "Memory": 1073741824, │" -"│ "CgroupParent": "", │" -"│ "BlkioWeight": 0, │" -"│ "CpuPeriod": 0, │" -"│ "CpuQuota": 0, │" -"│ "CpuRealtimePeriod": 0, │" -"│ "CpuRealtimeRuntime": 0, │" -"│ "CpusetCpus": "", │" -"│ "CpusetMems": "", │" -"│ "MemoryReservation": 0, │" -"│ "MemorySwap": 2147483648, │" -"│ "NanoCpus": 0, │" -"│ "OomKillDisable": false, │" -"│ "CpuCount": 0, │" -"│ "CpuPercent": 0, │" -"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_default_valid.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_default_valid.snap deleted file mode 100644 index 0c5cf65..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_default_valid.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or i to exit ─────────────────────╮" -"│ "Id": "0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7", │" -"│ "Created": "2026-01-23T22:20:19.927967311Z", │" -"│ "Path": "docker-entrypoint.sh", │" -"│ "Args": [ │" -"│ "postgres" │" -"│ ], │" -"│ "State": { │" -"│ "Status": "running", │" -"│ "Running": true, │" -"│ "Paused": false, │" -"│ "Restarting": false, │" -"│ "OOMKilled": false, │" -"│ "Dead": false, │" -"│ "Pid": 782, │" -"│ "ExitCode": 0, │" -"│ "Error": "", │" -"│ "StartedAt": "2026-01-30T08:09:01.574885915Z", │" -"│ "FinishedAt": "2026-01-30T08:09:01.180567927Z" │" -"│ }, │" -"│ "Image": "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │" -"│ "ResolvConfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c853│" -"│ "HostnamePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358│" -"│ "HostsPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456│" -"│ "LogPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc│" -"│ "Name": "/postgres", │" -"│ "RestartCount": 0, │" -"│ "Driver": "overlay2", │" -"│ "Platform": "linux", │" -"│ "MountLabel": "", │" -"│ "ProcessLabel": "", │" -"│ "AppArmorProfile": "", │" -"│ "HostConfig": { │" -"│ "CpuShares": 0, │" -"│ "Memory": 1073741824, │" -"│ "CgroupParent": "", │" -"│ "BlkioWeight": 0, │" -"│ "CpuPeriod": 0, │" -"│ "CpuQuota": 0, │" -"│ "CpuRealtimePeriod": 0, │" -"│ "CpuRealtimeRuntime": 0, │" -"│ "CpusetCpus": "", │" -"│ "CpusetMems": "", │" -"│ "MemoryReservation": 0, │" -"│ "MemorySwap": 2147483648, │" -"│ "NanoCpus": 0, │" -"│ "OomKillDisable": false, │" -"│ "CpuCount": 0, │" -"│ "CpuPercent": 0, │" -"╰──────────────────────────────────── 0/158 ↓ 0/972 → ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_offset.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_offset.snap deleted file mode 100644 index 503c826..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_offset.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or i to exit ─────────────────────╮" -"│rting": false, │" -"│lled": false, │" -"│: false, │" -"│ 782, │" -"│ode": 0, │" -"│": "", │" -"│edAt": "2026-01-30T08:09:01.574885915Z", │" -"│hedAt": "2026-01-30T08:09:01.180567927Z" │" -"│ │" -"│ "sha256:aa3668fcbcb5ded731b7d5c27065a4edf545debb7f27bf514c709b1b4e032352", │" -"│onfPath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb│" -"│ePath": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60│" -"│th": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/│" -"│": "/var/lib/docker/containers/0bdea64212f9c75eb4a1184dd406c2c79a986a7a889a23c85358456cc1bb60c7/0b│" -"│"/postgres", │" -"│Count": 0, │" -"│: "overlay2", │" -"│m": "linux", │" -"│bel": "", │" -"│Label": "", │" -"│rProfile": "", │" -"│fig": { │" -"│ares": 0, │" -"│y": 1073741824, │" -"│pParent": "", │" -"│Weight": 0, │" -"│riod": 0, │" -"│ota": 0, │" -"│altimePeriod": 0, │" -"│altimeRuntime": 0, │" -"│tCpus": "", │" -"│tMems": "", │" -"│yReservation": 0, │" -"│ySwap": 2147483648, │" -"│pus": 0, │" -"│llDisable": false, │" -"│unt": 0, │" -"│rcent": 0, │" -"│imumIOps": 0, │" -"│imumBandwidth": 0, │" -"│inerIDFile": "", │" -"│nfig": { │" -"│e": "json-file", │" -"│fig": {} │" -"│ │" -"│rkMode": "oxker-examaple-net", │" -"│indings": {}, │" -"│rtPolicy": { │" -"╰──────────────────────────────────── ↑ 10/158 ↓ ← 10/972 → ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_offset_max.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_offset_max.snap deleted file mode 100644 index b7e3d72..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__inspect__tests__draw_blocks_inspect_offset_max.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: src/ui/draw_blocks/inspect.rs -expression: setup.terminal.backend() ---- -"╭───────────────────── inspecting: postgres 0bdea642 - c or Esc or i to exit ─────────────────────╮" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────── ↑ 158/158 ← 972/972 ────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_logs.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_logs.snap deleted file mode 100644 index 8794006..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_logs.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs 3/3 - container_1 - image_1 ╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"╰──────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_no_logs.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_no_logs.snap deleted file mode 100644 index 2dd7564..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_no_logs.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs - container_1 - image_1 ───╮" -"│ no logs found │" -"│ │" -"│ │" -"│ │" -"╰─────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_parsing.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_parsing.snap deleted file mode 100644 index e5d9fe3..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_custom_colors_parsing.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs - container_1 - image_1 ╮" -"│ parsing logs ⠙ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_long_name.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_long_name.snap deleted file mode 100644 index 292e367..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_long_name.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test - a_long_image╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"╰──────────────────────────────────────────────────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_none.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_none.snap deleted file mode 100644 index 2dd7564..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_none.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs - container_1 - image_1 ───╮" -"│ no logs found │" -"│ │" -"│ │" -"│ │" -"╰─────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing.snap deleted file mode 100644 index e5d9fe3..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs - container_1 - image_1 ╮" -"│ parsing logs ⠙ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing_frame_one.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing_frame_one.snap deleted file mode 100644 index e5d9fe3..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing_frame_one.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs - container_1 - image_1 ╮" -"│ parsing logs ⠙ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing_frame_two.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing_frame_two.snap deleted file mode 100644 index 7408f6c..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_parsing_frame_two.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs - container_1 - image_1 ╮" -"│ parsing logs ⠹ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_some_line_three.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_some_line_three.snap deleted file mode 100644 index 8794006..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_some_line_three.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs 3/3 - container_1 - image_1 ╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"╰──────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_some_line_two.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_some_line_two.snap deleted file mode 100644 index 45b697f..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__logs__tests__draw_blocks_logs_some_line_two.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: src/ui/draw_blocks/logs.rs -expression: setup.terminal.backend() ---- -"╭ Logs 2/3 - container_1 - image_1 ╮" -"│ line 1 │" -"│▶ line 2 │" -"│ line 3 │" -"│ │" -"╰──────────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_container_state.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_container_state.snap deleted file mode 100644 index 704a66e..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_container_state.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭─────────── ports ────────────╮" -"│ ip private public │" -"│ 8001 │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors.snap deleted file mode 100644 index 704a66e..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭─────────── ports ────────────╮" -"│ ip private public │" -"│ 8001 │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-2.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-2.snap deleted file mode 100644 index 704a66e..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-2.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭─────────── ports ────────────╮" -"│ ip private public │" -"│ 8001 │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-3.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-3.snap deleted file mode 100644 index 704a66e..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-3.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭─────────── ports ────────────╮" -"│ ip private public │" -"│ 8001 │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-4.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-4.snap deleted file mode 100644 index 704a66e..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state-4.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭─────────── ports ────────────╮" -"│ ip private public │" -"│ 8001 │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state.snap deleted file mode 100644 index 704a66e..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_custom_colors_state.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭─────────── ports ────────────╮" -"│ ip private public │" -"│ 8001 │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_multiple_ports.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_multiple_ports.snap deleted file mode 100644 index 0294461..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_multiple_ports.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭─────────── ports ────────────╮" -"│ ip private public │" -"│ 8001 │" -"│ 8002 │" -"│127.0.0.1 8003 8003 │" -"│ │" -"│ │" -"╰──────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_no_ports.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_no_ports.snap deleted file mode 100644 index 5d0aa17..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_no_ports.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭────────── ports ───────────╮" -"│ no ports │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_no_ports_dead.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_no_ports_dead.snap deleted file mode 100644 index af9a790..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__ports__tests__draw_blocks_ports_no_ports_dead.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/ui/draw_blocks/ports.rs -expression: setup.terminal.backend() ---- -"╭────────── ports ───────────╮" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_colors.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_colors.snap deleted file mode 100644 index aa27273..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_colors.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/search_logs.rs -expression: setup.terminal.backend() ---- -" Esc clear search term: e ↑ previous ↓ next matches: 2/3" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_keymap.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_keymap.snap deleted file mode 100644 index eafd76b..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_keymap.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/search_logs.rs -expression: setup.terminal.backend() ---- -" Esc clear search term: e b previous a next matches: 2/3" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_both_next_previous.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_both_next_previous.snap deleted file mode 100644 index aa27273..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_both_next_previous.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/search_logs.rs -expression: setup.terminal.backend() ---- -" Esc clear search term: e ↑ previous ↓ next matches: 2/3" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_next.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_next.snap deleted file mode 100644 index c341ef3..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_next.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/search_logs.rs -expression: setup.terminal.backend() ---- -" Esc clear search term: e ↓ next matches: 1/3" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_none.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_none.snap deleted file mode 100644 index 4b4d112..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_none.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/search_logs.rs -expression: setup.terminal.backend() ---- -" Esc clear search term: z " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_previous.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_previous.snap deleted file mode 100644 index 52a2e16..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_match_previous.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/search_logs.rs -expression: setup.terminal.backend() ---- -" Esc clear search term: e ↑ previous matches: 3/3" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_row.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_row.snap deleted file mode 100644 index 27d8aff..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__search_logs__tests__draw_blocks_log_search_row.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/ui/draw_blocks/search_logs.rs -expression: setup.terminal.backend() ---- -" Esc clear search term: " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout.snap deleted file mode 100644 index b9fa938..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭────────────── cpu 03.00% ───────────────╮╭──────────── memory 30.00 kB ────────────╮╭────── rx: 0.00 kb/s tx: 0.00 kb/s ──────╮ ╭────────── ports ───────────╮" -"│10.00%│ •• ││100.00 kB│ •• ││ │••••••• │ │ ip private public│" -"│ │ ••• ││ │ ••• ││ │ •• │ │ 8001 │" -"│ │ •• • ││ │ •• • ││0.00 kb/s│ •• │ │127.0.0.1 8003 8003│" -"│ │ • •• ││ │ • •• ││0.00 kb/s│ • │ │ │" -"│ │• • ││ │• • ││ │ • │ │ │" -"╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯ ╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_delete.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_delete.snap deleted file mode 100644 index 0f52b1d..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_delete.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"│ ││ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ ╭──────────────────────── Confirm Delete ────────────────────────╮ │" -"│ │ │ │" -"│ │ Are you sure you want to delete container: container_1 │ │" -"│ │ │ │" -"│ │ ╭─────────────────────╮ ╭─────────────────────╮ │ │" -"│ │ │ ( n ) no │ │ ( y ) yes │ │ │" -"│ │ ╰─────────────────────╯ ╰─────────────────────╯ │ │" -"│ ╰────────────────────────────────────────────────────────────────╯ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭────────────── cpu 03.00% ───────────────╮╭──────────── memory 30.00 kB ────────────╮╭────── rx: 0.00 kb/s tx: 0.00 kb/s ──────╮ ╭────────── ports ───────────╮" -"│10.00%│ • ││100.00 kB│ • ││ │••••••• │ │ ip private public│" -"│ │ •• ││ │ •• ││ │ •• │ │ 8001 │" -"│ │ • • ││ │ • • ││ │ •• │ │127.0.0.1 8003 8003│" -"│ │ • • ││ │ • • ││0.00 kb/s│ •• │ │ │" -"│ │ • • ││ │ • • ││ │ • │ │ │" -"│ │ • •• ││ │ • •• ││0.00 kb/s│ • │ │ │" -"│ │•• • ││ │•• •• ││ │ • │ │ │" -"│ │ ││ │ ││ │ • │ │ │" -"╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯ ╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_error.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_error.snap deleted file mode 100644 index 90db02f..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_error.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"│ ││ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"│ │" -"│ │" -"│ ╭──────────── Error ─────────────╮ │" -"│ │ │ │" -"│ │ Unable to pause container │ │" -"│ │ │ │" -"│ │ ( c ) clear error │ │" -"│ │ │ │" -"│ │ ( q ) quit oxker │ │" -"│ │ │ │" -"│ │ │ │" -"│ ╰────────────────────────────────╯ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭────────────── cpu 03.00% ───────────────╮╭──────────── memory 30.00 kB ────────────╮╭────── rx: 0.00 kb/s tx: 0.00 kb/s ──────╮ ╭────────── ports ───────────╮" -"│10.00%│ • ││100.00 kB│ • ││ │••••••• │ │ ip private public│" -"│ │ •• ││ │ •• ││ │ •• │ │ 8001 │" -"│ │ • • ││ │ • • ││ │ •• │ │127.0.0.1 8003 8003│" -"│ │ • • ││ │ • • ││0.00 kb/s│ •• │ │ │" -"│ │ • • ││ │ • • ││ │ • │ │ │" -"│ │ • •• ││ │ • •• ││0.00 kb/s│ • │ │ │" -"│ │•• • ││ │•• •• ││ │ • │ │ │" -"│ │ ││ │ ││ │ • │ │ │" -"╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯ ╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap deleted file mode 100644 index 706c766..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_help_panel.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) exit help " -"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"│ ││ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_╭ 0.00.000 ──────────────────────────────────────────────────────────────────────────────────────────────────────╮──────────────────────╮" -"│ line 1 │ 88 │ │" -"│ line 2 │ 88 │ │" -"│▶ line 3 │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYb │ │" -"│ │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' " │ │" -"│ │ 8b d8 )888( 8888( 8PP""""""" 88 │ │" -"│ │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ │" -"│ │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ │" -"│ │ a work in progress, all and any input appreciated │ │" -"│ │ A simple tui to view & control docker containers https://github.com/mrjackwills/oxker │ │" -"│ │ │ │" -"│ │ Keymap │ │" -"│ │ q quit c Esc close dialog │ │" -"│ │ Down Up j k Home End scroll vertically Left Right scroll horizontally │ │" -"│ │ Control increase scroll speed Enter send docker command │ │" -"│ │ e exec into a container i container inspect mode │ │" -"│ │ / F1 filter mode # log search mode │ │" -"│ │ h toggle this panel f force clear screen and redraw │ │" -"│ │ - = change log section height \ toggle of section visibility │ │" -"│ │ 1 ~ 9 sort by header - or click header 0 stop sort │ │" -"│ │ Tab Back Tab change panel m toggle mouse capture - allows text selection │ │" -"╰──────────────────────│ s save logs to file │──────────────────────╯" -"╭────────────── cpu 03.│ │──── ports ───────────╮" -"│10.00%│ • ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ip private public│" -"│ │ •• ││ │ •• ││ │ •• │ │ 8001 │" -"│ │ • • ││ │ • • ││ │ •• │ │127.0.0.1 8003 8003│" -"│ │ • • ││ │ • • ││0.00 kb/s│ •• │ │ │" -"│ │ • • ││ │ • • ││ │ • │ │ │" -"│ │ • •• ││ │ • •• ││0.00 kb/s│ • │ │ │" -"│ │•• • ││ │•• •• ││ │ • │ │ │" -"│ │ ││ │ ││ │ • │ │ │" -"╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯ ╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_info_box.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_info_box.snap deleted file mode 100644 index b12a958..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_info_box.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"│ ││ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭────────────── cpu 03.00% ───────────────╮╭──────────── memory 30.00 kB ────────────╮╭────── rx: 0.00 kb/s tx: 0.00 kb/s ──────╮ ╭────────── ports ───────────╮" -"│10.00%│ • ││100.00 kB│ • ││ │••••••• │ │ ip private public│" -"│ │ •• ││ │ •• ││ │ •• │ │ 8001 │" -"│ │ • • ││ │ • • ││ │ •• │ │127.0.0.1 8003 8003│" -"│ │ • • ││ │ • • ││0.00 kb/s│ •• │ │ │" -"│ │ • • ││ │ • • ││ │ • │ │ │" -"│ │ • •• ││ │ • •• ││0.00 kb/s│ • │ │ " -"│ │•• • ││ │•• •• ││ │ • │ │ This is a test " -"│ │ ││ │ ││ │ • │ │ " -"╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯ ╰─────── " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_long_name.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_long_name.snap deleted file mode 100644 index e7b9019..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_long_name.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/3 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭─────────────────╮" -"│⚪ a_long_container_name_for_the… ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰─────────────────╯" -"╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test - a_long_image_name_for_the_purposes_of_this_test ──────────────────────────────────────────────────────────────────────────╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭─────────────────── cpu 03.00% ────────────────────╮╭───────────────── memory 30.00 kB ─────────────────╮╭────────── rx: 0.00 kb/s tx: 0.00 kb/s ───────────╮ ╭────────── ports ───────────╮" -"│10.00%│ • ││100.00 kB│ •• ││ │•••••• • │ │ ip private public│" -"│ │ ••• ││ │ ••• ││ │ • • │ │ 8001 │" -"│ │ ••• • ││ │ •• • ││0.00 kb/s│ • • │ │127.0.0.1 8003 8003│" -"│ │ • ••• ││ │ • •• ││0.00 kb/s│ • │ │ │" -"│ │• • ││ │• • ││ │ • │ │ │" -"╰───────────────────────────────────────────────────╯╰───────────────────────────────────────────────────╯╰──────────────────────────────────────────────────╯ ╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_no_logs.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_no_logs.snap deleted file mode 100644 index e0131b5..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_no_logs.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭────────────── cpu 03.00% ───────────────╮╭──────────── memory 30.00 kB ────────────╮╭────── rx: 0.00 kb/s tx: 0.00 kb/s ──────╮ ╭────────── ports ───────────╮" -"│10.00%│ •• ││100.00 kB│ •• ││ │••••••• │ │ ip private public│" -"│ │ ••• ││ │ ••• ││ │ •• │ │ 8001 │" -"│ │ •• • ││ │ •• • ││0.00 kb/s│ •• │ │127.0.0.1 8003 8003│" -"│ │ • •• ││ │ • •• ││0.00 kb/s│ • │ │ │" -"│ │• • ││ │• • ││ │ • │ │ │" -"╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯ ╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_short_height_logs.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_short_height_logs.snap deleted file mode 100644 index 06bb20f..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_short_height_logs.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"│ ││ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│ line 2 │" -"│▶ line 3 │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭────────────── cpu 03.00% ───────────────╮╭──────────── memory 30.00 kB ────────────╮╭────── rx: 0.00 kb/s tx: 0.00 kb/s ──────╮ ╭────────── ports ───────────╮" -"│10.00%│ •• ││100.00 kB│ •• ││ │••••••• │ │ ip private public│" -"│ │ ••• ││ │ ••• ││ │ •• │ │ 8001 │" -"│ │ •• • ││ │ •• • ││0.00 kb/s│ •• │ │127.0.0.1 8003 8003│" -"│ │ • •• ││ │ • •• ││0.00 kb/s│ • │ │ │" -"│ │• • ││ │• • ││ │ • │ │ │" -"╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯ ╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter.snap deleted file mode 100644 index 15e9f0f..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" 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 ││ restart │" -"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │" -"│ ││ delete │" -"│ ││ │" -"│ ││ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮" -"│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│" -"│ │ ••• • ││ │ ••• • ││ 8001 │" -"│ │•• ••• ││ │•• ••• ││ │" -"│ │ ││ │ ││ │" -"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯" diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter_a.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter_a.snap deleted file mode 100644 index 4e98c14..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter_a.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/1 - filtered ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" Hidden by multi-width symbols: [(2, " ")] -"│ ││ restart │" -"│ ││ stop │" -"│ ││ delete │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮" -"│10.00%│ ••• ││100.00 kB│ •• ││ ip private public│" -"│ │ •• • ││ │ •• • ││ 8001 │" -"│ │ ••• • • ││ │ ••• • • ││ │" -"│ │• •• ││ │• •• ││ │" -"│ │ ││ │ ││ │" -"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯" -" Esc clear ← by → Name Image Status All term: r_1 " diff --git a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter_bar.snap b/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter_bar.snap deleted file mode 100644 index d3a3751..0000000 --- a/src/ui/draw_blocks/snapshots/oxker__ui__draw_blocks__tests__draw_blocks_whole_layout_with_filter_bar.snap +++ /dev/null @@ -1,34 +0,0 @@ ---- -source: src/ui/draw_blocks/mod.rs -expression: setup.terminal.backend() ---- -" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help " -"╭ Containers 1/1 - filtered ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮" -"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │" Hidden by multi-width symbols: [(2, " ")] -"│ ││ restart │" -"│ ││ stop │" -"│ ││ delete │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯" -"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮" -"│ line 1 │" -"│ line 2 │" -"│▶ line 3 │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"│ │" -"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" -"╭────────────── cpu 03.00% ───────────────╮╭──────────── memory 30.00 kB ────────────╮╭────── rx: 0.00 kb/s tx: 0.00 kb/s ──────╮ ╭────────── ports ───────────╮" -"│10.00%│ •• ││100.00 kB│ •• ││ │••••••• │ │ ip private public│" -"│ │ ••• ││ │ ••• ││ │ •• │ │ 8001 │" -"│ │ •• • ││ │ •• • ││0.00 kb/s│ •• │ │ │" -"│ │ • •• ││ │ • •• ││0.00 kb/s│ • │ │ │" -"│ │• • ││ │• • ││ │ • │ │ │" -"╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯╰─────────────────────────────────────────╯ ╰────────────────────────────╯" -" Esc clear ← by → Name Image Status All filter term: r_1 " diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs deleted file mode 100644 index 46ad5f1..0000000 --- a/src/ui/gui_state.rs +++ /dev/null @@ -1,566 +0,0 @@ -use parking_lot::Mutex; -use ratatui::layout::{Constraint, Rect}; -use std::{ - collections::{HashMap, HashSet}, - sync::Arc, - time::Instant, -}; -use tokio::task::JoinHandle; -use uuid::Uuid; - -use crate::{ - app_data::{AppData, ContainerId, Header, ScrollDirection}, - exec::ExecMode, -}; - -use super::Rerender; - -#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] -pub enum SelectablePanel { - #[default] - Containers, - Commands, - Logs, -} - -impl SelectablePanel { - pub const fn title(self) -> &'static str { - match self { - Self::Containers => "Containers", - Self::Logs => "Logs", - Self::Commands => "", - } - } - pub const fn next(self) -> Self { - match self { - Self::Containers => Self::Commands, - Self::Commands => Self::Logs, - Self::Logs => Self::Containers, - } - } - pub const fn prev(self) -> Self { - match self { - Self::Containers => Self::Logs, - Self::Commands => Self::Containers, - Self::Logs => Self::Commands, - } - } -} - -#[derive(Debug, Copy, Clone)] -pub enum Region { - Panel(SelectablePanel), - Header(Header), - HelpPanel, - Delete(DeleteButton), -} - -#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] -pub enum DeleteButton { - Confirm, - Cancel, -} - -#[allow(unused)] -#[derive(Debug, Clone, Copy)] -pub enum BoxLocation { - TopLeft, - TopCentre, - TopRight, - MiddleLeft, - MiddleCentre, - MiddleRight, - BottomLeft, - BottomCentre, - BottomRight, -} - -impl BoxLocation { - /// Screen is divided into 3x3 sections - pub const fn get_indexes(self) -> (usize, usize) { - match self { - Self::TopLeft => (0, 0), - Self::TopCentre => (0, 1), - Self::TopRight => (0, 2), - Self::MiddleLeft => (1, 0), - Self::MiddleCentre => (1, 1), - Self::MiddleRight => (1, 2), - Self::BottomLeft => (2, 0), - Self::BottomCentre => (2, 1), - Self::BottomRight => (2, 2), - } - } - - /// Get both the vertical and hoziztonal constrains - pub const fn get_constraints( - self, - blank_horizontal: u16, - blank_vertical: u16, - text_lines: u16, - text_width: u16, - ) -> ([Constraint; 3], [Constraint; 3]) { - ( - Self::get_horizontal_constraints(self, blank_horizontal, text_width), - Self::get_vertical_constraints(self, blank_vertical, text_lines), - ) - } - - const fn get_horizontal_constraints( - self, - blank_horizontal: u16, - text_width: u16, - ) -> [Constraint; 3] { - match self { - Self::TopLeft | Self::MiddleLeft | Self::BottomLeft => [ - Constraint::Min(text_width), - Constraint::Max(blank_horizontal), - Constraint::Max(blank_horizontal), - ], - Self::TopCentre | Self::MiddleCentre | Self::BottomCentre => [ - Constraint::Max(blank_horizontal), - Constraint::Min(text_width), - Constraint::Max(blank_horizontal), - ], - Self::TopRight | Self::MiddleRight | Self::BottomRight => [ - Constraint::Max(blank_horizontal), - Constraint::Max(blank_horizontal), - Constraint::Min(text_width), - ], - } - } - - const fn get_vertical_constraints( - self, - blank_vertical: u16, - number_lines: u16, - ) -> [Constraint; 3] { - match self { - Self::TopLeft | Self::TopCentre | Self::TopRight => [ - Constraint::Min(number_lines), - Constraint::Max(blank_vertical), - Constraint::Max(blank_vertical), - ], - Self::MiddleLeft | Self::MiddleCentre | Self::MiddleRight => [ - Constraint::Max(blank_vertical), - Constraint::Min(number_lines), - Constraint::Max(blank_vertical), - ], - Self::BottomLeft | Self::BottomCentre | Self::BottomRight => [ - Constraint::Max(blank_vertical), - Constraint::Max(blank_vertical), - Constraint::Min(number_lines), - ], - } - } -} - -// loading animation frames -const FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; -const FRAMES_LEN: u8 = 9; - -/// The application gui state can be in multiple of these four states at the same time -/// Various functions (e.g input handler), operate differently depending upon current Status -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum Status { - DeleteConfirm, - DockerConnect(Option), - Error, - Exec, - Filter, - Help, - Init, - Inspect, - Logs, - SearchLogs, -} - -#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)] -pub struct ScrollOffset { - pub x: usize, - pub y: usize, -} - -/// Global gui_state, stored in an Arc -#[derive(Debug)] -pub struct GuiState { - delete_container_id: Option, - exec_mode: Option, - intersect_delete: HashMap, - intersect_heading: HashMap, - intersect_help: Option, - intersect_panel: HashMap, - loading_handle: Option>, - loading_index: u8, - loading_set: HashSet, - log_height: u16, - rerender: Arc, - selected_panel: SelectablePanel, - screen_width: u16, - show_logs: bool, - inspect_offset: ScrollOffset, - inspect_offset_max: ScrollOffset, - status: HashSet, - pub info_box_text: Option<(String, Instant)>, -} -impl GuiState { - pub fn new(redraw: &Arc, show_logs: bool) -> Self { - Self { - delete_container_id: None, - exec_mode: None, - info_box_text: None, - intersect_delete: HashMap::new(), - intersect_heading: HashMap::new(), - intersect_help: None, - intersect_panel: HashMap::new(), - inspect_offset: ScrollOffset::default(), - inspect_offset_max: ScrollOffset::default(), - loading_handle: None, - loading_index: 0, - loading_set: HashSet::new(), - log_height: 75, - screen_width: 0, - rerender: Arc::clone(redraw), - selected_panel: SelectablePanel::default(), - show_logs, - status: HashSet::new(), - } - } - /// Increase the height of the log panel, then rerender - pub fn log_height_increase(&mut self) { - if self.show_logs && self.log_height <= 75 { - self.log_height = self.log_height.saturating_add(5); - self.rerender.update_draw(); - } - } - - /// Reduce the height of the logs panel, then rerender - /// Unselect logs panel if currently selected - pub fn log_height_decrease(&mut self) { - if self.show_logs { - self.log_height = self.log_height.saturating_sub(5); - if self.log_height == 0 && self.selected_panel == SelectablePanel::Logs { - self.show_logs = false; - self.selected_panel = SelectablePanel::Containers; - } - self.rerender.update_draw(); - } - } - - pub fn set_inspect_offset(&mut self, sd: &ScrollDirection) { - match sd { - ScrollDirection::Up => self.inspect_offset.y = self.inspect_offset.y.saturating_sub(1), - ScrollDirection::Down => { - self.inspect_offset.y = self - .inspect_offset - .y - .saturating_add(1) - .min(self.inspect_offset_max.y) - } - ScrollDirection::Left => { - self.inspect_offset.x = self.inspect_offset.x.saturating_sub(1) - } - ScrollDirection::Right => { - self.inspect_offset.x = self - .inspect_offset - .x - .saturating_add(1) - .min(self.inspect_offset_max.x) - } - } - self.rerender.update_draw(); - } - - pub fn get_inspect_offset(&self) -> ScrollOffset { - self.inspect_offset - } - - pub fn set_inspect_offset_max(&mut self, offset: ScrollOffset) { - self.inspect_offset_max = offset - } - - pub fn set_inspect_offset_y_to_max(&mut self) { - self.inspect_offset.y = self.inspect_offset_max.y; - self.rerender.update_draw(); - } - - pub fn clear_inspect_offset(&mut self) { - self.inspect_offset.x = 0; - self.inspect_offset.y = 0; - self.inspect_offset_max = ScrollOffset::default(); - self.rerender.update_draw(); - } - - /// Set the screen width, used for offset char calculations - pub const fn set_screen_width(&mut self, width: u16) { - self.screen_width = width; - } - - /// Get the screen width, used for offset char calculations - pub const fn get_screen_width(&self) -> u16 { - self.screen_width - } - - pub const fn get_show_logs(&self) -> bool { - self.show_logs - } - - pub fn toggle_show_logs(&mut self) { - self.show_logs = !self.show_logs; - if !self.show_logs && self.selected_panel == SelectablePanel::Logs { - self.selected_panel = SelectablePanel::Containers; - } - self.rerender.update_draw(); - } - - /// Set the log_height to zero, for now only used by tests - #[cfg(test)] - pub const fn log_height_zero(&mut self) { - self.log_height = 0; - } - - /// Get the log height, *should* be a u8 between 0 and 80, essentially a percentage - pub const fn get_log_height(&self) -> u16 { - self.log_height - } - - /// Clear panels hash map, so on resize can fix the sizes for mouse clicks - pub fn clear_area_map(&mut self) { - self.intersect_panel.clear(); - } - - /// Set the rerender clear to true, to flush the screen and redraw - pub fn set_clear(&self) { - self.rerender.set_clear(); - } - - /// Get the currently selected panel - pub const fn get_selected_panel(&self) -> SelectablePanel { - self.selected_panel - } - - /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels - pub fn check_panel_intersect(&mut self, rect: Rect) { - if let Some(data) = self - .intersect_panel - .iter() - .filter(|i| i.1.intersects(rect)) - .collect::>() - .first() - { - self.selected_panel = *data.0; - self.rerender.update_draw(); - } - } - - /// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button - pub fn get_intersect_button(&self, rect: Rect) -> Option { - self.intersect_delete - .iter() - .filter(|i| i.1.intersects(rect)) - .collect::>() - .first() - .map(|data| *data.0) - } - - /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels - pub fn get_intersect_header(&self, rect: Rect) -> Option
{ - self.intersect_heading - .iter() - .filter(|i| i.1.intersects(rect)) - .collect::>() - .first() - .map(|data| *data.0) - } - - /// Check if a the "show/hide help" section has been clicked - pub fn get_intersect_help(&self, rect: Rect) -> bool { - self.intersect_help - .as_ref() - .is_some_and(|i| i.intersects(rect)) - } - - /// Insert, or updates header area panel into heading_map - pub fn update_region_map(&mut self, region: Region, area: Rect) { - match region { - Region::Header(header) => { - self.intersect_heading - .entry(header) - .and_modify(|w| *w = area) - .or_insert(area); - } - Region::Panel(panel) => { - self.intersect_panel - .entry(panel) - .and_modify(|w| *w = area) - .or_insert(area); - } - Region::Delete(button) => { - self.intersect_delete - .entry(button) - .and_modify(|w| *w = area) - .or_insert(area); - } - Region::HelpPanel => { - self.intersect_help = Some(area); - } - } - } - - /// Check if an ContainerId is set in the delete_container field - pub fn get_delete_container(&self) -> Option { - self.delete_container_id.clone() - } - - /// Set either a ContainerId, or None, to the delete_container field - /// If Some, will also insert the DeleteConfirm status into self.status - pub fn set_delete_container(&mut self, id: Option) { - if id.is_some() { - self.status.insert(Status::DeleteConfirm); - } else { - self.intersect_delete.clear(); - self.status_del(Status::DeleteConfirm); - } - self.delete_container_id = id; - self.rerender.update_draw(); - } - - /// Return a copy of the Status HashSet - pub fn get_status(&self) -> HashSet { - self.status.clone() - } - - /// Remove a gui_status into the current gui_status HashSet - /// Remove exec mode & deleteConfirm is required - pub fn status_del(&mut self, status: Status) { - self.status.remove(&status); - match status { - Status::DeleteConfirm => { - self.status.remove(&Status::DeleteConfirm); - } - Status::Exec => { - self.exec_mode = None; - } - _ => (), - } - self.rerender.update_draw(); - } - - /// Inset the ExecMode into self, and set the Status as exec - /// Using StatusPush with Status::Exec won't insert into the hash map - /// To force self.exec_mode to be set - pub fn set_exec_mode(&mut self, mode: ExecMode) { - self.exec_mode = Some(mode); - self.status.insert(Status::Exec); - self.rerender.update_draw(); - } - - pub fn get_exec_mode(&self) -> Option { - self.exec_mode.clone() - } - - /// Insert a gui_status into the current gui_status HashSet - /// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead - pub fn status_push(&mut self, status: Status) { - if status != Status::Exec { - self.status.insert(status); - self.rerender.update_draw(); - } - } - - pub fn set_logs_panel_selected(&mut self, app_data: &Arc>) { - self.selected_panel = SelectablePanel::Logs; - if (app_data.lock().get_container_len() == 0 - && self.get_selected_panel() == SelectablePanel::Commands) - || (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs) - { - self.selected_panel = self.selected_panel.next(); - } - self.rerender.update_draw(); - } - /// Change to next selectable panel - pub fn selectable_panel_next(&mut self, app_data: &Arc>) { - self.selected_panel = self.selected_panel.next(); - if (app_data.lock().get_container_len() == 0 - && self.get_selected_panel() == SelectablePanel::Commands) - || (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs) - { - self.selected_panel = self.selected_panel.next(); - } - self.rerender.update_draw(); - } - - /// Change to previous selectable panel - pub fn selectable_panel_previous(&mut self, app_data: &Arc>) { - self.selected_panel = self.selected_panel.prev(); - if (app_data.lock().get_container_len() == 0 - && self.get_selected_panel() == SelectablePanel::Commands) - || (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs) - { - self.selected_panel = self.selected_panel.prev(); - } - self.rerender.update_draw(); - } - - /// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array - pub fn next_loading(&mut self, uuid: Uuid) { - if self.loading_index == FRAMES_LEN { - self.loading_index = 0; - } else { - self.loading_index += 1; - } - self.loading_set.insert(uuid); - self.rerender.update_draw(); - } - - pub fn is_loading(&self) -> bool { - !self.loading_set.is_empty() - } - /// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' ' - pub fn get_loading(&self) -> char { - if self.is_loading() { - FRAMES[usize::from(self.loading_index)] - } else { - ' ' - } - } - - /// Animate the loading icon in its own Tokio thread - /// This should only be able to executed once, rather than multiple spawns - pub fn start_loading_animation(gui_state: &Arc>, loading_uuid: Uuid) { - if !gui_state.lock().is_loading() { - let inner_state = Arc::clone(gui_state); - gui_state.lock().loading_handle = Some(tokio::spawn(async move { - loop { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - inner_state.lock().next_loading(loading_uuid); - } - })); - } - gui_state.lock().next_loading(loading_uuid); - } - - /// Stop the loading_spin function, and reset gui loading status - pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) { - self.loading_set.remove(&loading_uuid); - self.rerender.update_draw(); - if self.loading_set.is_empty() { - self.loading_index = 0; - if let Some(h) = &self.loading_handle { - h.abort(); - } - self.loading_handle = None; - } - } - - /// Set info box content - pub fn set_info_box(&mut self, text: &str) { - self.info_box_text = Some((text.to_owned(), std::time::Instant::now())); - self.rerender.update_draw(); - } - - /// Remove info box content - pub fn reset_info_box(&mut self) { - self.info_box_text = None; - self.rerender.update_draw(); - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs deleted file mode 100644 index 096191c..0000000 --- a/src/ui/mod.rs +++ /dev/null @@ -1,490 +0,0 @@ -use anyhow::Result; -use crossterm::{ - event::{self, DisableMouseCapture, Event}, - execute, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, -}; -use parking_lot::Mutex; -use ratatui::{ - Frame, Terminal, - backend::CrosstermBackend, - layout::{Constraint, Direction, Layout, Position}, -}; -use std::{ - collections::HashSet, - io::{self, Stdout, Write}, - sync::{Arc, atomic::Ordering}, - time::Duration, -}; -use std::{sync::atomic::AtomicBool, time::Instant}; -use tokio::sync::mpsc::Sender; -use tracing::error; - -mod color_match; -mod draw_blocks; -mod gui_state; -mod redraw; -pub use redraw::Rerender; - -pub use self::color_match::*; -pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; -use crate::{ - app_data::{ - AppData, ChartsData, Columns, ContainerId, ContainerPorts, FilterBy, Header, LogSearch, - SortedOrder, State, - }, - app_error::AppError, - config::{AppColors, Keymap}, - input_handler::InputMessages, -}; - -const POLL_RATE: Duration = std::time::Duration::from_millis(50); - -// could have a render struct, which takes in poll rate, and docker - -pub struct Ui { - app_data: Arc>, - cursor_position: Position, - gui_state: Arc>, - input_tx: Sender, - is_running: Arc, - now: Instant, - rerender: Arc, - terminal: Terminal>, -} - -impl Ui { - /// Enable mouse capture, but don't enable capture of all the mouse movements, doing so will improve performance, and is part of the fix for the weird mouse event output bug - pub fn enable_mouse_capture() -> Result<()> { - Ok(io::stdout().write_all( - concat!( - crossterm::csi!("?1000h"), - crossterm::csi!("?1015h"), - crossterm::csi!("?1006h"), - ) - .as_bytes(), - )?) - } - - /// Create a new Ui struct, and execute the drawing loop - pub async fn start( - app_data: Arc>, - gui_state: Arc>, - input_tx: Sender, - is_running: Arc, - rerender: Arc, - ) { - match Self::setup_terminal() { - Ok(mut terminal) => { - let cursor_position = terminal.get_cursor_position().unwrap_or_default(); - let mut ui = Self { - app_data, - cursor_position, - gui_state, - input_tx, - is_running, - now: Instant::now(), - rerender, - terminal, - }; - if let Err(e) = ui.draw_ui().await { - error!("{e}"); - } - if let Err(e) = ui.reset_terminal() { - error!("{e}"); - } - } - _ => { - error!("Terminal Error"); - } - } - } - - /// Setup the terminal for full-screen drawing mode, with mouse capture - fn setup_terminal() -> Result>> { - let stdout = Self::init_terminal()?; - let backend = CrosstermBackend::new(stdout); - Ok(Terminal::new(backend)?) - } - - fn init_terminal() -> Result { - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen)?; - Self::enable_mouse_capture()?; - Ok(stdout) - } - - /// reset the terminal back to default settings - pub fn reset_terminal(&mut self) -> Result<()> { - self.terminal.clear()?; - - execute!( - self.terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - disable_raw_mode()?; - self.terminal.clear().ok(); - self.terminal.set_cursor_position(self.cursor_position)?; - Ok(self.terminal.show_cursor()?) - } - - /// Draw the the error message ui, for 5 seconds, with a countdown - async fn err_loop(&mut self, host: Option) -> Result<(), AppError> { - let mut seconds = 5; - let colors = self.app_data.lock().config.app_colors; - let keymap = self.app_data.lock().config.keymap.clone(); - let mut redraw = true; - while self.is_running.load(Ordering::SeqCst) { - if self.now.elapsed() >= std::time::Duration::from_secs(1) { - seconds -= 1; - self.now = Instant::now(); - redraw = true; - if seconds < 1 { - break; - } - } - - if redraw - && self - .terminal - .draw(|f| { - draw_blocks::error::draw( - colors, - &AppError::DockerConnect, - f, - host.clone(), - &keymap, - Some(seconds), - ); - }) - .is_err() - { - return Err(AppError::Terminal); - } - if crossterm::event::poll(POLL_RATE).unwrap_or(false) - && let Ok(event) = event::read() - && let Event::Key(key) = event - && key.kind == event::KeyEventKind::Press - { - self.input_tx - .send(InputMessages::ButtonPress((key.code, key.modifiers))) - .await - .ok(); - } - redraw = false; - // std::thread::sleep(POLL_RATE); - } - Ok(()) - } - - /// Check if the user has attempt to clear the screen, and if so clear and redraw - fn check_clear(&mut self) { - if self.rerender.get_clear() { - self.terminal.clear().ok(); - self.rerender.update_draw(); - } - } - /// Use external docker cli to exec into a container - async fn exec(&mut self) { - let exec_mode = self.gui_state.lock().get_exec_mode(); - - if let Some(mode) = exec_mode { - self.reset_terminal().ok(); - self.terminal.clear().ok(); - - if let Err(e) = mode.run(self.terminal.size().ok()).await { - self.app_data - .lock() - .set_error(e, &self.gui_state, Status::Error); - } - } - self.terminal.clear().ok(); - self.reset_terminal().ok(); - Self::init_terminal().ok(); - self.gui_state.lock().status_del(Status::Exec); - } - - /// Use the previously redrawn time, the current time, the docker_interval, and the redraw struct, to calculate - /// if the screen should be redrawn or not - fn should_redraw(&self, previous: &mut Instant, docker_interval_ms: u128) -> bool { - let result = - self.rerender.swap_draw() || previous.elapsed().as_millis() >= docker_interval_ms; - if result { - *previous = std::time::Instant::now(); - } - result - } - - /// The loop for drawing the main UI to the terminal - async fn gui_loop(&mut self) -> Result<(), AppError> { - let colors = self.app_data.lock().config.app_colors; - let keymap = self.app_data.lock().config.keymap.clone(); - let docker_interval_ms = u128::from(self.app_data.lock().config.docker_interval_ms); - let mut drawn_at = std::time::Instant::now(); - - if let Ok(size) = self.terminal.size() { - self.gui_state.lock().set_screen_width(size.width); - } - - while self.is_running.load(Ordering::SeqCst) { - if self.should_redraw(&mut drawn_at, docker_interval_ms) { - let fd = FrameData::from(&*self); - - let exec = fd.status.contains(&Status::Exec); - if exec { - self.exec().await; - } - - if self - .terminal - .draw(|frame| { - draw_frame(&self.app_data, colors, &keymap, frame, &fd, &self.gui_state); - }) - .is_err() - { - return Err(AppError::Terminal); - } - } - - if crossterm::event::poll(POLL_RATE).unwrap_or(false) - && let Ok(event) = event::read() - { - if let Event::Key(key) = event { - if key.kind == event::KeyEventKind::Press { - self.input_tx - .send(InputMessages::ButtonPress((key.code, key.modifiers))) - .await - .ok(); - } - } else if let Event::Mouse(m) = event { - match m.kind { - event::MouseEventKind::Down(_) - | event::MouseEventKind::ScrollDown - | event::MouseEventKind::ScrollUp => { - self.input_tx - .send(InputMessages::MouseEvent((m, m.modifiers))) - .await - .ok(); - } - _ => (), - } - } else if let Event::Resize(width, _) = event { - self.gui_state.lock().clear_area_map(); - self.terminal.autoresize().ok(); - self.gui_state.lock().set_screen_width(width); - } - } - self.check_clear(); - } - Ok(()) - } - - /// Draw either the Error, or main oxker ui, to the terminal - async fn draw_ui(&mut self) -> Result<(), AppError> { - let status = self.gui_state.lock().get_status(); - if let Some(Status::DockerConnect(msg)) = status - .iter() - .find(|s| matches!(s, Status::DockerConnect(_))) - { - self.err_loop(msg.clone()).await?; - } else { - self.gui_loop().await?; - } - Ok(()) - } -} - -/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here -/// TODO refactor this -#[derive(Debug, Clone)] -#[allow(clippy::struct_excessive_bools)] -pub struct FrameData { - chart_data: Option, - color_logs: bool, - columns: Columns, - container_title: String, - log_search: Option, - delete_confirm: Option, - filter_by: FilterBy, - filter_term: Option, - has_containers: bool, - log_height: u16, - show_logs: bool, - has_error: Option, - info_text: Option<(String, Instant)>, - is_loading: bool, - loading_icon: String, - log_title: String, - port_max_lens: (usize, usize, usize), - ports: Option<(Vec, State)>, - selected_panel: SelectablePanel, - scroll_title: Option, - sorted_by: Option<(Header, SortedOrder)>, - status: HashSet, -} - -impl From<&Ui> for FrameData { - fn from(ui: &Ui) -> Self { - let (mut app_data, gui_data) = (ui.app_data.lock(), ui.gui_state.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, - log_search: app_data.gen_log_search(), - has_error: app_data.get_error(), - info_text: gui_data.info_box_text.clone(), - is_loading: gui_data.is_loading(), - show_logs: gui_data.get_show_logs(), - loading_icon: gui_data.get_loading().to_string(), - log_height: gui_data.get_log_height(), - log_title: app_data.get_log_title(), - port_max_lens: app_data.get_longest_port(), - ports: app_data.get_selected_ports(), - scroll_title: app_data.get_scroll_title(gui_data.get_screen_width()), - selected_panel: gui_data.get_selected_panel(), - sorted_by: app_data.get_sorted(), - status: gui_data.get_status(), - } - } -} - -/// Draw the main ui to a frame of the terminal -fn draw_frame( - app_data: &Arc>, - colors: AppColors, - keymap: &Keymap, - f: &mut Frame, - fd: &FrameData, - gui_state: &Arc>, -) { - let contains_filter = fd.status.contains(&Status::Filter); - let contains_search_logs = fd.status.contains(&Status::SearchLogs); - - let contains_inspect = fd.status.contains(&Status::Inspect); - - let inspect_data = app_data.lock().get_inspect_data(); - if contains_inspect && let Some(inspect_data) = inspect_data { - draw_blocks::inspect::draw(f, colors, inspect_data, gui_state, keymap); - } else { - let whole_layout = Layout::default() - .direction(Direction::Vertical) - .constraints(if contains_filter || contains_search_logs { - vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)] - } else { - vec![Constraint::Max(1), Constraint::Min(1)] - }) - .split(f.area()); - - draw_blocks::headers::draw(whole_layout[0], colors, f, fd, gui_state, keymap); - - if let Some(rect) = whole_layout.get(2) { - if contains_filter { - draw_blocks::filter::draw(*rect, colors, f, fd); - } else { - draw_blocks::search_logs::draw(*rect, colors, f, fd, keymap); - } - } - - let upper_main = Layout::default() - .direction(Direction::Vertical) - .constraints(if fd.has_containers { - vec![Constraint::Percentage(75), Constraint::Percentage(25)] - } else { - vec![Constraint::Percentage(100), Constraint::Percentage(0)] - }) - .split(whole_layout[1]); - - let containers_logs_section = Layout::default() - .direction(Direction::Vertical) - .constraints(if fd.show_logs { - vec![Constraint::Min(6), Constraint::Percentage(fd.log_height)] - } else { - vec![Constraint::Percentage(100)] - }) - .split(upper_main[0]); - - // Containers + docker commands - let containers_commands = Layout::default() - .direction(Direction::Horizontal) - .constraints(if fd.has_containers { - vec![Constraint::Percentage(90), Constraint::Percentage(10)] - } else { - vec![Constraint::Percentage(100)] - }) - .split(containers_logs_section[0]); - - draw_blocks::containers::draw(app_data, containers_commands[0], colors, f, fd, gui_state); - - if fd.show_logs { - draw_blocks::logs::draw( - app_data, - containers_logs_section[1], - colors, - f, - fd, - gui_state, - ); - } - - if let Some(id) = fd.delete_confirm.as_ref() { - app_data.lock().get_container_name_by_id(id).map_or_else( - || { - // If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation - // so if in that unique situation, just clear the delete_container id - gui_state.lock().set_delete_container(None); - }, - |name| { - draw_blocks::delete_confirm::draw(colors, f, gui_state, keymap, name); - }, - ); - } - - // only draw commands + charts if there are containers - if let Some(rect) = containers_commands.get(1) { - draw_blocks::commands::draw(app_data, *rect, colors, f, fd, gui_state); - - // Can calculate the max string length here, and then use that to keep the ports section as small as possible (+4 for some padding + border) - let ports_len = - u16::try_from(fd.port_max_lens.0 + fd.port_max_lens.1 + fd.port_max_lens.2 + 2) - .unwrap_or(26); - - let lower = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Min(1), Constraint::Max(ports_len)]) - .split(upper_main[1]); - - let charts_rect = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(66), Constraint::Percentage(33)]) - .split(lower[0]); - - draw_blocks::chart_cpu_mem::draw(charts_rect[0], colors, f, fd); - draw_blocks::chart_bandwidth::draw(charts_rect[1], colors, f, fd); - - draw_blocks::ports::draw(lower[1], colors, f, fd); - } - - // Check if error, and show popup if so - if fd.status.contains(&Status::Help) { - let config = app_data.lock().config.clone(); - draw_blocks::help::draw(&config, f); - } - } - - if let Some((text, instant)) = fd.info_text.as_ref() { - draw_blocks::info::draw(colors, f, gui_state, instant, text.to_owned()); - } - - if let Some(error) = fd.has_error.as_ref() { - draw_blocks::error::draw(colors, error, f, None, keymap, None); - } -} diff --git a/src/ui/redraw.rs b/src/ui/redraw.rs deleted file mode 100644 index 7593643..0000000 --- a/src/ui/redraw.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - -#[derive(Debug)] -pub struct Rerender { - draw: AtomicBool, - clear: AtomicBool, -} - -impl Rerender { - pub const fn new() -> Self { - Self { - draw: AtomicBool::new(true), - clear: AtomicBool::new(false), - } - } - - pub fn update_draw(&self) { - self.draw.store(true, Ordering::SeqCst); - } - - pub fn get_clear(&self) -> bool { - self.clear.swap(false, Ordering::SeqCst) - } - - pub fn set_clear(&self) { - self.clear.store(true, Ordering::SeqCst); - } - - /// Return the value of the draw, and set to false - pub fn swap_draw(&self) -> bool { - match self - .draw - .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst) - { - Ok(previous_value) => previous_value, - Err(current_value) => current_value, - } - } -}