feat: config file, cloes #47
Enable use of a config file, with custom keymap and custom colours
This commit is contained in:
Generated
+67
@@ -1067,8 +1067,12 @@ dependencies = [
|
||||
"futures-util",
|
||||
"parking_lot",
|
||||
"ratatui",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_jsonc",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
@@ -1295,6 +1299,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[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.19"
|
||||
@@ -1306,6 +1321,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -1580,6 +1604,40 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
|
||||
dependencies = [
|
||||
"indexmap 2.7.1",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
@@ -1930,6 +1988,15 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.33.0"
|
||||
|
||||
@@ -35,8 +35,12 @@ directories = "6.0"
|
||||
futures-util = "0.3"
|
||||
parking_lot = { version = "0.12" }
|
||||
ratatui = "0.29"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_jsonc = "1.0"
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
tokio-util = "0.7"
|
||||
toml = { version = "0.8", default-features = false, features = ["parse"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
uuid = { version = "1.12", features = ["fast-rng", "v4"] }
|
||||
|
||||
@@ -186,11 +186,11 @@ docker compose -f ./docker/docker-compose.yml up -d
|
||||
or individually
|
||||
|
||||
```shell
|
||||
docker run --name redis -d redis:alpine3.19
|
||||
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.19
|
||||
docker run --name postgres -e POSTGRES_PASSWORD=never_use_this_password_in_production -d postgres:alpine3.21
|
||||
```
|
||||
|
||||
```shell
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"color_logs": false,
|
||||
"docker_interval": 1000,
|
||||
"gui": true,
|
||||
"host": "/var/run/docker.sock",
|
||||
"raw_logs": false,
|
||||
"show_self": false,
|
||||
"show_std_err": false,
|
||||
"show_timestamp": true,
|
||||
"use_cli": false,
|
||||
"colors": {
|
||||
"borders": {
|
||||
"selected": "lightcyan",
|
||||
"unselected": "grey"
|
||||
},
|
||||
"chart_cpu": {
|
||||
"background": "reset",
|
||||
"border": "white",
|
||||
"max": "#FFB224",
|
||||
"points": "magenta",
|
||||
"title": "green",
|
||||
"y_axis": "white"
|
||||
},
|
||||
"chart_memory": {
|
||||
"background": "reset",
|
||||
"border": "white",
|
||||
"max": "#FFB224",
|
||||
"points": "cyan",
|
||||
"title": "green",
|
||||
"y_axis": "white"
|
||||
},
|
||||
"chart_ports": {
|
||||
"background": "reset",
|
||||
"border": "white",
|
||||
"headings": "yellow",
|
||||
"text": "white",
|
||||
"title": "green"
|
||||
},
|
||||
"commands": {
|
||||
"background": "reset",
|
||||
"delete": "gray",
|
||||
"pause": "yellow",
|
||||
"restart": "magenta",
|
||||
"resume": "blue",
|
||||
"start": "green",
|
||||
"stop": "red"
|
||||
},
|
||||
"container_state": {
|
||||
"dead": "red",
|
||||
"exited": "red",
|
||||
"paused": "yellow",
|
||||
"removing": "lightred",
|
||||
"restarting": "lightgreen",
|
||||
"running_healthy": "green",
|
||||
"running_unhealthy": "#FFB224",
|
||||
"unknown": "red"
|
||||
},
|
||||
"containers": {
|
||||
"background": "reset",
|
||||
"icon": "white",
|
||||
"text": "blue",
|
||||
"text_rx": "#FFE9C1",
|
||||
"text_tx": "#CD8C8C"
|
||||
},
|
||||
"headers_bar": {
|
||||
"background": "magenta",
|
||||
"loading_spinner": "white",
|
||||
"text": "black",
|
||||
"text_selected": "gray"
|
||||
},
|
||||
"popup_delete": {
|
||||
"background": "white",
|
||||
"text": "black",
|
||||
"text_highlight": "red"
|
||||
},
|
||||
"popup_error": {
|
||||
"background": "red",
|
||||
"text": "white"
|
||||
},
|
||||
"popup_help": {
|
||||
"background": "magenta",
|
||||
"text": "black",
|
||||
"text_highlight": "white"
|
||||
},
|
||||
"popup_info": {
|
||||
"background": "blue",
|
||||
"text": "white"
|
||||
}
|
||||
},
|
||||
"keymap": {
|
||||
"clear": [
|
||||
"c",
|
||||
"esc"
|
||||
],
|
||||
"delete_confirm": [
|
||||
"y"
|
||||
],
|
||||
"delete_deny": [
|
||||
"n"
|
||||
],
|
||||
"exec": [
|
||||
"e"
|
||||
],
|
||||
"filter_mode": [
|
||||
"/",
|
||||
"F1"
|
||||
],
|
||||
"quit": [
|
||||
"q"
|
||||
],
|
||||
"save_logs": [
|
||||
"s"
|
||||
],
|
||||
"scroll_down_many": [
|
||||
"pagedown"
|
||||
],
|
||||
"scroll_down_one": [
|
||||
"down",
|
||||
"j"
|
||||
],
|
||||
"scroll_end": [
|
||||
"end"
|
||||
],
|
||||
"scroll_start": [
|
||||
"home"
|
||||
],
|
||||
"scroll_up_many": [
|
||||
"pageup"
|
||||
],
|
||||
"scroll_up_one": [
|
||||
"up",
|
||||
"k"
|
||||
],
|
||||
"select_next_panel": [
|
||||
"tab"
|
||||
],
|
||||
"select_previous_panel": [
|
||||
"backtab"
|
||||
],
|
||||
"sort_by_cpu": [
|
||||
"4"
|
||||
],
|
||||
"sort_by_id": [
|
||||
"6"
|
||||
],
|
||||
"sort_by_image": [
|
||||
"7"
|
||||
],
|
||||
"sort_by_memory": [
|
||||
"5"
|
||||
],
|
||||
"sort_by_name": [
|
||||
"1"
|
||||
],
|
||||
"sort_by_rx": [
|
||||
"8"
|
||||
],
|
||||
"sort_by_state": [
|
||||
"2"
|
||||
],
|
||||
"sort_by_status": [
|
||||
"3"
|
||||
],
|
||||
"sort_by_tx": [
|
||||
"9"
|
||||
],
|
||||
"sort_reset": [
|
||||
"0"
|
||||
],
|
||||
"toggle_help": [
|
||||
"h"
|
||||
],
|
||||
"toggle_mouse_capture": [
|
||||
"m"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
{
|
||||
// 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 .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": false,
|
||||
// Show a timestamp for every log entry
|
||||
"show_timestamp": true,
|
||||
// Don't draw gui - for debugging - mostly pointless
|
||||
"gui": true,
|
||||
// Docker host location
|
||||
"host": "/var/run/docker.sock",
|
||||
// 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,
|
||||
//////////////////
|
||||
// Custom Keymap //
|
||||
//////////////////
|
||||
// Available keys are;
|
||||
// 1) a-z and A-Z
|
||||
// 2) 0-9
|
||||
// 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
|
||||
// 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"
|
||||
],
|
||||
// Quit at anytime
|
||||
"quit": [
|
||||
"q"
|
||||
],
|
||||
// Save logs of selected container to file on disk
|
||||
"save_logs": [
|
||||
"s"
|
||||
],
|
||||
// Scroll down a list by many
|
||||
"scroll_down_many": [
|
||||
"pagedown"
|
||||
],
|
||||
// Scroll down a list by one item
|
||||
"scroll_down_one": [
|
||||
"down",
|
||||
"j"
|
||||
],
|
||||
// Scroll down to the end of a list
|
||||
"scroll_end": [
|
||||
"end"
|
||||
],
|
||||
// Scroll up to the start of a list
|
||||
"scroll_start": [
|
||||
"home"
|
||||
],
|
||||
// Scroll up a list by many
|
||||
"scroll_up_many": [
|
||||
"pageup"
|
||||
],
|
||||
// Scroll up a list by one item
|
||||
"scroll_up_one": [
|
||||
"up",
|
||||
"k"
|
||||
],
|
||||
// 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"
|
||||
]
|
||||
},
|
||||
////////////////////
|
||||
// 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 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
# Example toml config file
|
||||
|
||||
# This needs to be renamed to "config.toml" in order for oxker to automatically load
|
||||
# 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 = false
|
||||
|
||||
# Show a timestamp for every log entry
|
||||
show_timestamp = true
|
||||
|
||||
# Don't draw gui - for debugging - mostly pointless
|
||||
gui = true
|
||||
|
||||
# Docker host location
|
||||
host = "/var/run/docker.sock"
|
||||
|
||||
# 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
|
||||
|
||||
#################
|
||||
# Custom Keymap #
|
||||
#################
|
||||
|
||||
# Available keys are;
|
||||
# 1) a-z and A-Z
|
||||
# 2) 0-9
|
||||
# 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
|
||||
# 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"]
|
||||
# Quit at anytime
|
||||
quit = ["q"]
|
||||
# Save logs of selected container to file on disk
|
||||
save_logs=["s"]
|
||||
# scroll down a list by many
|
||||
scroll_down_many=["pagedown"]
|
||||
# scroll down a list by one item
|
||||
scroll_down_one=["down", "j"]
|
||||
# scroll down to the end of a list
|
||||
scroll_end=["end"]
|
||||
# scroll up to the start of a list
|
||||
scroll_start=["home"]
|
||||
# scroll up a list by many
|
||||
scroll_up_many=["pageup"]
|
||||
# scroll up a list by one item
|
||||
scroll_up_one=["up", "k"]
|
||||
# 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"]
|
||||
|
||||
|
||||
#################
|
||||
# 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"
|
||||
|
||||
# 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 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 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"
|
||||
@@ -11,7 +11,7 @@ use ratatui::{
|
||||
widgets::{ListItem, ListState},
|
||||
};
|
||||
|
||||
use crate::ui::ORANGE;
|
||||
use crate::config::AppColors;
|
||||
|
||||
use super::Header;
|
||||
|
||||
@@ -265,16 +265,28 @@ impl State {
|
||||
pub const fn is_alive(self) -> bool {
|
||||
matches!(self, Self::Running(_))
|
||||
}
|
||||
/// Color of the state for the containers section
|
||||
/// TODO allow usable editable colours
|
||||
pub const fn get_color(self) -> Color {
|
||||
|
||||
/// Check if state is running & healthy
|
||||
pub const fn is_healthy(self) -> bool {
|
||||
match self {
|
||||
Self::Paused => Color::Yellow,
|
||||
Self::Removing => Color::LightRed,
|
||||
Self::Restarting => Color::LightGreen,
|
||||
Self::Running(RunningState::Healthy) => Color::Green,
|
||||
Self::Running(RunningState::Unhealthy) => ORANGE,
|
||||
_ => Color::Red,
|
||||
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
|
||||
@@ -348,14 +360,14 @@ pub enum DockerCommand {
|
||||
}
|
||||
|
||||
impl DockerCommand {
|
||||
pub const fn get_color(self) -> Color {
|
||||
pub const fn get_color(self, colors: AppColors) -> Color {
|
||||
match self {
|
||||
Self::Pause => Color::Yellow,
|
||||
Self::Restart => Color::Magenta,
|
||||
Self::Start => Color::Green,
|
||||
Self::Stop => Color::Red,
|
||||
Self::Delete => Color::Gray,
|
||||
Self::Resume => Color::Blue,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-11
@@ -12,7 +12,7 @@ mod container_state;
|
||||
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
parse_args::CliArgs,
|
||||
config::Config,
|
||||
ui::{log_sanitizer, GuiState, Status},
|
||||
ENTRY_POINT,
|
||||
};
|
||||
@@ -123,13 +123,13 @@ pub struct AppData {
|
||||
filter: Filter,
|
||||
hidden_containers: Vec<ContainerItem>,
|
||||
sorted_by: Option<(Header, SortedOrder)>,
|
||||
pub args: CliArgs,
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(test)]
|
||||
pub struct AppData {
|
||||
pub args: CliArgs,
|
||||
pub config: Config,
|
||||
pub containers: StatefulList<ContainerItem>,
|
||||
pub error: Option<AppError>,
|
||||
pub filter: Filter,
|
||||
@@ -139,9 +139,9 @@ pub struct AppData {
|
||||
|
||||
impl AppData {
|
||||
/// Generate a default app_state
|
||||
pub fn default(args: CliArgs) -> Self {
|
||||
pub fn default(config: Config) -> Self {
|
||||
Self {
|
||||
args,
|
||||
config,
|
||||
containers: StatefulList::new(vec![]),
|
||||
error: None,
|
||||
filter: Filter::new(),
|
||||
@@ -657,8 +657,8 @@ impl AppData {
|
||||
|
||||
/// Error related methods
|
||||
/// Get single app_state error
|
||||
pub const fn get_error(&self) -> Option<AppError> {
|
||||
self.error
|
||||
pub fn get_error(&self) -> Option<AppError> {
|
||||
self.error.clone()
|
||||
}
|
||||
|
||||
/// Remove single app_state error
|
||||
@@ -682,7 +682,7 @@ impl AppData {
|
||||
/// 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.args.in_container)
|
||||
.is_some_and(|i| i.is_oxker && self.config.in_container)
|
||||
}
|
||||
|
||||
/// Find the widths for the strings in the containers panel.
|
||||
@@ -877,10 +877,10 @@ impl AppData {
|
||||
|
||||
/// Update logs of a given container, based on id
|
||||
pub fn update_log_by_id(&mut self, logs: Vec<String>, id: &ContainerId) {
|
||||
let color = self.args.color;
|
||||
let raw = self.args.raw;
|
||||
let color = self.config.color_logs;
|
||||
let raw = self.config.raw_logs;
|
||||
|
||||
let timestamp = self.args.timestamp;
|
||||
let timestamp = self.config.show_timestamp;
|
||||
|
||||
if let Some(container) = self.get_any_container_by_id(id) {
|
||||
if !container.is_oxker {
|
||||
|
||||
+5
-1
@@ -2,13 +2,15 @@ use crate::app_data::DockerCommand;
|
||||
use std::fmt;
|
||||
|
||||
/// app errors to set in global state
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AppError {
|
||||
DockerCommand(DockerCommand),
|
||||
DockerExec,
|
||||
DockerLogs,
|
||||
DockerConnect,
|
||||
IO(String),
|
||||
MouseCapture(bool),
|
||||
Parse(String),
|
||||
Terminal,
|
||||
}
|
||||
|
||||
@@ -20,10 +22,12 @@ impl fmt::Display for AppError {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
use ratatui::style::Color;
|
||||
|
||||
/// The macro accepts a list of struct names with key names
|
||||
/// Returns a struct where every key name is an Option<String>, with the correct derived attributes
|
||||
macro_rules! optional_config_struct {
|
||||
($($struct_name:ident, $($key_name:ident),*);*) => {
|
||||
$(
|
||||
#[derive(Debug, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
struct $struct_name {
|
||||
$(
|
||||
$key_name: Option<String>,
|
||||
)*
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
/// The macro accepts a list of struct names with key names
|
||||
macro_rules! config_struct {
|
||||
($($struct_name:ident, $($key_name:ident),*);*) => {
|
||||
$(
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub struct $struct_name {
|
||||
$(
|
||||
pub $key_name: Color,
|
||||
)*
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl AppColors {
|
||||
fn map_color(color_str: Option<&str>, setter: &mut Color) {
|
||||
color_str.map(|i| i.parse::<Color>().map(|i| *setter = i));
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<ConfigColors>> for AppColors {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn from(value: Option<ConfigColors>) -> Self {
|
||||
let mut app_colors = Self::new();
|
||||
|
||||
if let Some(config_colors) = value {
|
||||
// Heading bar
|
||||
if let Some(hb) = config_colors.headers_bar {
|
||||
Self::map_color(
|
||||
hb.background.as_deref(),
|
||||
&mut app_colors.headers_bar.background,
|
||||
);
|
||||
Self::map_color(
|
||||
hb.loading_spinner.as_deref(),
|
||||
&mut app_colors.headers_bar.loading_spinner,
|
||||
);
|
||||
Self::map_color(hb.text.as_deref(), &mut app_colors.headers_bar.text);
|
||||
Self::map_color(
|
||||
hb.text_selected.as_deref(),
|
||||
&mut app_colors.headers_bar.text_selected,
|
||||
);
|
||||
}
|
||||
|
||||
// Seletable 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
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;
|
||||
ConfigHeadersBar, background, loading_spinner, text, text_selected
|
||||
);
|
||||
|
||||
config_struct!(
|
||||
Borders, selected, unselected;
|
||||
ChartCpu, background, border, title, max, points, y_axis;
|
||||
ChartMemory, background, border, title, max, points, 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;
|
||||
HeadersBar, background, text_selected, loading_spinner, text;
|
||||
PopupDelete, background, text, text_highlight;
|
||||
PopupError, background, text;
|
||||
PopupHelp, background, text, text_highlight;
|
||||
PopupInfo, background, text
|
||||
);
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct ConfigColors {
|
||||
borders: Option<ConfigBorders>,
|
||||
chart_cpu: Option<ConfigChartCpu>,
|
||||
chart_memory: Option<ConfigChartMemory>,
|
||||
chart_ports: Option<ConfigChartPorts>,
|
||||
commands: Option<ConfigCommands>,
|
||||
container_state: Option<ConfigContainerState>,
|
||||
containers: Option<ConfigContainers>,
|
||||
headers_bar: Option<ConfigHeadersBar>,
|
||||
popup_delete: Option<ConfigBackgroundTextHighlight>,
|
||||
popup_error: Option<ConfigBackgroundText>,
|
||||
popup_help: Option<ConfigBackgroundTextHighlight>,
|
||||
popup_info: Option<ConfigBackgroundText>,
|
||||
}
|
||||
|
||||
/// Default colours for the header bar
|
||||
impl HeadersBar {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Magenta,
|
||||
loading_spinner: Color::White,
|
||||
text: Color::Black,
|
||||
text_selected: Color::Gray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the borders
|
||||
impl Borders {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
selected: Color::LightCyan,
|
||||
unselected: Color::Gray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the delete popup
|
||||
impl Commands {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
background: Color::Reset,
|
||||
pause: Color::Yellow,
|
||||
restart: Color::Magenta,
|
||||
stop: Color::Red,
|
||||
delete: Color::Gray,
|
||||
resume: Color::Blue,
|
||||
start: Color::Green,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default colours for the help popup
|
||||
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::Rgb(255, 233, 193),
|
||||
text_tx: Color::Rgb(205, 140, 140),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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_ports: ChartPorts,
|
||||
pub commands: Commands,
|
||||
pub container_state: ContainerState,
|
||||
pub containers: Containers,
|
||||
pub headers_bar: HeadersBar,
|
||||
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_ports: ChartPorts::new(),
|
||||
commands: Commands::new(),
|
||||
container_state: ContainerState::new(),
|
||||
containers: Containers::new(),
|
||||
headers_bar: HeadersBar::new(),
|
||||
popup_delete: PopupDelete::new(),
|
||||
popup_error: PopupError::new(),
|
||||
popup_help: PopupHelp::new(),
|
||||
popup_info: PopupInfo::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
# Example toml config file
|
||||
|
||||
# This needs to be renamed to "config.toml" in order for oxker to automatically load
|
||||
# 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 = false
|
||||
|
||||
# Show a timestamp for every log entry
|
||||
show_timestamp = true
|
||||
|
||||
# Don't draw gui - for debugging - mostly pointless
|
||||
gui = true
|
||||
|
||||
# Docker host location
|
||||
host = "/var/run/docker.sock"
|
||||
|
||||
# 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
|
||||
|
||||
#################
|
||||
# Custom Keymap #
|
||||
#################
|
||||
|
||||
# Available keys are;
|
||||
# 1) a-z and A-Z
|
||||
# 2) 0-9
|
||||
# 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
|
||||
# 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"]
|
||||
# Quit at anytime
|
||||
quit = ["q"]
|
||||
# Save logs of selected container to file on disk
|
||||
save_logs=["s"]
|
||||
# scroll down a list by many
|
||||
scroll_down_many=["pagedown"]
|
||||
# scroll down a list by one item
|
||||
scroll_down_one=["down", "j"]
|
||||
# scroll down to the end of a list
|
||||
scroll_end=["end"]
|
||||
# scroll up to the start of a list
|
||||
scroll_start=["home"]
|
||||
# scroll up a list by many
|
||||
scroll_up_many=["pageup"]
|
||||
# scroll up a list by one item
|
||||
scroll_up_one=["up", "k"]
|
||||
# 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"]
|
||||
|
||||
|
||||
#################
|
||||
# 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"
|
||||
|
||||
# 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 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 ports chart
|
||||
[colors.chart_ports]
|
||||
# Background color of panel
|
||||
background = "reset"
|
||||
# Border color
|
||||
border="white"
|
||||
# Chart title - only whilst container is running, paused & stopped will use colors.container_state
|
||||
title="green"
|
||||
# Private/Public/IP headings
|
||||
headings="yellow"
|
||||
# Ports & IP listing text
|
||||
text="white"
|
||||
|
||||
# The help popup
|
||||
[colors.popup_help]
|
||||
# Background color
|
||||
background="magenta"
|
||||
# Text color
|
||||
text="black"
|
||||
# Highlighted text color
|
||||
text_highlight="white"
|
||||
|
||||
# The info popup - used to display small messages - such as saving logs to disk, or change of mouse capture settings
|
||||
[colors.popup_info]
|
||||
# Background color
|
||||
background="blue"
|
||||
# Text color
|
||||
text="white"
|
||||
|
||||
# The delete popup - used to display a confirmation warning when about to delete a container
|
||||
[colors.popup_delete]
|
||||
# Background color
|
||||
background="white"
|
||||
# Text color
|
||||
text="black"
|
||||
# Highlighted text color
|
||||
text_highlight="red"
|
||||
|
||||
# The error popup - hopefully you'll never have to see this
|
||||
[colors.popup_error]
|
||||
# Background color
|
||||
background="red"
|
||||
# Text color
|
||||
text="white"
|
||||
@@ -0,0 +1,437 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
/// The macro accepts a list of struct names with key names
|
||||
/// Returns a struct where every key name is an Option<String>, with the correct derived attributes
|
||||
macro_rules! optional_config_struct {
|
||||
($($struct_name:ident, $($key_name:ident),*);*) => {
|
||||
$(
|
||||
#[derive(Debug, serde::Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct $struct_name {
|
||||
$(
|
||||
$key_name: Option<Vec<String>>,
|
||||
)*
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
/// The macro accepts a list of struct names with key names
|
||||
/// Similar to the optional_config_struct macro as above, but returns struct where every key name is Color
|
||||
macro_rules! config_struct {
|
||||
($($struct_name:ident, $($key_name:ident),*);*) => {
|
||||
$(
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct $struct_name {
|
||||
$(
|
||||
pub $key_name: (KeyCode, Option<KeyCode>),
|
||||
)*
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
optional_config_struct!(
|
||||
ConfigKeymap,
|
||||
clear,
|
||||
delete_deny,
|
||||
delete_confirm,
|
||||
exec,
|
||||
filter_mode,
|
||||
quit,
|
||||
save_logs,
|
||||
scroll_down_many,
|
||||
scroll_down_one,
|
||||
scroll_end,
|
||||
scroll_start,
|
||||
scroll_up_many,
|
||||
scroll_up_one,
|
||||
select_next_panel,
|
||||
select_previous_panel,
|
||||
sort_by_name,
|
||||
sort_by_state,
|
||||
sort_by_status,
|
||||
sort_by_cpu,
|
||||
sort_by_memory,
|
||||
sort_by_id,
|
||||
sort_by_image,
|
||||
sort_by_rx,
|
||||
sort_by_tx,
|
||||
sort_reset,
|
||||
toggle_help,
|
||||
toggle_mouse_capture
|
||||
);
|
||||
|
||||
config_struct!(
|
||||
Keymap,
|
||||
clear,
|
||||
delete_deny,
|
||||
delete_confirm,
|
||||
exec,
|
||||
filter_mode,
|
||||
quit,
|
||||
save_logs,
|
||||
scroll_down_many,
|
||||
scroll_down_one,
|
||||
scroll_end,
|
||||
scroll_start,
|
||||
scroll_up_many,
|
||||
scroll_up_one,
|
||||
select_next_panel,
|
||||
select_previous_panel,
|
||||
sort_by_name,
|
||||
sort_by_state,
|
||||
sort_by_status,
|
||||
sort_by_cpu,
|
||||
sort_by_memory,
|
||||
sort_by_id,
|
||||
sort_by_image,
|
||||
sort_by_rx,
|
||||
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_deny: (KeyCode::Char('n'), None),
|
||||
delete_confirm: (KeyCode::Char('y'), None),
|
||||
exec: (KeyCode::Char('e'), None),
|
||||
filter_mode: (KeyCode::Char('/'), Some(KeyCode::F(1))),
|
||||
quit: (KeyCode::Char('q'), None),
|
||||
save_logs: (KeyCode::Char('s'), None),
|
||||
scroll_down_many: (KeyCode::PageDown, None),
|
||||
scroll_down_one: (KeyCode::Down, Some(KeyCode::Char('j'))),
|
||||
scroll_end: (KeyCode::End, None),
|
||||
scroll_start: (KeyCode::Home, None),
|
||||
scroll_up_many: (KeyCode::PageUp, None),
|
||||
scroll_up_one: (KeyCode::Up, Some(KeyCode::Char('k'))),
|
||||
select_next_panel: (KeyCode::Tab, None),
|
||||
select_previous_panel: (KeyCode::BackTab, None),
|
||||
sort_by_name: (KeyCode::Char('1'), None),
|
||||
sort_by_state: (KeyCode::Char('2'), None),
|
||||
sort_by_status: (KeyCode::Char('3'), None),
|
||||
sort_by_cpu: (KeyCode::Char('4'), None),
|
||||
sort_by_memory: (KeyCode::Char('5'), None),
|
||||
sort_by_id: (KeyCode::Char('6'), None),
|
||||
sort_by_image: (KeyCode::Char('7'), None),
|
||||
sort_by_rx: (KeyCode::Char('8'), None),
|
||||
sort_by_tx: (KeyCode::Char('9'), None),
|
||||
sort_reset: (KeyCode::Char('0'), None),
|
||||
toggle_help: (KeyCode::Char('h'), None),
|
||||
toggle_mouse_capture: (KeyCode::Char('m'), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<ConfigKeymap>> for Keymap {
|
||||
/// Probably a better way to do this, but for now it works
|
||||
fn from(value: Option<ConfigKeymap>) -> Self {
|
||||
let mut keymap = Self::new();
|
||||
|
||||
let mut clash = HashSet::new();
|
||||
let mut counter = 0;
|
||||
|
||||
let mut update_keymap =
|
||||
|vec_str: Option<Vec<String>>,
|
||||
keymap_field: &mut (KeyCode, Option<KeyCode>),
|
||||
keymap_clash: &mut HashSet<KeyCode>| {
|
||||
if let Some(vec_str) = vec_str {
|
||||
if 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.exec, &mut keymap.exec, &mut clash);
|
||||
update_keymap(ck.filter_mode, &mut keymap.filter_mode, &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_many,
|
||||
&mut keymap.scroll_down_many,
|
||||
&mut clash,
|
||||
);
|
||||
update_keymap(ck.scroll_down_one, &mut keymap.scroll_down_one, &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_many, &mut keymap.scroll_up_many, &mut clash);
|
||||
update_keymap(ck.scroll_up_one, &mut keymap.scroll_up_one, &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,
|
||||
);
|
||||
}
|
||||
// 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 {
|
||||
/// Try to parse a &[String] into a Vec of keycodes, at most the output will have 2 entries
|
||||
fn try_parse_keycode(input: &[String]) -> Option<Vec<KeyCode>> {
|
||||
let mut output = vec![];
|
||||
|
||||
for key in input.iter().take(2) {
|
||||
if key.chars().count() == 1 {
|
||||
if let Some(first_char) = key.chars().next() {
|
||||
if 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)),
|
||||
"backspace" => Some(KeyCode::Backspace),
|
||||
"backtab" => Some(KeyCode::BackTab),
|
||||
"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 deinition
|
||||
if output.first() == output.get(1) {
|
||||
output.pop();
|
||||
}
|
||||
Some(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
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,
|
||||
quit: None,
|
||||
save_logs: None,
|
||||
scroll_down_many: None,
|
||||
scroll_down_one: None,
|
||||
scroll_end: None,
|
||||
scroll_start: None,
|
||||
scroll_up_many: None,
|
||||
scroll_up_one: None,
|
||||
select_next_panel: None,
|
||||
select_previous_panel: None,
|
||||
sort_by_name: None,
|
||||
sort_by_state: None,
|
||||
sort_by_status: None,
|
||||
sort_by_cpu: None,
|
||||
sort_by_memory: None,
|
||||
sort_by_id: None,
|
||||
sort_by_image: None,
|
||||
sort_by_rx: 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_deny: gen_v(("c", "d")),
|
||||
delete_confirm: gen_v(("e", "f")),
|
||||
exec: gen_v(("g", "h")),
|
||||
filter_mode: gen_v(("i", "j")),
|
||||
quit: gen_v(("k", "l")),
|
||||
save_logs: gen_v(("m", "n")),
|
||||
scroll_down_many: gen_v(("o", "p")),
|
||||
scroll_down_one: gen_v(("q", "r")),
|
||||
scroll_end: gen_v(("s", "t")),
|
||||
scroll_start: gen_v(("u", "v")),
|
||||
scroll_up_many: gen_v(("w", "x")),
|
||||
scroll_up_one: gen_v(("y", "z")),
|
||||
select_next_panel: gen_v(("0", "1")),
|
||||
select_previous_panel: gen_v(("2", "3")),
|
||||
sort_by_name: gen_v(("4", "5")),
|
||||
sort_by_state: gen_v(("6", "7")),
|
||||
sort_by_status: gen_v(("8", "9")),
|
||||
sort_by_cpu: gen_v(("F1", "F12")),
|
||||
sort_by_memory: gen_v(("/", "\\")),
|
||||
sort_by_id: gen_v(("[", "]")),
|
||||
sort_by_image: gen_v(("A", "B")),
|
||||
sort_by_rx: gen_v(("C", "D")),
|
||||
sort_by_tx: gen_v(("backspace", "TAB")),
|
||||
sort_reset: gen_v(("up", "down")),
|
||||
toggle_help: gen_v(("home", "end")),
|
||||
toggle_mouse_capture: gen_v(("pagedown", "PAGEUP")),
|
||||
};
|
||||
|
||||
let result = Keymap::from(Some(input));
|
||||
|
||||
let expected = Keymap {
|
||||
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
||||
delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('d'))),
|
||||
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
||||
exec: (KeyCode::Char('g'), Some(KeyCode::Char('h'))),
|
||||
filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))),
|
||||
quit: (KeyCode::Char('k'), Some(KeyCode::Char('l'))),
|
||||
save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('n'))),
|
||||
scroll_down_many: (KeyCode::Char('o'), Some(KeyCode::Char('p'))),
|
||||
scroll_down_one: (KeyCode::Char('q'), Some(KeyCode::Char('r'))),
|
||||
scroll_end: (KeyCode::Char('s'), Some(KeyCode::Char('t'))),
|
||||
scroll_start: (KeyCode::Char('u'), Some(KeyCode::Char('v'))),
|
||||
scroll_up_many: (KeyCode::Char('w'), Some(KeyCode::Char('x'))),
|
||||
scroll_up_one: (KeyCode::Char('y'), Some(KeyCode::Char('z'))),
|
||||
select_next_panel: (KeyCode::Char('0'), Some(KeyCode::Char('1'))),
|
||||
select_previous_panel: (KeyCode::Char('2'), Some(KeyCode::Char('3'))),
|
||||
sort_by_name: (KeyCode::Char('4'), Some(KeyCode::Char('5'))),
|
||||
sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))),
|
||||
sort_by_status: (KeyCode::Char('8'), Some(KeyCode::Char('9'))),
|
||||
sort_by_cpu: (KeyCode::F(1), Some(KeyCode::F(12))),
|
||||
sort_by_memory: (KeyCode::Char('/'), Some(KeyCode::Char('\\'))),
|
||||
sort_by_id: (KeyCode::Char('['), Some(KeyCode::Char(']'))),
|
||||
sort_by_image: (KeyCode::Char('A'), Some(KeyCode::Char('B'))),
|
||||
sort_by_rx: (KeyCode::Char('C'), Some(KeyCode::Char('D'))),
|
||||
sort_by_tx: (KeyCode::Backspace, Some(KeyCode::Tab)),
|
||||
sort_reset: (KeyCode::Up, Some(KeyCode::Down)),
|
||||
toggle_help: (KeyCode::Home, Some(KeyCode::End)),
|
||||
toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)),
|
||||
};
|
||||
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
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: u32,
|
||||
pub gui: bool,
|
||||
pub host: Option<String>,
|
||||
pub in_container: bool,
|
||||
pub keymap: Keymap,
|
||||
pub raw_logs: bool,
|
||||
pub save_dir: Option<PathBuf>,
|
||||
pub show_self: bool,
|
||||
pub show_std_err: bool,
|
||||
pub show_timestamp: bool,
|
||||
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: args.docker_interval,
|
||||
gui: !args.gui,
|
||||
host: args.host,
|
||||
in_container: Self::check_if_in_container(),
|
||||
keymap: Keymap::new(),
|
||||
raw_logs: args.raw,
|
||||
save_dir: Self::try_get_logs_dir(args.save_dir.as_ref()),
|
||||
show_self: !args.show_self,
|
||||
show_std_err: !args.no_std_err,
|
||||
show_timestamp: !args.timestamp,
|
||||
use_cli: args.use_cli,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigFile> for Config {
|
||||
fn from(config_file: ConfigFile) -> Self {
|
||||
Self {
|
||||
app_colors: AppColors::from(config_file.colors),
|
||||
color_logs: config_file.color_logs.unwrap_or(false),
|
||||
docker_interval: config_file.docker_interval.unwrap_or(1000),
|
||||
gui: config_file.gui.unwrap_or(true),
|
||||
host: config_file.host,
|
||||
in_container: Self::check_if_in_container(),
|
||||
keymap: Keymap::from(config_file.keymap),
|
||||
raw_logs: config_file.raw_logs.unwrap_or(false),
|
||||
save_dir: Self::try_get_logs_dir(config_file.save_dir.as_ref()),
|
||||
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),
|
||||
use_cli: config_file.use_cli.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Check if oxker is running inside of a container
|
||||
fn check_if_in_container() -> bool {
|
||||
std::env::var(ENV_KEY).is_ok_and(|i| i == ENV_VALUE)
|
||||
}
|
||||
|
||||
/// If a cli_arg is provided, create a pathbuf from it, else try to get home_dir automatically
|
||||
fn try_get_logs_dir(dir: Option<&String>) -> Option<PathBuf> {
|
||||
dir.as_ref()
|
||||
.map_or_else(Self::try_get_home_dir, |home_dir| {
|
||||
Some(std::path::Path::new(&home_dir).to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to get the home dir of the current user
|
||||
fn try_get_home_dir() -> Option<PathBuf> {
|
||||
directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned())
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn new() -> Self {
|
||||
let in_container = Self::check_if_in_container();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
if let Some(config_file) = &args.config_file {
|
||||
if let Some(config_file) =
|
||||
parse_config_file::ConfigFile::try_parse_from_file(config_file)
|
||||
{
|
||||
return Self::from(config_file);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(config_file) = parse_config_file::ConfigFile::try_parse(in_container) {
|
||||
return Self::from(config_file);
|
||||
}
|
||||
|
||||
Self::from(args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use clap::Parser;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Parser, Debug, Clone, Deserialize)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[command(version, about)]
|
||||
pub struct Args {
|
||||
/// Docker update interval in ms, minimum effectively 1000
|
||||
#[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
|
||||
pub docker_interval: u32,
|
||||
|
||||
/// Remove timestamps from Docker logs
|
||||
#[clap(short = 't')]
|
||||
pub timestamp: bool,
|
||||
|
||||
/// Attempt to colorize the logs, conflicts with "-r"
|
||||
#[clap(short = 'c', conflicts_with = "raw")]
|
||||
pub color: bool,
|
||||
|
||||
/// Show raw logs, default is to remove ansi formatting, conflicts with "-c"
|
||||
#[clap(short = 'r', conflicts_with = "color")]
|
||||
pub raw: bool,
|
||||
|
||||
/// Show self when running as a docker container
|
||||
#[clap(short = 's')]
|
||||
pub show_self: bool,
|
||||
|
||||
/// Don't draw gui - for debugging - mostly pointless
|
||||
#[clap(short = 'g')]
|
||||
pub gui: bool,
|
||||
|
||||
/// Docker host, defaults to `/var/run/docker.sock`
|
||||
#[clap(long, short = None)]
|
||||
pub host: Option<String>,
|
||||
|
||||
/// Do not include stderr output in logs
|
||||
#[clap(long = "no-stderr")]
|
||||
pub no_std_err: bool,
|
||||
|
||||
/// Directory for saving exported logs, defaults to `$HOME`
|
||||
#[clap(long="save-dir", short = None)]
|
||||
pub save_dir: Option<String>,
|
||||
|
||||
/// Path to a config file, readable as TOML, JSONC, or JSON
|
||||
#[clap(long="config-file", short = None)]
|
||||
pub config_file: Option<String>,
|
||||
|
||||
/// Force use of docker cli when execing into containers
|
||||
#[clap(long="use-cli", short = None)]
|
||||
pub use_cli: bool,
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
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 ConfigFileType {
|
||||
Toml,
|
||||
Jsonc,
|
||||
Json,
|
||||
JsoncAsJson,
|
||||
}
|
||||
|
||||
impl TryFrom<&PathBuf> for ConfigFileType {
|
||||
type Error = AppError;
|
||||
|
||||
fn try_from(value: &PathBuf) -> Result<Self, AppError> {
|
||||
let err = || AppError::IO(format!("Can't parse give config file: {}", value.display()));
|
||||
let Some(ext) = value.extension() else {
|
||||
return Err(err());
|
||||
};
|
||||
let Some(ext) = ext.to_str() else {
|
||||
return Err(err());
|
||||
};
|
||||
match ext {
|
||||
"toml" => Ok(Self::Toml),
|
||||
"json" => Ok(Self::Json),
|
||||
"jsonc" => Ok(Self::Jsonc),
|
||||
_ => Err(err()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigFileType {
|
||||
/// Get the local config directory, to be used by default config parser
|
||||
fn get_config_dir(in_container: bool) -> Option<PathBuf> {
|
||||
if in_container {
|
||||
Some(PathBuf::from("/"))
|
||||
} else {
|
||||
directories::BaseDirs::new()
|
||||
.map(|base_dirs| base_dirs.config_local_dir().join(env!("CARGO_PKG_NAME")))
|
||||
}
|
||||
}
|
||||
// should take in a pathbuf as well?
|
||||
fn get_default_filename(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))
|
||||
}
|
||||
}
|
||||
|
||||
// impl ConfigFileType
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct ConfigFile {
|
||||
pub color_logs: Option<bool>,
|
||||
pub docker_interval: Option<u32>,
|
||||
pub gui: Option<bool>,
|
||||
pub host: Option<String>,
|
||||
pub raw_logs: Option<bool>,
|
||||
pub show_timestamp: Option<bool>,
|
||||
pub save_dir: Option<String>,
|
||||
pub show_self: Option<bool>,
|
||||
pub show_std_err: Option<bool>,
|
||||
pub use_cli: Option<bool>,
|
||||
pub colors: Option<ConfigColors>,
|
||||
pub keymap: Option<ConfigKeymap>,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
/// Attempt to create an example.config.toml file, will attempt to recursively create the directories as well
|
||||
fn create_example_file(in_container: bool) -> Result<(), AppError> {
|
||||
if in_container {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config_dir = ConfigFileType::get_config_dir(in_container)
|
||||
.ok_or_else(|| AppError::IO("config_dir".to_owned()))?;
|
||||
let file_name = config_dir.join("example.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_type: ConfigFileType, input: &str) -> Result<Self, AppError> {
|
||||
match file_type {
|
||||
ConfigFileType::Json => {
|
||||
serde_json::from_str::<Self>(input).map_err(|i| AppError::Parse(i.to_string()))
|
||||
}
|
||||
ConfigFileType::Jsonc | ConfigFileType::JsoncAsJson => {
|
||||
serde_jsonc::from_str::<Self>(input).map_err(|i| AppError::Parse(i.to_string()))
|
||||
}
|
||||
ConfigFileType::Toml => {
|
||||
toml::from_str::<Self>(input).map_err(|i| AppError::Parse(i.message().to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the config file path to string, then attempt to parse
|
||||
fn parse_config_file(file_type: ConfigFileType, path: &PathBuf) -> Result<Self, AppError> {
|
||||
let mut file = std::fs::File::open(path).map_err(|_| {
|
||||
AppError::IO(
|
||||
path.to_str()
|
||||
.map_or_else(String::new, std::borrow::ToOwned::to_owned),
|
||||
)
|
||||
})?;
|
||||
let mut input = String::new();
|
||||
file.read_to_string(&mut input)
|
||||
.map_err(|i| AppError::IO(i.to_string()))?;
|
||||
Self::parse(file_type, &input)
|
||||
}
|
||||
|
||||
/// Resolve conflict in the args, this is handled automatically by Clap, basically just by rejecting it
|
||||
/// But here we can just change the options - although maybe should be also reject to follow the same behaviour as Clap?
|
||||
fn resolve_conflict(&mut self) {
|
||||
if let Some(color) = self.color_logs.as_ref() {
|
||||
if *color {
|
||||
self.raw_logs = Some(false);
|
||||
}
|
||||
}
|
||||
if let Some(interval) = self.docker_interval.as_ref() {
|
||||
if interval < &1000 {
|
||||
self.docker_interval = Some(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to parse the config file when the path is user supplied via cliargs
|
||||
pub fn try_parse_from_file(path: &str) -> Option<Self> {
|
||||
let path = PathBuf::from(path);
|
||||
let Ok(file_type) = ConfigFileType::try_from(&path) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Self::parse_config_file(file_type, &path).map_or(None, |mut config_file| {
|
||||
config_file.resolve_conflict();
|
||||
Some(config_file)
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let mut config = None;
|
||||
for file_type in [
|
||||
ConfigFileType::Toml,
|
||||
ConfigFileType::Jsonc,
|
||||
ConfigFileType::JsoncAsJson,
|
||||
ConfigFileType::Json,
|
||||
] {
|
||||
if let Ok(mut config_file) =
|
||||
Self::parse_config_file(file_type, &file_type.get_default_filename(in_container))
|
||||
{
|
||||
Self::resolve_conflict(&mut config_file);
|
||||
|
||||
config = Some(config_file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if config.is_none() {
|
||||
Self::create_example_file(in_container).ok();
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
#[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::ConfigFileType::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::ConfigFileType::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::ConfigFileType::Jsonc, example_jsonc).unwrap();
|
||||
assert!(result.keymap.is_some());
|
||||
assert_eq!(Keymap::from(result.keymap), Keymap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// make sure example.config.json matches the default keymap
|
||||
fn test_parse_config_keymap_json() {
|
||||
let example_json = include_str!("../../example_config/example.config.json");
|
||||
let result = ConfigFile::parse(super::ConfigFileType::Json, example_json).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::ConfigFileType::Jsonc, example_jsonc).unwrap();
|
||||
assert!(result_jsonc.keymap.is_some());
|
||||
let result_jsonc = result_jsonc.keymap.unwrap();
|
||||
|
||||
let example_json = include_str!("../../example_config/example.config.json");
|
||||
let result_json = ConfigFile::parse(super::ConfigFileType::Json, example_json).unwrap();
|
||||
assert!(result_json.keymap.is_some());
|
||||
let result_json = result_json.keymap.unwrap();
|
||||
|
||||
let example_toml = include_str!("./config.toml");
|
||||
let result_toml = ConfigFile::parse(super::ConfigFileType::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);
|
||||
assert_eq!(result_jsonc, result_json);
|
||||
}
|
||||
|
||||
#[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::ConfigFileType::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::ConfigFileType::Jsonc, example_jsonc).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_json() {
|
||||
let example_json = include_str!("../../example_config/example.config.json");
|
||||
let result = ConfigFile::parse(super::ConfigFileType::Json, example_json).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::ConfigFileType::Jsonc, example_jsonc).unwrap();
|
||||
assert!(result_jsonc.colors.is_some());
|
||||
let result_jsonc = result_jsonc.colors.unwrap();
|
||||
|
||||
let example_json = include_str!("../../example_config/example.config.json");
|
||||
let result_json = ConfigFile::parse(super::ConfigFileType::Json, example_json).unwrap();
|
||||
assert!(result_json.colors.is_some());
|
||||
let result_json = result_json.colors.unwrap();
|
||||
|
||||
let example_toml = include_str!("./config.toml");
|
||||
let result_toml = ConfigFile::parse(super::ConfigFileType::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);
|
||||
assert_eq!(result_jsonc, result_json);
|
||||
}
|
||||
}
|
||||
+11
-11
@@ -21,7 +21,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
app_data::{AppData, ContainerId, DockerCommand, State},
|
||||
app_error::AppError,
|
||||
parse_args::CliArgs,
|
||||
config::Config,
|
||||
ui::{GuiState, Status},
|
||||
ENTRY_POINT,
|
||||
};
|
||||
@@ -64,7 +64,7 @@ impl Binate {
|
||||
|
||||
pub struct DockerData {
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
args: CliArgs,
|
||||
config: Config,
|
||||
binate: Binate,
|
||||
docker: Arc<Docker>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
@@ -214,11 +214,11 @@ impl DockerData {
|
||||
.into_iter()
|
||||
.filter_map(|f| match f.id {
|
||||
Some(_) => {
|
||||
if self.args.in_container
|
||||
if self.config.in_container
|
||||
&& f.command
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.starts_with(ENTRY_POINT))
|
||||
&& self.args.show_self
|
||||
&& self.config.show_self
|
||||
{
|
||||
None
|
||||
} else {
|
||||
@@ -271,7 +271,7 @@ impl DockerData {
|
||||
Arc::clone(&self.app_data);
|
||||
let docker = Arc::clone(&self.docker);
|
||||
let spawns = Arc::clone(&self.spawns);
|
||||
let std_err = self.args.std_err;
|
||||
let std_err = self.config.show_std_err;
|
||||
let init = Arc::clone(&init);
|
||||
self.spawns.lock().insert(
|
||||
SpawnId::Log(id.clone()),
|
||||
@@ -319,7 +319,7 @@ impl DockerData {
|
||||
container.id.clone(),
|
||||
last_updated,
|
||||
Arc::clone(&self.spawns),
|
||||
self.args.std_err,
|
||||
self.config.show_std_err,
|
||||
)));
|
||||
}
|
||||
};
|
||||
@@ -399,8 +399,8 @@ impl DockerData {
|
||||
}
|
||||
|
||||
/// Send an update message every x ms, where x is the args.docker_interval
|
||||
fn heartbeat(args: &CliArgs, docker_tx: Sender<DockerMessage>) {
|
||||
let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
|
||||
fn heartbeat(config: &Config, docker_tx: Sender<DockerMessage>) {
|
||||
let update_duration = std::time::Duration::from_millis(u64::from(config.docker_interval));
|
||||
let mut now = std::time::Instant::now();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -421,11 +421,11 @@ impl DockerData {
|
||||
docker_tx: Sender<DockerMessage>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let args = app_data.lock().args.clone();
|
||||
let args = app_data.lock().config.clone();
|
||||
if app_data.lock().get_error().is_none() {
|
||||
let mut inner = Self {
|
||||
app_data,
|
||||
args,
|
||||
config: args,
|
||||
binate: Binate::One,
|
||||
docker: Arc::new(docker),
|
||||
gui_state,
|
||||
@@ -433,7 +433,7 @@ impl DockerData {
|
||||
spawns: Arc::new(Mutex::new(HashMap::new())),
|
||||
};
|
||||
inner.initialise_container_data().await;
|
||||
Self::heartbeat(&inner.args, docker_tx);
|
||||
Self::heartbeat(&inner.config, docker_tx);
|
||||
inner.message_handler().await;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ impl ExecMode {
|
||||
return None;
|
||||
}
|
||||
|
||||
let use_cli = app_data.lock().args.use_cli;
|
||||
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 {
|
||||
|
||||
+202
-59
@@ -21,6 +21,7 @@ mod message;
|
||||
use crate::{
|
||||
app_data::{AppData, DockerCommand, Header},
|
||||
app_error::AppError,
|
||||
config,
|
||||
docker_data::DockerMessage,
|
||||
exec::{tty_readable, ExecMode},
|
||||
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
|
||||
@@ -32,6 +33,7 @@ pub use message::InputMessages;
|
||||
pub struct InputHandler {
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
docker_tx: Sender<DockerMessage>,
|
||||
keymap: config::Keymap,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
mouse_capture: bool,
|
||||
@@ -47,11 +49,13 @@ impl InputHandler {
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
) {
|
||||
let keymap = app_data.lock().config.keymap.clone();
|
||||
let mut inner = Self {
|
||||
app_data,
|
||||
docker_tx,
|
||||
gui_state,
|
||||
is_running,
|
||||
keymap,
|
||||
rec,
|
||||
mouse_capture: true,
|
||||
};
|
||||
@@ -67,16 +71,15 @@ impl InputHandler {
|
||||
let status = self.gui_state.lock().get_status();
|
||||
let contains = |s: Status| status.contains(&s);
|
||||
|
||||
if !contains(Status::Error)
|
||||
if contains(Status::DeleteConfirm) {
|
||||
self.button_intersect(mouse_event).await;
|
||||
} else if !contains(Status::Error)
|
||||
| !contains(Status::Help)
|
||||
| !contains(Status::DeleteConfirm)
|
||||
| !contains(Status::Filter)
|
||||
{
|
||||
self.mouse_press(mouse_event);
|
||||
}
|
||||
if contains(Status::DeleteConfirm) {
|
||||
self.button_intersect(mouse_event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +118,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// Validate that one can exec into a Docker container
|
||||
async fn e_key(&self) {
|
||||
async fn exec_key(&self) {
|
||||
let is_oxker = self.app_data.lock().is_oxker();
|
||||
if !is_oxker && tty_readable() {
|
||||
let uuid = Uuid::new_v4();
|
||||
@@ -142,7 +145,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// Toggle the mouse capture (via input of the 'm' key)
|
||||
fn m_key(&mut self) {
|
||||
fn mouse_capture_key(&mut self) {
|
||||
let err = || {
|
||||
self.app_data.lock().set_error(
|
||||
AppError::MouseCapture(!self.mouse_capture),
|
||||
@@ -171,7 +174,7 @@ impl InputHandler {
|
||||
|
||||
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
|
||||
async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = self.app_data.lock().args.clone();
|
||||
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 {
|
||||
if let Some(log_path) = args.save_dir {
|
||||
@@ -187,7 +190,7 @@ impl InputHandler {
|
||||
let options = Some(LogsOptions::<String> {
|
||||
stderr: true,
|
||||
stdout: true,
|
||||
timestamps: args.timestamp,
|
||||
timestamps: args.show_timestamp,
|
||||
since: 0,
|
||||
..Default::default()
|
||||
});
|
||||
@@ -230,7 +233,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// Attempt to save the currently selected container logs to a file
|
||||
async fn s_key(&self) {
|
||||
async fn save_key(&self) {
|
||||
let status = self.gui_state.lock().get_status();
|
||||
let contains = |s: Status| status.contains(&s);
|
||||
|
||||
@@ -284,7 +287,7 @@ impl InputHandler {
|
||||
|
||||
/// 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 tab_key(&self) {
|
||||
fn next_panel_key(&self) {
|
||||
self.gui_state.lock().next_panel();
|
||||
if self.app_data.lock().get_container_len() == 0
|
||||
&& self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands
|
||||
@@ -295,7 +298,7 @@ impl InputHandler {
|
||||
|
||||
/// Change to previously selected panel
|
||||
/// Need to skip the commands planel if there no are current containers running
|
||||
fn back_tab_key(&self) {
|
||||
fn previous_panel_key(&self) {
|
||||
self.gui_state.lock().previous_panel();
|
||||
if self.app_data.lock().get_container_len() == 0
|
||||
&& self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands
|
||||
@@ -304,7 +307,7 @@ impl InputHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn home_key(&self) {
|
||||
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(),
|
||||
@@ -314,7 +317,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// Go to end of the list of the currently selected panel
|
||||
fn end_key(&self) {
|
||||
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(),
|
||||
@@ -325,32 +328,41 @@ impl InputHandler {
|
||||
|
||||
/// Actions to take when in Help status active
|
||||
fn handle_help(&mut self, key_code: KeyCode) {
|
||||
match key_code {
|
||||
KeyCode::Esc | KeyCode::Char('h' | 'H') => {
|
||||
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);
|
||||
}
|
||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||
_ => (),
|
||||
|
||||
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) {
|
||||
match key_code {
|
||||
KeyCode::Esc | KeyCode::Char('c' | 'C') => {
|
||||
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) {
|
||||
match key_code {
|
||||
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
||||
KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(),
|
||||
_ => (),
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +373,10 @@ impl InputHandler {
|
||||
self.app_data.lock().filter_term_clear();
|
||||
self.gui_state.lock().status_del(Status::Filter);
|
||||
}
|
||||
KeyCode::Enter | KeyCode::F(1) | KeyCode::Char('/') => {
|
||||
_ 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 => {
|
||||
@@ -380,47 +395,159 @@ impl InputHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle input that refers to the sorting of columns
|
||||
fn handle_sort(&self, key_code: KeyCode) {
|
||||
match key_code {
|
||||
_ 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);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle button presses in all other scenarios
|
||||
async fn handle_others(&mut self, key_code: KeyCode) {
|
||||
self.handle_sort(key_code);
|
||||
match key_code {
|
||||
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
||||
KeyCode::Char('1') => self.sort(Header::Name),
|
||||
KeyCode::Char('2') => self.sort(Header::State),
|
||||
KeyCode::Char('3') => self.sort(Header::Status),
|
||||
KeyCode::Char('4') => self.sort(Header::Cpu),
|
||||
KeyCode::Char('5') => self.sort(Header::Memory),
|
||||
KeyCode::Char('6') => self.sort(Header::Id),
|
||||
KeyCode::Char('7') => self.sort(Header::Image),
|
||||
KeyCode::Char('8') => self.sort(Header::Rx),
|
||||
KeyCode::Char('9') => self.sort(Header::Tx),
|
||||
KeyCode::Char('e' | 'E') => self.e_key().await,
|
||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||
KeyCode::Char('s' | 'S') => self.s_key().await,
|
||||
KeyCode::Tab => self.tab_key(),
|
||||
KeyCode::BackTab => self.back_tab_key(),
|
||||
KeyCode::Home => self.home_key(),
|
||||
KeyCode::End => self.end_key(),
|
||||
KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
|
||||
KeyCode::PageUp => {
|
||||
_ 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.save_logs.0 == key_code
|
||||
|| self.keymap.save_logs.1 == Some(key_code) =>
|
||||
{
|
||||
self.save_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_one.0 == key_code
|
||||
|| self.keymap.scroll_up_one.1 == Some(key_code) =>
|
||||
{
|
||||
self.previous();
|
||||
}
|
||||
|
||||
_ if self.keymap.scroll_up_many.0 == key_code
|
||||
|| self.keymap.scroll_up_many.1 == Some(key_code) =>
|
||||
{
|
||||
for _ in 0..=6 {
|
||||
self.previous();
|
||||
}
|
||||
}
|
||||
KeyCode::F(1) | KeyCode::Char('/') => {
|
||||
self.gui_state.lock().status_push(Status::Filter);
|
||||
self.docker_tx.send(DockerMessage::Update).await.ok();
|
||||
|
||||
_ if self.keymap.scroll_down_one.0 == key_code
|
||||
|| self.keymap.scroll_down_one.1 == Some(key_code) =>
|
||||
{
|
||||
self.next();
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
|
||||
KeyCode::PageDown => {
|
||||
|
||||
_ if self.keymap.scroll_down_many.0 == key_code
|
||||
|| self.keymap.scroll_down_many.1 == Some(key_code) =>
|
||||
{
|
||||
for _ in 0..=6 {
|
||||
self.next();
|
||||
}
|
||||
}
|
||||
|
||||
_ 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();
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -433,9 +560,10 @@ impl InputHandler {
|
||||
let contains_delete = contains(Status::DeleteConfirm);
|
||||
|
||||
if !contains_exec {
|
||||
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
||||
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
||||
if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() && !contains_filter {
|
||||
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
|
||||
{
|
||||
// Always just quit on Ctrl + c/C or q/Q, unless in Filter status active
|
||||
self.quit();
|
||||
}
|
||||
@@ -457,7 +585,7 @@ impl InputHandler {
|
||||
/// 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().button_intersect(Rect::new(
|
||||
let intersect = self.gui_state.lock().get_intersect_button(Rect::new(
|
||||
mouse_event.column,
|
||||
mouse_event.row,
|
||||
1,
|
||||
@@ -466,8 +594,8 @@ impl InputHandler {
|
||||
|
||||
if let Some(button) = intersect {
|
||||
match button {
|
||||
DeleteButton::Yes => self.confirm_delete().await,
|
||||
DeleteButton::No => self.clear_delete(),
|
||||
DeleteButton::Confirm => self.confirm_delete().await,
|
||||
DeleteButton::Cancel => self.clear_delete(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -475,21 +603,36 @@ impl InputHandler {
|
||||
|
||||
/// Handle mouse button events
|
||||
fn mouse_press(&self, mouse_event: MouseEvent) {
|
||||
// If in help panel, ignore?
|
||||
|
||||
let status = self.gui_state.lock().get_status();
|
||||
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.previous(),
|
||||
MouseEventKind::ScrollDown => self.next(),
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
|
||||
let header = self.gui_state.lock().header_intersect(mouse_point);
|
||||
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().panel_intersect(mouse_point);
|
||||
self.gui_state.lock().get_intersect_panel(mouse_point);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change state to next, depending which panel is currently in focus
|
||||
fn next(&self) {
|
||||
|
||||
+21
-18
@@ -1,10 +1,10 @@
|
||||
use app_data::AppData;
|
||||
use app_error::AppError;
|
||||
use bollard::{Docker, API_DEFAULT_VERSION};
|
||||
use config::Config;
|
||||
use docker_data::DockerData;
|
||||
use input_handler::InputMessages;
|
||||
use parking_lot::Mutex;
|
||||
use parse_args::CliArgs;
|
||||
use std::{
|
||||
process,
|
||||
sync::{
|
||||
@@ -17,10 +17,10 @@ use tracing::{error, info, Level};
|
||||
|
||||
mod app_data;
|
||||
mod app_error;
|
||||
mod config;
|
||||
mod docker_data;
|
||||
mod exec;
|
||||
mod input_handler;
|
||||
mod parse_args;
|
||||
mod ui;
|
||||
|
||||
use ui::{GuiState, Status, Ui};
|
||||
@@ -40,8 +40,9 @@ fn setup_tracing() {
|
||||
}
|
||||
|
||||
/// Read the optional docker_host path, the cli args take priority over the DOCKER_HOST env
|
||||
fn read_docker_host(args: &CliArgs) -> Option<String> {
|
||||
args.host
|
||||
fn read_docker_host(config: &Config) -> Option<String> {
|
||||
config
|
||||
.host
|
||||
.as_ref()
|
||||
.map_or_else(|| std::env::var(DOCKER_HOST).ok(), |x| Some(x.to_string()))
|
||||
}
|
||||
@@ -53,7 +54,7 @@ async fn docker_init(
|
||||
docker_tx: Sender<DockerMessage>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let host = read_docker_host(&app_data.lock().args);
|
||||
let host = read_docker_host(&app_data.lock().config);
|
||||
|
||||
let connection = host.map_or_else(Docker::connect_with_socket_defaults, |host| {
|
||||
Docker::connect_with_socket(&host, 120, API_DEFAULT_VERSION)
|
||||
@@ -96,17 +97,16 @@ fn handler_init(
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
setup_tracing();
|
||||
let config = config::Config::new();
|
||||
|
||||
let args = CliArgs::new();
|
||||
|
||||
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
||||
let app_data = Arc::new(Mutex::new(AppData::default(config.clone())));
|
||||
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
||||
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 args.gui {
|
||||
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).await;
|
||||
@@ -120,7 +120,7 @@ async fn main() {
|
||||
error!("{}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
if let Some(Ok(to_sleep)) = u128::from(args.docker_interval)
|
||||
if let Some(Ok(to_sleep)) = u128::from(config.docker_interval)
|
||||
.checked_sub(now.elapsed().as_millis())
|
||||
.map(u64::try_from)
|
||||
{
|
||||
@@ -155,21 +155,24 @@ mod tests {
|
||||
AppData, ContainerId, ContainerItem, ContainerPorts, ContainerStatus, Filter,
|
||||
RunningState, State, StatefulList,
|
||||
},
|
||||
parse_args::CliArgs,
|
||||
config::{AppColors, Config, Keymap},
|
||||
};
|
||||
|
||||
pub const fn gen_args() -> CliArgs {
|
||||
CliArgs {
|
||||
color: false,
|
||||
/// Default test config, has timestamps turned off
|
||||
pub const fn gen_config() -> Config {
|
||||
Config {
|
||||
color_logs: false,
|
||||
docker_interval: 1000,
|
||||
gui: true,
|
||||
host: None,
|
||||
std_err: false,
|
||||
show_std_err: false,
|
||||
in_container: false,
|
||||
save_dir: None,
|
||||
raw: false,
|
||||
raw_logs: false,
|
||||
show_self: false,
|
||||
timestamp: false,
|
||||
app_colors: AppColors::new(),
|
||||
keymap: Keymap::new(),
|
||||
show_timestamp: false,
|
||||
use_cli: false,
|
||||
}
|
||||
}
|
||||
@@ -198,7 +201,7 @@ mod tests {
|
||||
error: None,
|
||||
sorted_by: None,
|
||||
filter: Filter::new(),
|
||||
args: gen_args(),
|
||||
config: gen_config(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
use std::{path::PathBuf, process};
|
||||
|
||||
use clap::Parser;
|
||||
use tracing::error;
|
||||
|
||||
use crate::{ENV_KEY, ENV_VALUE};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[command(version, about)]
|
||||
pub struct Args {
|
||||
/// Docker update interval in ms, minimum effectively 1000
|
||||
#[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
|
||||
pub docker_interval: u32,
|
||||
|
||||
/// Remove timestamps from Docker logs
|
||||
#[clap(short = 't')]
|
||||
pub timestamp: bool,
|
||||
|
||||
/// Attempt to colorize the logs, conflicts with "-r"
|
||||
#[clap(short = 'c', conflicts_with = "raw")]
|
||||
pub color: bool,
|
||||
|
||||
/// Show raw logs, default is to remove ansi formatting, conflicts with "-c"
|
||||
#[clap(short = 'r', conflicts_with = "color")]
|
||||
pub raw: bool,
|
||||
|
||||
/// Show self when running as a docker container
|
||||
#[clap(short = 's')]
|
||||
pub show_self: bool,
|
||||
|
||||
/// Don't draw gui - for debugging - mostly pointless
|
||||
#[clap(short = 'g')]
|
||||
pub gui: bool,
|
||||
|
||||
/// Docker host, defaults to `/var/run/docker.sock`
|
||||
#[clap(long, short = None)]
|
||||
pub host: Option<String>,
|
||||
|
||||
/// Do not include stderr output in logs
|
||||
#[clap(long = "no-stderr")]
|
||||
pub no_std_err: bool,
|
||||
|
||||
/// Directory for saving exported logs, defaults to `$HOME`
|
||||
#[clap(long="save-dir", short = None)]
|
||||
pub save_dir: Option<String>,
|
||||
|
||||
/// Force use of docker cli when execing into containers
|
||||
#[clap(long="use-cli", short = None)]
|
||||
pub use_cli: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct CliArgs {
|
||||
pub color: bool,
|
||||
pub docker_interval: u32,
|
||||
pub gui: bool,
|
||||
pub host: Option<String>,
|
||||
pub in_container: bool,
|
||||
pub save_dir: Option<PathBuf>,
|
||||
pub raw: bool,
|
||||
pub show_self: bool,
|
||||
pub timestamp: bool,
|
||||
pub std_err: bool,
|
||||
pub use_cli: bool,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
/// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately
|
||||
/// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container
|
||||
fn check_if_in_container() -> bool {
|
||||
std::env::var(ENV_KEY).is_ok_and(|i| i == ENV_VALUE)
|
||||
}
|
||||
|
||||
/// Parse cli arguments
|
||||
pub fn new() -> Self {
|
||||
let args = Args::parse();
|
||||
|
||||
let logs_dir = args.save_dir.map_or_else(
|
||||
|| directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned()),
|
||||
|logs_dir| Some(std::path::Path::new(&logs_dir).to_owned()),
|
||||
);
|
||||
|
||||
// Quit the program if the docker update argument is 0
|
||||
// Should maybe change it to check if less than 100
|
||||
if args.docker_interval == 0 {
|
||||
error!("\"-d\" argument needs to be greater than 0");
|
||||
process::exit(1)
|
||||
}
|
||||
Self {
|
||||
color: args.color,
|
||||
docker_interval: args.docker_interval,
|
||||
use_cli: args.use_cli,
|
||||
gui: !args.gui,
|
||||
host: args.host,
|
||||
in_container: Self::check_if_in_container(),
|
||||
save_dir: logs_dir,
|
||||
raw: args.raw,
|
||||
std_err: !args.no_std_err,
|
||||
show_self: !args.show_self,
|
||||
timestamp: !args.timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub mod log_sanitizer {
|
||||
};
|
||||
|
||||
/// Attempt to colorize the given string to ratatui standards
|
||||
/// TODO this is somewhat slow/cpu intensive
|
||||
pub fn colorize_logs<'a>(input: &str) -> Vec<Line<'a>> {
|
||||
vec![Line::from(
|
||||
categorise_text(input)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,507 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use ratatui::{
|
||||
layout::{Alignment, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{FrameData, CONSTRAINT_50_50};
|
||||
use crate::{
|
||||
app_data::{ByteStats, CpuStats, State, Stats},
|
||||
config::AppColors,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ChartType {
|
||||
Cpu,
|
||||
Memory,
|
||||
}
|
||||
|
||||
impl ChartType {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mem_stats, mem_dataset, mem.1, "", cpu.2
|
||||
// current, dataset, max, name, state
|
||||
/// Create charts
|
||||
fn make_chart<'a, T: Stats + Display>(
|
||||
chart_type: ChartType,
|
||||
colors: AppColors,
|
||||
current: &'a T,
|
||||
dataset: Vec<Dataset<'a>>,
|
||||
max: &'a T,
|
||||
state: State,
|
||||
) -> Chart<'a> {
|
||||
let max_color = chart_type.get_max_color(colors, state);
|
||||
|
||||
Chart::new(dataset)
|
||||
.bg(chart_type.get_bg_color(colors))
|
||||
.block(
|
||||
Block::default()
|
||||
.style(Style::default().bg(chart_type.get_bg_color(colors)))
|
||||
.title_alignment(Alignment::Center)
|
||||
.title(Span::styled(
|
||||
format!(" {} {current} ", chart_type.name()),
|
||||
Style::default()
|
||||
.fg(chart_type.get_title_color(colors, state))
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
// .bg(chart_type.get_bg_color(colors))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(chart_type.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()
|
||||
// .bg(chart_type.get_bg_color(colors))
|
||||
.fg(chart_type.get_y_axis_color(colors)),
|
||||
)
|
||||
// Add 0.01, so that max point is always visible?
|
||||
.bounds([0.0, max.get_value() + 0.01]),
|
||||
)
|
||||
|
||||
// .style(Style::new().bg(chart_type.get_bg_color(colors)))
|
||||
}
|
||||
|
||||
/// Draw the cpu + mem charts
|
||||
pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
||||
if let Some((cpu, mem)) = 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(&cpu.0)];
|
||||
let mem_dataset = vec![Dataset::default()
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(colors.chart_memory.points))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&mem.0)];
|
||||
|
||||
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(
|
||||
ChartType::Cpu,
|
||||
colors,
|
||||
&cpu_stats,
|
||||
cpu_dataset,
|
||||
&cpu.1,
|
||||
cpu.2,
|
||||
);
|
||||
let mem_chart = make_chart(
|
||||
ChartType::Memory,
|
||||
colors,
|
||||
&mem_stats,
|
||||
mem_dataset,
|
||||
&mem.1,
|
||||
mem.2,
|
||||
);
|
||||
|
||||
f.render_widget(cpu_chart, area[0]);
|
||||
f.render_widget(mem_chart, area[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::State,
|
||||
config::AppColors,
|
||||
ui::{
|
||||
draw_blocks::tests::{
|
||||
expected_to_vec, get_result, insert_chart_data, test_setup, COLOR_ORANGE,
|
||||
},
|
||||
FrameData,
|
||||
},
|
||||
};
|
||||
|
||||
/// 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); 15] = [
|
||||
(1, 12),
|
||||
(2, 11),
|
||||
(2, 12),
|
||||
(3, 10),
|
||||
(3, 11),
|
||||
(3, 12),
|
||||
(4, 10),
|
||||
(4, 12),
|
||||
(5, 9),
|
||||
(5, 13),
|
||||
(5, 14),
|
||||
(6, 8),
|
||||
(6, 13),
|
||||
(7, 8),
|
||||
(7, 13),
|
||||
];
|
||||
|
||||
// co-ordinates of the dots from the memory chart
|
||||
const MEM_XY: [(usize, usize); 16] = [
|
||||
(1, 54),
|
||||
(1, 55),
|
||||
(2, 54),
|
||||
(2, 55),
|
||||
(3, 53),
|
||||
(3, 55),
|
||||
(4, 52),
|
||||
(4, 55),
|
||||
(5, 51),
|
||||
(5, 52),
|
||||
(5, 55),
|
||||
(5, 56),
|
||||
(6, 51),
|
||||
(6, 55),
|
||||
(7, 51),
|
||||
(7, 55),
|
||||
];
|
||||
|
||||
#[test]
|
||||
/// When status is Running, but not data, charts drawn without dots etc, colours correct
|
||||
fn test_draw_blocks_charts_running_none() {
|
||||
let (w, h) = (80, 10);
|
||||
let mut setup = test_setup(w, h, 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();
|
||||
|
||||
let expected = [
|
||||
"╭───────────── cpu 00.00% ─────────────╮╭─────────── memory 0.00 kB ───────────╮",
|
||||
"│00.00%│ ││0.00 kB│ │",
|
||||
"│ │ ││ │ │",
|
||||
"│ │ ││ │ │",
|
||||
"│ │ ││ │ │",
|
||||
"│ │ ││ │ │",
|
||||
"│ │ ││ │ │",
|
||||
"│ │ ││ │ │",
|
||||
"│ │ ││ │ │",
|
||||
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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 (w, h) = (80, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
insert_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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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 (w, h) = (80, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
insert_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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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 (w, h) = (80, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
insert_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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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_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 (w, h) = (80, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
insert_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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::RIGHT_ARROW;
|
||||
use crate::{
|
||||
app_data::AppData,
|
||||
config::AppColors,
|
||||
ui::{FrameData, GuiState, SelectablePanel},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{List, ListItem, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::generate_block;
|
||||
|
||||
/// Draw the command panel
|
||||
pub fn draw(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let block = generate_block(area, colors, fd, gui_state, SelectablePanel::Commands)
|
||||
.bg(colors.commands.background);
|
||||
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
|
||||
i.iter()
|
||||
.map(|c| {
|
||||
let lines = Line::from(vec![Span::styled(
|
||||
c.to_string(),
|
||||
Style::default().fg(c.get_color(colors)),
|
||||
)]);
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
if let Some(i) = app_data.lock().get_control_state() {
|
||||
let items = List::new(items)
|
||||
.block(block)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol(RIGHT_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 ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
tests::gen_container_summary,
|
||||
ui::{
|
||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup, BORDER_CHARS},
|
||||
FrameData,
|
||||
},
|
||||
};
|
||||
|
||||
// cusomt border colors
|
||||
#[test]
|
||||
/// Test that when DockerCommands are available, they are drawn correctly, dependant on container state
|
||||
fn test_draw_blocks_commands_none() {
|
||||
let (w, h) = (12, 6);
|
||||
let mut setup = test_setup(w, h, 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();
|
||||
|
||||
let expected = [
|
||||
"╭──────────╮",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
|
||||
for (row_index, row) in get_result(&setup, w) {
|
||||
let expected_row = expected_to_vec(&expected, row_index);
|
||||
for (cell_index, cell) in row.iter().enumerate() {
|
||||
assert_eq!(cell.symbol(), expected_row[cell_index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test that when DockerCommands are available, they are drawn correctly, dependant on container state
|
||||
fn test_draw_blocks_commands_some() {
|
||||
let (w, h) = (12, 6);
|
||||
let mut setup = test_setup(w, h, 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();
|
||||
|
||||
let expected = [
|
||||
"╭──────────╮",
|
||||
"│▶ pause │",
|
||||
"│ restart │",
|
||||
"│ stop │",
|
||||
"│ delete │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Change the controls state
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.update_containers(vec![gen_container_summary(1, "paused")]);
|
||||
setup.app_data.lock().docker_controls_next();
|
||||
|
||||
let expected = [
|
||||
"╭──────────╮",
|
||||
"│ resume │",
|
||||
"│▶ stop │",
|
||||
"│ delete │",
|
||||
"│ │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
|
||||
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, w) {
|
||||
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);
|
||||
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 (w, h) = (12, 6);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let expected = [
|
||||
"╭──────────╮",
|
||||
"│▶ pause │",
|
||||
"│ restart │",
|
||||
"│ stop │",
|
||||
"│ delete │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
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().next_panel();
|
||||
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, w) {
|
||||
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]);
|
||||
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 correctlty
|
||||
fn test_draw_blocks_commands_custom_colors() {
|
||||
let (w, h) = (12, 6);
|
||||
let mut setup = test_setup(w, h, 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();
|
||||
|
||||
let expected = [
|
||||
"╭──────────╮",
|
||||
"│▶ pause │",
|
||||
"│ restart │",
|
||||
"│ stop │",
|
||||
"│ delete │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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::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);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Change the controls state
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.update_containers(vec![gen_container_summary(1, "paused")]);
|
||||
setup.app_data.lock().docker_controls_next();
|
||||
|
||||
let expected = [
|
||||
"╭──────────╮",
|
||||
"│ resume │",
|
||||
"│▶ stop │",
|
||||
"│ delete │",
|
||||
"│ │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
|
||||
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, w) {
|
||||
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::White);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
// resume
|
||||
(1, 3..=7) => {
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
// stop
|
||||
(2, 3..=6) => {
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
// delete
|
||||
(3, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,421 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Direction, Layout},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{CONSTRAINT_BUTTONS, CONSTRAINT_POPUP};
|
||||
use crate::{
|
||||
app_data::ContainerName,
|
||||
config::{AppColors, Keymap},
|
||||
ui::{
|
||||
gui_state::{BoxLocation, Region},
|
||||
DeleteButton, GuiState,
|
||||
},
|
||||
};
|
||||
|
||||
use super::popup;
|
||||
|
||||
/// Draw the delete confirm box in the centre of the screen
|
||||
/// take in container id and container name here?
|
||||
pub fn draw(
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
keymap: &Keymap,
|
||||
name: &ContainerName,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(" Confirm Delete ")
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_delete.background)
|
||||
.fg(colors.popup_delete.text),
|
||||
)
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let confirm = Line::from(vec![
|
||||
Span::from("Are you sure you want to delete container: "),
|
||||
Span::styled(
|
||||
name.get(),
|
||||
Style::default()
|
||||
.fg(colors.popup_delete.text_highlight)
|
||||
.bg(colors.popup_delete.background)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]);
|
||||
|
||||
let yes_text = if keymap.delete_confirm == Keymap::new().delete_confirm {
|
||||
"( y ) yes".to_owned()
|
||||
} else if let Some(secondary) = keymap.delete_confirm.1 {
|
||||
format!("( {} | {} ) yes", keymap.delete_confirm.0, secondary)
|
||||
} else {
|
||||
format!("( {} ) yes", keymap.delete_confirm.0)
|
||||
};
|
||||
|
||||
let no_text = if keymap.delete_deny == Keymap::new().delete_deny {
|
||||
"( n ) no".to_owned()
|
||||
} else if let Some(secondary) = keymap.delete_deny.1 {
|
||||
format!("( {} | {} ) no", keymap.delete_deny.0, secondary)
|
||||
} else {
|
||||
format!("( {} ) no", keymap.delete_deny.0)
|
||||
};
|
||||
|
||||
// Find the maximum line width & height, and add some padding
|
||||
let max_line_width = u16::try_from(confirm.width()).unwrap_or(64) + 12;
|
||||
let lines = 8;
|
||||
|
||||
let confirm_para = Paragraph::new(confirm).alignment(Alignment::Center);
|
||||
|
||||
let button_block = || {
|
||||
Block::default()
|
||||
.border_type(BorderType::Rounded)
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().bg(colors.popup_delete.background))
|
||||
};
|
||||
|
||||
let yes_para = Paragraph::new(yes_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(button_block());
|
||||
|
||||
let no_para = Paragraph::new(no_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(button_block());
|
||||
|
||||
let area = popup::draw(
|
||||
lines,
|
||||
max_line_width.into(),
|
||||
f.area(),
|
||||
BoxLocation::MiddleCentre,
|
||||
);
|
||||
|
||||
let split_popup = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(CONSTRAINT_POPUP)
|
||||
.split(area);
|
||||
|
||||
let split_buttons = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(CONSTRAINT_BUTTONS)
|
||||
.split(split_popup[3]);
|
||||
|
||||
let no_area = split_buttons[1];
|
||||
let yes_area = split_buttons[3];
|
||||
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(block, area);
|
||||
f.render_widget(confirm_para, split_popup[1]);
|
||||
f.render_widget(no_para, no_area);
|
||||
f.render_widget(yes_para, yes_area);
|
||||
// Insert button areas into region map, so can interact with them on click
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Delete(DeleteButton::Cancel), no_area);
|
||||
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Delete(DeleteButton::Confirm), yes_area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::ContainerName,
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Delete container popup is drawn correctly
|
||||
fn test_draw_blocks_delete() {
|
||||
let (w, h) = (82, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Are you sure you want to delete container: container_1 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
||||
" │ │ ( n ) no │ │ ( y ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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 (w, h) = (106, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let name = ContainerName::from("container_1_container_1_container_1");
|
||||
setup.app_data.lock().containers.items[0].name = name.clone();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭──────────────────────────────────── Confirm Delete ────────────────────────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Are you sure you want to delete container: container_1_container_1_container_1 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭──────────────────────────────╮ ╭─────────────────────────────╮ │ ",
|
||||
" │ │ ( n ) no │ │ ( y ) yes │ │ ",
|
||||
" │ ╰──────────────────────────────╯ ╰─────────────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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 (w, h) = (82, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Are you sure you want to delete container: container_1 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
||||
" │ │ ( n ) no │ │ ( y ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn test_draw_blocks_delete_custom_keymap() {
|
||||
let (w, h) = (82, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Are you sure you want to delete container: container_1 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
||||
" │ │ ( End ) no │ │ ( F10 ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
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();
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Are you sure you want to delete container: container_1 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
||||
" │ │ ( End | Up ) no │ │ ( F10 | L ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Are you sure you want to delete container: container_1 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
||||
" │ │ ( End | Up ) no │ │ ( F10 ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
use ratatui::{
|
||||
layout::Alignment,
|
||||
style::Style,
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{max_line_width, NAME, VERSION};
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
config::{AppColors, Keymap},
|
||||
ui::gui_state::BoxLocation,
|
||||
};
|
||||
|
||||
use super::popup;
|
||||
|
||||
/// Draw an error popup over whole screen
|
||||
pub fn draw(
|
||||
f: &mut Frame,
|
||||
error: &AppError,
|
||||
keymap: &Keymap,
|
||||
seconds: Option<u8>,
|
||||
colors: AppColors,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(" Error ")
|
||||
.border_type(BorderType::Rounded)
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let to_push = if matches!(error, AppError::DockerConnect) {
|
||||
format!(
|
||||
"\n\n {}::v{} closing in {:02} seconds",
|
||||
NAME,
|
||||
VERSION,
|
||||
seconds.unwrap_or(5)
|
||||
)
|
||||
} else {
|
||||
let clear_suffix = "clear error";
|
||||
let clear_text = if keymap.clear == Keymap::new().clear {
|
||||
format!("( {} ) {clear_suffix}", keymap.clear.0)
|
||||
} else if let Some(secondary) = keymap.clear.1 {
|
||||
format!(" ( {} | {secondary} ) {clear_suffix}", keymap.clear.0)
|
||||
} else {
|
||||
format!(" ( {} ) {clear_suffix}", keymap.clear.0)
|
||||
};
|
||||
|
||||
let quit_suffix = "quit oxker";
|
||||
let quit_text = if keymap.quit == Keymap::new().quit {
|
||||
format!("( {} ) {quit_suffix}", keymap.quit.0)
|
||||
} else if let Some(secondary) = keymap.quit.1 {
|
||||
format!(" ( {} | {secondary} ) {quit_suffix}", keymap.quit.0)
|
||||
} else {
|
||||
format!(" ( {} ) {quit_suffix}", keymap.quit.0)
|
||||
};
|
||||
|
||||
format!("\n\n{clear_text}\n\n{quit_text}")
|
||||
};
|
||||
|
||||
let mut text = format!("\n{error}");
|
||||
|
||||
text.push_str(to_push.as_str());
|
||||
|
||||
// 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 super::VERSION;
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
};
|
||||
use crossterm::event::KeyCode;
|
||||
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_docker_connect_error() {
|
||||
let (w, h) = (46, 9);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let app_colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerConnect, keymap, Some(4), app_colors);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let version_row = format!(" │ oxker::v{VERSION} closing in 04 seconds │ ");
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭───────────────── Error ──────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to access docker daemon │ ",
|
||||
" │ │ ",
|
||||
version_row.as_str(),
|
||||
" │ │ ",
|
||||
" ╰──────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
(0 | 8, _) | (1..=7, 0 | 45) => {
|
||||
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_clearable_error() {
|
||||
let (w, h) = (39, 11);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let app_colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, keymap, Some(4), app_colors);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭────────────── Error ──────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to exec into container │ ",
|
||||
" │ │ ",
|
||||
" │ ( c ) clear error │ ",
|
||||
" │ │ ",
|
||||
" │ ( q ) quit oxker │ ",
|
||||
" │ │ ",
|
||||
" ╰───────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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_clearable_error_custom_colors() {
|
||||
let (w, h) = (39, 11);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.popup_error.background = Color::Yellow;
|
||||
colors.popup_error.text = Color::Black;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, keymap, Some(4), colors);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭────────────── Error ──────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to exec into container │ ",
|
||||
" │ │ ",
|
||||
" │ ( c ) clear error │ ",
|
||||
" │ │ ",
|
||||
" │ ( q ) quit oxker │ ",
|
||||
" │ │ ",
|
||||
" ╰───────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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 correct with both 1 and 2 definitions
|
||||
fn test_draw_blocks_clearable_error_custom_keymap() {
|
||||
let (w, h) = (39, 11);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.clear = (KeyCode::BackTab, None);
|
||||
keymap.quit = (KeyCode::F(4), None);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭────────────── Error ──────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to exec into container │ ",
|
||||
" │ │ ",
|
||||
" │ ( Back Tab ) clear error │ ",
|
||||
" │ │ ",
|
||||
" │ ( F4 ) quit oxker │ ",
|
||||
" │ │ ",
|
||||
" ╰───────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
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(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭────────────── Error ──────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to exec into container │ ",
|
||||
" │ │ ",
|
||||
" │ ( Back Tab | m ) clear error │ ",
|
||||
" │ │ ",
|
||||
" │ ( F4 | End ) quit oxker │ ",
|
||||
" │ │ ",
|
||||
" ╰───────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.quit = (KeyCode::F(4), Some(KeyCode::End));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭────────────── Error ──────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to exec into container │ ",
|
||||
" │ │ ",
|
||||
" │ ( c ) clear error │ ",
|
||||
" │ │ ",
|
||||
" │ ( F4 | End ) quit oxker │ ",
|
||||
" │ │ ",
|
||||
" ╰───────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{app_data::FilterBy, ui::FrameData};
|
||||
|
||||
/// Create the filter_by by spans, coloured dependant on which one is selected
|
||||
fn filter_by_spans(fd: &FrameData) -> [Span; 4] {
|
||||
let selected = Style::default().bg(Color::Gray).fg(Color::Black);
|
||||
let not_selected = Style::default().bg(Color::Reset).fg(Color::Reset);
|
||||
|
||||
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, frame: &mut Frame, fd: &FrameData) {
|
||||
let style_but = Style::default().fg(Color::Black).bg(Color::Magenta);
|
||||
let style_desc = Style::default().fg(Color::Gray).bg(Color::Reset);
|
||||
|
||||
let mut line = vec![
|
||||
Span::styled(" Esc ", style_but),
|
||||
Span::styled(" clear ", style_desc),
|
||||
Span::styled(" ← by → ", style_but),
|
||||
Span::from(" "),
|
||||
];
|
||||
line.extend_from_slice(&filter_by_spans(fd));
|
||||
line.extend_from_slice(&[
|
||||
Span::styled(
|
||||
" term: ",
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
fd.filter_term
|
||||
.as_ref()
|
||||
.map_or(String::new(), std::clone::Clone::clone),
|
||||
Style::default().fg(Color::Gray),
|
||||
),
|
||||
]);
|
||||
frame.render_widget(Line::from(line), area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::ui::{
|
||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
FrameData,
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
|
||||
/// Filter row is drawn correctly & colors are correct
|
||||
/// Colours change when filter_by option is changed
|
||||
fn test_draw_blocks_filter_row() {
|
||||
let (w, h) = (140, 1);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
setup
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_push(crate::ui::Status::Filter);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, f, &setup.fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" Esc clear ← by → Name Image Status All term: "
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
match result_cell_index {
|
||||
0..=4 | 12..=19 => {
|
||||
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);
|
||||
}
|
||||
21..=26 => {
|
||||
assert_eq!(result_cell.bg, Color::Gray);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
47..=53 => {
|
||||
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 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, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" Esc clear ← by → Name Image Status All term: cd "
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
match result_cell_index {
|
||||
0..=4 | 12..=19 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 54..=55 => {
|
||||
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..=53 => {
|
||||
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 when filter_by chances
|
||||
setup.app_data.lock().filter_by_next();
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, f, &fd);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" Esc clear ← by → Name Image Status All term: cd "
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
match result_cell_index {
|
||||
0..=4 | 12..=19 => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
5..=11 | 54..=55 => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
27..=33 => {
|
||||
assert_eq!(result_cell.bg, Color::Gray);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
47..=53 => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{CONSTRAINT_100, MARGIN};
|
||||
use crate::{
|
||||
app_data::{Header, SortedOrder},
|
||||
config::{AppColors, Keymap},
|
||||
ui::{gui_state::Region, FrameData, GuiState, Status},
|
||||
};
|
||||
|
||||
// Draw heading bar at top of program, always visible
|
||||
/// TODO Should separate into loading icon/headers/help functions
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn draw(
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
frame: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
keymap: &Keymap,
|
||||
) {
|
||||
let gen_style = |bg: Option<Color>, fg: Color| {
|
||||
bg.map_or_else(
|
||||
|| Style::default().fg(fg),
|
||||
|bg| Style::default().bg(bg).fg(fg),
|
||||
)
|
||||
};
|
||||
|
||||
frame.render_widget(
|
||||
Block::default().style(gen_style(Some(colors.headers_bar.background), Color::Reset)),
|
||||
area,
|
||||
);
|
||||
|
||||
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
|
||||
let header_block = |x: &Header, colors: AppColors| {
|
||||
let mut color = colors.headers_bar.text;
|
||||
let mut suffix = "";
|
||||
if let Some((a, b)) = &fd.sorted_by {
|
||||
if x == a {
|
||||
match b {
|
||||
SortedOrder::Asc => suffix = " ▲",
|
||||
SortedOrder::Desc => suffix = " ▼",
|
||||
}
|
||||
color = colors.headers_bar.text_selected;
|
||||
};
|
||||
};
|
||||
|
||||
(color, suffix)
|
||||
};
|
||||
|
||||
// Generate block for the headers, state and status has a specific layout, others all equal
|
||||
// width is dependant on it that column is selected to sort - or not
|
||||
// TODO - yes this is a mess, needs documenting correctly
|
||||
let gen_header = |header: &Header, width: usize, colors: AppColors| {
|
||||
let block = header_block(header, colors);
|
||||
|
||||
let text = format!(
|
||||
"{x:<width$}{MARGIN}",
|
||||
x = format!("{header}{ic}", ic = block.1),
|
||||
);
|
||||
let count = u16::try_from(text.chars().count()).unwrap_or_default();
|
||||
let status = Paragraph::new(text)
|
||||
.style(gen_style(None, block.0))
|
||||
.alignment(Alignment::Left);
|
||||
(status, count)
|
||||
};
|
||||
|
||||
// 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),
|
||||
];
|
||||
|
||||
let suffix = if fd.status.contains(&Status::Help) {
|
||||
"exit"
|
||||
} else {
|
||||
"show"
|
||||
};
|
||||
|
||||
let info_text = 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)
|
||||
};
|
||||
let info_width = info_text.chars().count();
|
||||
|
||||
let column_width = usize::from(area.width).saturating_sub(info_width);
|
||||
let column_width = if column_width > 0 { column_width } else { 1 };
|
||||
let splits = if fd.has_containers {
|
||||
vec![
|
||||
Constraint::Max(4),
|
||||
Constraint::Max(column_width.try_into().unwrap_or_default()),
|
||||
Constraint::Max(info_width.try_into().unwrap_or_default()),
|
||||
]
|
||||
} else {
|
||||
CONSTRAINT_100.to_vec()
|
||||
};
|
||||
|
||||
let split_bar = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(splits)
|
||||
.split(area);
|
||||
|
||||
// Draw loading icon, or not, and a prefix with a single space
|
||||
let loading_paragraph = Paragraph::new(format!("{:>2}", fd.loading_icon))
|
||||
.style(gen_style(None, colors.headers_bar.loading_spinner))
|
||||
.alignment(Alignment::Left);
|
||||
frame.render_widget(loading_paragraph, split_bar[0]);
|
||||
if fd.has_containers {
|
||||
let header_section_width = split_bar[1].width;
|
||||
|
||||
let mut counter = 0;
|
||||
|
||||
// Only show a header if the header cumulative header width is less than the header section width
|
||||
let header_data = header_meta
|
||||
.iter()
|
||||
.filter_map(|i| {
|
||||
let header_block = gen_header(&i.0, i.1.into(), colors);
|
||||
counter += header_block.1;
|
||||
if counter <= header_section_width {
|
||||
Some((header_block.0, i.0, Constraint::Max(header_block.1)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
||||
let headers_section = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(container_splits)
|
||||
.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);
|
||||
frame.render_widget(paragraph, rect);
|
||||
}
|
||||
}
|
||||
|
||||
// show/hide help
|
||||
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(info_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]);
|
||||
frame.render_widget(help_paragraph, split_bar[help_index]);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::style::Color;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_data::{Header, SortedOrder, StatefulList},
|
||||
config::{AppColors, Keymap},
|
||||
ui::{
|
||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
FrameData, Status,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Heading back only has show/exit help when no containers, correctly coloured
|
||||
fn test_draw_blocks_headers_no_containers() {
|
||||
let (w, h) = (140, 1);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
setup.app_data.lock().containers = StatefulList::new(vec![]);
|
||||
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
let expected = [" ( h ) show help "];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
let expected_row = expected_to_vec(&expected, row_index);
|
||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||
assert_eq!(result_cell.fg, Color::Gray,);
|
||||
}
|
||||
}
|
||||
|
||||
fd.status.insert(Status::Help);
|
||||
let expected = [" ( h ) exit help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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::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 (w, h) = (140, 1);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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::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 (w, h) = (80, 1);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
let expected =
|
||||
[" name state status cpu ( h ) show help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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::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]
|
||||
/// Test all combination of headers & sort by
|
||||
fn test_draw_blocks_headers_sort_containers() {
|
||||
let (w, h) = (140, 1);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
// Actual test, used for each header and sorted type
|
||||
let mut test =
|
||||
|expected: &[&str], range: RangeInclusive<usize>, x: (Header, SortedOrder)| {
|
||||
fd.sorted_by = Some(x);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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::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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Name
|
||||
test(&[" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 1..=17, (Header::Name, SortedOrder::Asc));
|
||||
test(&[" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 1..=17, (Header::Name, SortedOrder::Desc));
|
||||
// state
|
||||
test(&[" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "],18..=29, (Header::State, SortedOrder::Asc));
|
||||
test(&[" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 18..=29, (Header::State, SortedOrder::Desc));
|
||||
// status
|
||||
test(&[" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 30..=41, (Header::Status, SortedOrder::Asc));
|
||||
test(&[" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 30..=41, (Header::Status, SortedOrder::Desc));
|
||||
// cpu
|
||||
test(&[" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help "],42..=50, (Header::Cpu, SortedOrder::Asc));
|
||||
test(&[" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help "],42..=50, (Header::Cpu, SortedOrder::Desc));
|
||||
// memory
|
||||
test(&[" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help "], 51..=70, (Header::Memory, SortedOrder::Asc));
|
||||
test(&[" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help "], 51..=70, (Header::Memory, SortedOrder::Desc));
|
||||
//id
|
||||
test(&[" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help "], 71..=81, (Header::Id, SortedOrder::Asc));
|
||||
test(&[" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help "], 71..=81, (Header::Id, SortedOrder::Desc));
|
||||
// image
|
||||
test(&[" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help "], 82..=91, (Header::Image, SortedOrder::Asc));
|
||||
test(&[" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help "], 82..=91, (Header::Image, SortedOrder::Desc));
|
||||
// rx
|
||||
test(&[" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help "], 92..=101, (Header::Rx, SortedOrder::Asc));
|
||||
test(&[" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help "], 92..=101, (Header::Rx, SortedOrder::Desc));
|
||||
// tx
|
||||
test(&[" name state status cpu memory/limit id image ↓ rx ↑ tx ▲ ( h ) show help "], 102..=111, (Header::Tx, SortedOrder::Asc));
|
||||
test(&[" name state status cpu memory/limit id image ↓ rx ↑ tx ▼ ( h ) show help "], 102..=111, (Header::Tx, SortedOrder::Desc));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Show animation
|
||||
fn test_draw_blocks_headers_animation() {
|
||||
let (w, h) = (140, 1);
|
||||
let mut setup = test_setup(w, h, 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 expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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::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_cusomt_colors() {
|
||||
let (w, h) = (140, 1);
|
||||
let mut setup = test_setup(w, h, 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;
|
||||
|
||||
let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd, &setup.gui_state, keymap);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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::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 and two definitions
|
||||
fn test_draw_blocks_headers_custom_keymap() {
|
||||
let (w, h) = (140, 1);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.toggle_help = (KeyCode::Char('T'), None);
|
||||
|
||||
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( T ) show help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
keymap.toggle_help = (KeyCode::Char('T'), Some(KeyCode::Tab));
|
||||
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( T | Tab ) show help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,886 @@
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{AppColors, Keymap},
|
||||
ui::gui_state::BoxLocation,
|
||||
};
|
||||
|
||||
use super::{popup, DESCRIPTION, NAME_TEXT, REPO, VERSION};
|
||||
|
||||
/// Help popup box needs these three pieces of information
|
||||
struct HelpInfo {
|
||||
lines: Vec<Line<'static>>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
impl HelpInfo {
|
||||
/// Find the max width of a Span in &[Line]
|
||||
fn calc_width(lines: &[Line]) -> usize {
|
||||
lines
|
||||
.iter()
|
||||
.map(ratatui::prelude::Line::width)
|
||||
.max()
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Just an empty span, i.e. a new line
|
||||
fn empty_span<'a>() -> Line<'a> {
|
||||
Line::from(String::new())
|
||||
}
|
||||
|
||||
/// generate a span, of given &str and given color
|
||||
fn span<'a>(input: &str, color: Color) -> Span<'a> {
|
||||
Span::styled(input.to_owned(), Style::default().fg(color))
|
||||
}
|
||||
|
||||
/// &str to black text span
|
||||
fn text_span<'a>(input: &str, color: AppColors) -> Span<'a> {
|
||||
Self::span(input, color.popup_help.text)
|
||||
}
|
||||
|
||||
/// &str to white text span
|
||||
fn highlighted_text_span<'a>(input: &str, color: AppColors) -> Span<'a> {
|
||||
Self::span(input, color.popup_help.text_highlight)
|
||||
}
|
||||
|
||||
/// Generate the `oxker` name span + metadata
|
||||
fn gen_name(colors: AppColors) -> Self {
|
||||
let mut lines = NAME_TEXT
|
||||
.lines()
|
||||
.map(|i| Line::from(Self::highlighted_text_span(i, colors)))
|
||||
.collect::<Vec<_>>();
|
||||
lines.insert(0, Self::empty_span());
|
||||
let width = Self::calc_width(&lines);
|
||||
let height = lines.len();
|
||||
|
||||
Self {
|
||||
lines,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the description span + metadata
|
||||
fn gen_description(colors: AppColors) -> Self {
|
||||
let lines = [
|
||||
Self::empty_span(),
|
||||
Line::from(Self::highlighted_text_span(DESCRIPTION, colors)),
|
||||
Self::empty_span(),
|
||||
];
|
||||
|
||||
Self {
|
||||
lines: lines.to_vec(),
|
||||
width: Self::calc_width(&lines),
|
||||
height: lines.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the button information span + metadata
|
||||
fn gen_keymap_info(colors: AppColors) -> Self {
|
||||
let button_item = |x: &str| Self::highlighted_text_span(&format!(" ( {x} ) "), colors);
|
||||
let button_desc = |x: &str| Self::text_span(x, colors);
|
||||
let or = || button_desc("or");
|
||||
let space = || button_desc(" ");
|
||||
|
||||
let lines = [
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("tab"),
|
||||
or(),
|
||||
button_item("shift+tab"),
|
||||
button_desc("change panels"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("↑ ↓"),
|
||||
or(),
|
||||
button_item("j k"),
|
||||
or(),
|
||||
button_item("PgUp PgDown"),
|
||||
or(),
|
||||
button_item("Home End"),
|
||||
button_desc("change selected line"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("enter"),
|
||||
button_desc("send docker container command"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("e"),
|
||||
button_desc("exec into a container"),
|
||||
#[cfg(target_os = "windows")]
|
||||
button_desc(" - not available on Windows"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("h"),
|
||||
button_desc("toggle this help information - or click heading"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("s"),
|
||||
button_desc("save logs to file"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("m"),
|
||||
button_desc(
|
||||
"toggle mouse capture - if disabled, text on screen can be selected & copied",
|
||||
),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("F1"),
|
||||
or(),
|
||||
button_item("/"),
|
||||
button_desc("enter filter mode"),
|
||||
]),
|
||||
Line::from(vec![space(), button_item("0"), button_desc("stop sort")]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("1 - 9"),
|
||||
button_desc("sort by header - or click header"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("esc"),
|
||||
button_desc("close dialog"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("q"),
|
||||
button_desc("quit at any time"),
|
||||
]),
|
||||
];
|
||||
|
||||
Self {
|
||||
lines: lines.to_vec(),
|
||||
width: Self::calc_width(&lines),
|
||||
height: lines.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the final lines, GitHub link etc, + metadata
|
||||
fn gen_final(colors: AppColors) -> Self {
|
||||
let lines = [
|
||||
Self::empty_span(),
|
||||
Line::from(vec![Self::text_span(
|
||||
"currently an early work in progress, all and any input appreciated",
|
||||
colors,
|
||||
)]),
|
||||
Line::from(vec![Span::styled(
|
||||
REPO,
|
||||
Style::default()
|
||||
.fg(colors.popup_help.text_highlight)
|
||||
.add_modifier(Modifier::UNDERLINED),
|
||||
)]),
|
||||
];
|
||||
|
||||
Self {
|
||||
lines: lines.to_vec(),
|
||||
width: Self::calc_width(&lines),
|
||||
height: lines.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the display information when a custom keymap is being used
|
||||
fn gen_custom_keymap_info(colors: AppColors, km: &Keymap) -> Self {
|
||||
let button_item = |x: &str| Self::highlighted_text_span(&format!(" ( {x} ) "), colors);
|
||||
let button_desc = |x: &str| Self::text_span(x, colors);
|
||||
let or = || button_desc("or");
|
||||
let space = || button_desc(" ");
|
||||
|
||||
let or_secondary = |a: (KeyCode, Option<KeyCode>), desc: &str| {
|
||||
a.1.map_or_else(
|
||||
|| {
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item(&a.0.to_string()),
|
||||
button_desc(desc),
|
||||
])
|
||||
},
|
||||
|secondary| {
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item(&a.0.to_string()),
|
||||
or(),
|
||||
button_item(&secondary.to_string()),
|
||||
button_desc(desc),
|
||||
])
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let lines = [
|
||||
Line::from(vec![Span::from("Custom keymap config in use\n")])
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::default().fg(colors.popup_help.text_highlight)),
|
||||
or_secondary(km.select_next_panel, "select next panel"),
|
||||
or_secondary(km.select_previous_panel, "select previous panel"),
|
||||
or_secondary(km.scroll_down_one, "scroll list down by one"),
|
||||
or_secondary(km.scroll_up_one, "scroll list up by one"),
|
||||
or_secondary(km.scroll_down_many, "scroll list down by many"),
|
||||
or_secondary(km.scroll_up_many, "scroll list by up many"),
|
||||
or_secondary(km.scroll_end, "scroll list to end"),
|
||||
or_secondary(km.scroll_start, "scroll list to start"),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("enter"),
|
||||
button_desc("send docker container command"),
|
||||
]),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
or_secondary(km.exec, "exec into a container"),
|
||||
#[cfg(target_os = "windows")]
|
||||
or_secondary(km.exec, "exec into a container - not available on Windows"),
|
||||
or_secondary(
|
||||
km.toggle_help,
|
||||
"toggle this help information - or click heading",
|
||||
),
|
||||
or_secondary(km.toggle_help, "save logs to file"),
|
||||
or_secondary(
|
||||
km.toggle_mouse_capture,
|
||||
"toggle mouse capture - if disabled, text on screen can be selected & copied",
|
||||
),
|
||||
or_secondary(km.filter_mode, "enter filter mode"),
|
||||
or_secondary(km.sort_reset, "reset container sorting"),
|
||||
or_secondary(km.sort_by_name, "sort containers by name"),
|
||||
or_secondary(km.sort_by_state, "sort containers by state"),
|
||||
or_secondary(km.sort_by_status, "sort containers by status"),
|
||||
or_secondary(km.sort_by_cpu, "sort containers by cpu"),
|
||||
or_secondary(km.sort_by_memory, "sort containers by memory"),
|
||||
or_secondary(km.sort_by_id, "sort containers by id"),
|
||||
or_secondary(km.sort_by_image, "sort containers by image"),
|
||||
or_secondary(km.sort_by_rx, "sort containers by rx"),
|
||||
or_secondary(km.sort_by_tx, "sort containers by tx"),
|
||||
or_secondary(km.clear, "close dialog"),
|
||||
or_secondary(km.quit, "quit at any time"),
|
||||
];
|
||||
|
||||
Self {
|
||||
lines: lines.to_vec(),
|
||||
width: Self::calc_width(&lines),
|
||||
height: lines.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the help box in the centre of the screen
|
||||
pub fn draw(f: &mut Frame, colors: AppColors, keymap: &Keymap) {
|
||||
let title = format!(" {VERSION} ");
|
||||
|
||||
let name_info = HelpInfo::gen_name(colors);
|
||||
let description_info = HelpInfo::gen_description(colors);
|
||||
let final_info = HelpInfo::gen_final(colors);
|
||||
|
||||
let button_info = if keymap == &Keymap::new() {
|
||||
HelpInfo::gen_keymap_info(colors)
|
||||
} else {
|
||||
HelpInfo::gen_custom_keymap_info(colors, keymap)
|
||||
};
|
||||
|
||||
let max_line_width = [
|
||||
name_info.width,
|
||||
description_info.width,
|
||||
button_info.width,
|
||||
final_info.width,
|
||||
]
|
||||
.into_iter()
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
+ 2;
|
||||
|
||||
let max_height =
|
||||
name_info.height + description_info.height + button_info.height + final_info.height + 2;
|
||||
|
||||
let area = popup::draw(
|
||||
max_height,
|
||||
max_line_width,
|
||||
f.area(),
|
||||
BoxLocation::MiddleCentre,
|
||||
);
|
||||
|
||||
let split_popup = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Max(name_info.height.try_into().unwrap_or_default()),
|
||||
Constraint::Max(description_info.height.try_into().unwrap_or_default()),
|
||||
Constraint::Max(button_info.height.try_into().unwrap_or_default()),
|
||||
Constraint::Min(final_info.height.try_into().unwrap_or_default()),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let name_paragraph = Paragraph::new(name_info.lines)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_help.background)
|
||||
.fg(colors.popup_help.text_highlight),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let style = || {
|
||||
Style::default()
|
||||
.bg(colors.popup_help.background)
|
||||
.fg(colors.popup_help.text)
|
||||
};
|
||||
let description_paragraph = Paragraph::new(description_info.lines)
|
||||
.style(style())
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let help_paragraph = Paragraph::new(button_info.lines)
|
||||
.style(style())
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
let final_paragraph = Paragraph::new(final_info.lines)
|
||||
.style(style())
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let block = Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(
|
||||
Style::default()
|
||||
.fg(colors.popup_help.text)
|
||||
.bg(colors.popup_help.background),
|
||||
);
|
||||
|
||||
// Order is important here
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(name_paragraph, split_popup[0]);
|
||||
f.render_widget(description_paragraph, split_popup[1]);
|
||||
f.render_widget(help_paragraph, split_popup[2]);
|
||||
f.render_widget(final_paragraph, split_popup[3]);
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::VERSION,
|
||||
};
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup};
|
||||
|
||||
#[test]
|
||||
/// This will cause issues once the version has more than the current 5 chars (0.5.0)
|
||||
fn test_draw_blocks_help() {
|
||||
let (w, h) = (87, 33);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &setup.app_data.lock().config.keymap);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let version_row = format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ ");
|
||||
let expected = [
|
||||
" ",
|
||||
version_row.as_str(),
|
||||
" │ │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ",
|
||||
r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#,
|
||||
r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#,
|
||||
r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#,
|
||||
r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#,
|
||||
" │ │ ",
|
||||
" │ A simple tui to view & control docker containers │ ",
|
||||
" │ │ ",
|
||||
" │ ( tab ) or ( shift+tab ) change panels │ ",
|
||||
" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ",
|
||||
" │ ( enter ) send docker container command │ ",
|
||||
" │ ( e ) exec into a container │ ",
|
||||
" │ ( h ) toggle this help information - or click heading │ ",
|
||||
" │ ( s ) save logs to file │ ",
|
||||
" │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||
" │ ( F1 ) or ( / ) enter filter mode │ ",
|
||||
" │ ( 0 ) stop sort │ ",
|
||||
" │ ( 1 - 9 ) sort by header - or click header │ ",
|
||||
" │ ( esc ) close dialog │ ",
|
||||
" │ ( q ) quit at any time │ ",
|
||||
" │ │ ",
|
||||
" │ currently an early work in progress, all and any input appreciated │ ",
|
||||
" │ https://github.com/mrjackwills/oxker │ ",
|
||||
" │ │ ",
|
||||
" │ │ ",
|
||||
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||
" "
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
// first & last row, and first & last char on each row, is reset/reset, making sure that the help info is centered in the given area
|
||||
(0 | 32, _) | (0..=33, 0 | 86) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
// border is black on magenta
|
||||
(1 | 31, _) | (1..=31, 1 | 85) => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
// oxker logo && description
|
||||
(2..=10, 2..=85) | (12, 19..=66)
|
||||
// button in the brackets
|
||||
| (14, 2..=10 | 13..=27)
|
||||
| (15, 2..=10 | 13..=21 | 24..=40 | 43..=56)
|
||||
| (16 | 23, 2..=12)
|
||||
| (17..=20 | 22 | 25, 2..=8)
|
||||
| (21, 2..=9 | 12..=18)
|
||||
| (24, 2..=10) => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
// The URL is white and underlined
|
||||
(28, 25..=60) => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
assert_eq!(result_cell.modifier, Modifier::UNDERLINED);
|
||||
}
|
||||
// The rest is black on magenta
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test that the help panel gets drawn with custom colors
|
||||
fn test_draw_blocks_help_custom_colors() {
|
||||
let (w, h) = (87, 33);
|
||||
let mut setup = test_setup(w, h, true, 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
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &setup.app_data.lock().config.keymap);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let version_row = format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ ");
|
||||
let expected = [
|
||||
" ",
|
||||
version_row.as_str(),
|
||||
" │ │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ",
|
||||
r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#,
|
||||
r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#,
|
||||
r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#,
|
||||
r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#,
|
||||
" │ │ ",
|
||||
" │ A simple tui to view & control docker containers │ ",
|
||||
" │ │ ",
|
||||
" │ ( tab ) or ( shift+tab ) change panels │ ",
|
||||
" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ",
|
||||
" │ ( enter ) send docker container command │ ",
|
||||
" │ ( e ) exec into a container │ ",
|
||||
" │ ( h ) toggle this help information - or click heading │ ",
|
||||
" │ ( s ) save logs to file │ ",
|
||||
" │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||
" │ ( F1 ) or ( / ) enter filter mode │ ",
|
||||
" │ ( 0 ) stop sort │ ",
|
||||
" │ ( 1 - 9 ) sort by header - or click header │ ",
|
||||
" │ ( esc ) close dialog │ ",
|
||||
" │ ( q ) quit at any time │ ",
|
||||
" │ │ ",
|
||||
" │ currently an early work in progress, all and any input appreciated │ ",
|
||||
" │ https://github.com/mrjackwills/oxker │ ",
|
||||
" │ │ ",
|
||||
" │ │ ",
|
||||
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||
" "
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
// first & last row, and first & last char on each row, is reset/reset, making sure that the help info is centered in the given area
|
||||
(0 | 32, _) | (0..=33, 0 | 86) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
// border is black on magenta
|
||||
(1 | 31, _) | (1..=31, 1 | 85) => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// oxker logo && description
|
||||
(2..=10, 2..=85) | (12, 19..=66)
|
||||
// button in the brackets
|
||||
| (14, 2..=10 | 13..=27)
|
||||
| (15, 2..=10 | 13..=21 | 24..=40 | 43..=56)
|
||||
| (16 | 23, 2..=12)
|
||||
| (17..=20 | 22 | 25, 2..=8)
|
||||
| (21, 2..=9 | 12..=18)
|
||||
| (24, 2..=10) => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
// The URL is yellow and underlined
|
||||
(28, 25..=60) => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
assert_eq!(result_cell.modifier, Modifier::UNDERLINED);
|
||||
}
|
||||
// The rest is red on black
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Help panel will show custom keymap if in use, with one definition for each entry
|
||||
fn test_draw_blocks_custom_keymap_one_definition() {
|
||||
let (w, h) = (98, 48);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
let input = Keymap {
|
||||
clear: (KeyCode::Char('a'), None),
|
||||
delete_deny: (KeyCode::Char('c'), None),
|
||||
delete_confirm: (KeyCode::Char('e'), None),
|
||||
exec: (KeyCode::Char('g'), None),
|
||||
filter_mode: (KeyCode::Char('i'), None),
|
||||
quit: (KeyCode::Char('k'), None),
|
||||
save_logs: (KeyCode::Char('m'), None),
|
||||
scroll_down_many: (KeyCode::Char('o'), None),
|
||||
scroll_down_one: (KeyCode::Char('q'), None),
|
||||
scroll_end: (KeyCode::Char('s'), None),
|
||||
scroll_start: (KeyCode::Char('u'), None),
|
||||
scroll_up_many: (KeyCode::Char('w'), None),
|
||||
scroll_up_one: (KeyCode::Char('y'), None),
|
||||
select_next_panel: (KeyCode::Char('0'), None),
|
||||
select_previous_panel: (KeyCode::Char('2'), None),
|
||||
sort_by_name: (KeyCode::Char('4'), None),
|
||||
sort_by_state: (KeyCode::Char('6'), None),
|
||||
sort_by_status: (KeyCode::Char('8'), None),
|
||||
sort_by_cpu: (KeyCode::F(1), None),
|
||||
sort_by_memory: (KeyCode::Char('#'), None),
|
||||
sort_by_id: (KeyCode::Char('/'), None),
|
||||
sort_by_image: (KeyCode::Char(','), None),
|
||||
sort_by_rx: (KeyCode::Char('.'), None),
|
||||
sort_by_tx: (KeyCode::Backspace, None),
|
||||
sort_reset: (KeyCode::Up, None),
|
||||
toggle_help: (KeyCode::Home, None),
|
||||
toggle_mouse_capture: (KeyCode::PageDown, None),
|
||||
};
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &input);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let version_row = format!(" ╭ {VERSION} ─────────────────────────────────────────────────────────────────────────────────────╮ ");
|
||||
let expected = [
|
||||
" ",
|
||||
version_row.as_str(),
|
||||
" │ │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ",
|
||||
r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#,
|
||||
r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#,
|
||||
r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#,
|
||||
r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#,
|
||||
" │ │ ",
|
||||
" │ A simple tui to view & control docker containers │ ",
|
||||
" │ │ ",
|
||||
" │ Custom keymap config in use │ ",
|
||||
" │ ( 0 ) select next panel │ ",
|
||||
" │ ( 2 ) select previous panel │ ",
|
||||
" │ ( q ) scroll list down by one │ ",
|
||||
" │ ( y ) scroll list up by one │ ",
|
||||
" │ ( o ) scroll list down by many │ ",
|
||||
" │ ( w ) scroll list by up many │ ",
|
||||
" │ ( s ) scroll list to end │ ",
|
||||
" │ ( u ) scroll list to start │ ",
|
||||
" │ ( enter ) send docker container command │ ",
|
||||
" │ ( g ) exec into a container │ ",
|
||||
" │ ( Home ) toggle this help information - or click heading │ ",
|
||||
" │ ( Home ) save logs to file │ ",
|
||||
" │ ( Page Down ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||
" │ ( i ) enter filter mode │ ",
|
||||
" │ ( Up ) reset container sorting │ ",
|
||||
" │ ( 4 ) sort containers by name │ ",
|
||||
" │ ( 6 ) sort containers by state │ ",
|
||||
" │ ( 8 ) sort containers by status │ ",
|
||||
" │ ( F1 ) sort containers by cpu │ ",
|
||||
" │ ( # ) sort containers by memory │ ",
|
||||
" │ ( / ) sort containers by id │ ",
|
||||
" │ ( , ) sort containers by image │ ",
|
||||
" │ ( . ) sort containers by rx │ ",
|
||||
" │ ( Backspace ) sort containers by tx │ ",
|
||||
" │ ( a ) close dialog │ ",
|
||||
" │ ( k ) quit at any time │ ",
|
||||
" │ │ ",
|
||||
" │ currently an early work in progress, all and any input appreciated │ ",
|
||||
" │ https://github.com/mrjackwills/oxker │ ",
|
||||
" │ │ ",
|
||||
" │ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||
" "
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
if row_index == 14 && (36..=62).contains(&result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Help panel will show custom keymap if in use, with two definition for each entry
|
||||
fn test_draw_blocks_custom_keymap_two_definitions() {
|
||||
let (w, h) = (110, 48);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
let input = Keymap {
|
||||
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
||||
delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('d'))),
|
||||
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
||||
exec: (KeyCode::Char('g'), Some(KeyCode::Char('h'))),
|
||||
filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))),
|
||||
quit: (KeyCode::Char('k'), Some(KeyCode::Char('l'))),
|
||||
save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('n'))),
|
||||
scroll_down_many: (KeyCode::Char('o'), Some(KeyCode::Char('p'))),
|
||||
scroll_down_one: (KeyCode::Char('q'), Some(KeyCode::Char('r'))),
|
||||
scroll_end: (KeyCode::Char('s'), Some(KeyCode::Char('t'))),
|
||||
scroll_start: (KeyCode::Char('u'), Some(KeyCode::Char('v'))),
|
||||
scroll_up_many: (KeyCode::Char('w'), Some(KeyCode::Char('x'))),
|
||||
scroll_up_one: (KeyCode::Char('y'), Some(KeyCode::Char('z'))),
|
||||
select_next_panel: (KeyCode::Char('0'), Some(KeyCode::Char('1'))),
|
||||
select_previous_panel: (KeyCode::Char('2'), Some(KeyCode::Char('3'))),
|
||||
sort_by_name: (KeyCode::Char('4'), Some(KeyCode::Char('5'))),
|
||||
sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))),
|
||||
sort_by_status: (KeyCode::Char('8'), Some(KeyCode::Char('9'))),
|
||||
sort_by_cpu: (KeyCode::F(1), Some(KeyCode::F(12))),
|
||||
sort_by_memory: (KeyCode::Char('#'), Some(KeyCode::Char('-'))),
|
||||
sort_by_id: (KeyCode::Char('/'), Some(KeyCode::Char('='))),
|
||||
sort_by_image: (KeyCode::Char(','), Some(KeyCode::Char('\\'))),
|
||||
sort_by_rx: (KeyCode::Char('.'), Some(KeyCode::Char(']'))),
|
||||
sort_by_tx: (KeyCode::Backspace, Some(KeyCode::BackTab)),
|
||||
sort_reset: (KeyCode::Up, Some(KeyCode::Down)),
|
||||
toggle_help: (KeyCode::Home, Some(KeyCode::Delete)),
|
||||
toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)),
|
||||
};
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &input);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let version_row = format!(" ╭ {VERSION} ───────────────────────────────────────────────────────────────────────────────────────────────────╮ ");
|
||||
let expected = [
|
||||
" ",
|
||||
version_row.as_str(),
|
||||
" │ │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ",
|
||||
r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#,
|
||||
r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#,
|
||||
r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#,
|
||||
r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#,
|
||||
" │ │ ",
|
||||
" │ A simple tui to view & control docker containers │ ",
|
||||
" │ │ ",
|
||||
" │ Custom keymap config in use │ ",
|
||||
" │ ( 0 ) or ( 1 ) select next panel │ ",
|
||||
" │ ( 2 ) or ( 3 ) select previous panel │ ",
|
||||
" │ ( q ) or ( r ) scroll list down by one │ ",
|
||||
" │ ( y ) or ( z ) scroll list up by one │ ",
|
||||
" │ ( o ) or ( p ) scroll list down by many │ ",
|
||||
" │ ( w ) or ( x ) scroll list by up many │ ",
|
||||
" │ ( s ) or ( t ) scroll list to end │ ",
|
||||
" │ ( u ) or ( v ) scroll list to start │ ",
|
||||
" │ ( enter ) send docker container command │ ",
|
||||
" │ ( g ) or ( h ) exec into a container │ ",
|
||||
" │ ( Home ) or ( Del ) toggle this help information - or click heading │ ",
|
||||
" │ ( Home ) or ( Del ) save logs to file │ ",
|
||||
" │ ( Page Down ) or ( Page Up ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||
" │ ( i ) or ( j ) enter filter mode │ ",
|
||||
" │ ( Up ) or ( Down ) reset container sorting │ ",
|
||||
" │ ( 4 ) or ( 5 ) sort containers by name │ ",
|
||||
" │ ( 6 ) or ( 7 ) sort containers by state │ ",
|
||||
" │ ( 8 ) or ( 9 ) sort containers by status │ ",
|
||||
" │ ( F1 ) or ( F12 ) sort containers by cpu │ ",
|
||||
" │ ( # ) or ( - ) sort containers by memory │ ",
|
||||
" │ ( / ) or ( = ) sort containers by id │ ",
|
||||
r" │ ( , ) or ( \ ) sort containers by image │ ",
|
||||
" │ ( . ) or ( ] ) sort containers by rx │ ",
|
||||
" │ ( Backspace ) or ( Back Tab ) sort containers by tx │ ",
|
||||
" │ ( a ) or ( b ) close dialog │ ",
|
||||
" │ ( k ) or ( l ) quit at any time │ ",
|
||||
" │ │ ",
|
||||
" │ currently an early work in progress, all and any input appreciated │ ",
|
||||
" │ https://github.com/mrjackwills/oxker │ ",
|
||||
" │ │ ",
|
||||
" │ │ ",
|
||||
" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Help panel will show custom keymap if in use, with either one or two definition for each entry
|
||||
fn test_draw_blocks_custom_keymap_one_and_two_definitions() {
|
||||
let (w, h) = (110, 48);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
let input = Keymap {
|
||||
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
||||
delete_deny: (KeyCode::Char('c'), None),
|
||||
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
||||
exec: (KeyCode::Char('g'), None),
|
||||
filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))),
|
||||
quit: (KeyCode::Char('k'), None),
|
||||
save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('n'))),
|
||||
scroll_down_many: (KeyCode::Char('o'), None),
|
||||
scroll_down_one: (KeyCode::Char('q'), Some(KeyCode::Char('r'))),
|
||||
scroll_end: (KeyCode::Char('s'), None),
|
||||
scroll_start: (KeyCode::Char('u'), Some(KeyCode::Char('v'))),
|
||||
scroll_up_many: (KeyCode::Char('w'), None),
|
||||
scroll_up_one: (KeyCode::Char('y'), Some(KeyCode::Char('z'))),
|
||||
select_next_panel: (KeyCode::Char('0'), None),
|
||||
select_previous_panel: (KeyCode::Char('2'), Some(KeyCode::Char('3'))),
|
||||
sort_by_name: (KeyCode::Char('4'), None),
|
||||
sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))),
|
||||
sort_by_status: (KeyCode::Char('8'), None),
|
||||
sort_by_cpu: (KeyCode::F(1), Some(KeyCode::F(12))),
|
||||
sort_by_memory: (KeyCode::Char('#'), None),
|
||||
sort_by_id: (KeyCode::Char('/'), Some(KeyCode::Char('='))),
|
||||
sort_by_image: (KeyCode::Char(','), None),
|
||||
sort_by_rx: (KeyCode::Char('.'), Some(KeyCode::Char(']'))),
|
||||
sort_by_tx: (KeyCode::Backspace, None),
|
||||
sort_reset: (KeyCode::Up, Some(KeyCode::Down)),
|
||||
toggle_help: (KeyCode::Home, None),
|
||||
toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)),
|
||||
};
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &input);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let version_row = format!(" ╭ {VERSION} ───────────────────────────────────────────────────────────────────────────────────────────────────╮ ");
|
||||
let expected = [
|
||||
" ",
|
||||
version_row.as_str(),
|
||||
" │ │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ 88 │ ",
|
||||
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ",
|
||||
r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#,
|
||||
r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#,
|
||||
r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#,
|
||||
r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#,
|
||||
" │ │ ",
|
||||
" │ A simple tui to view & control docker containers │ ",
|
||||
" │ │ ",
|
||||
" │ Custom keymap config in use │ ",
|
||||
" │ ( 0 ) select next panel │ ",
|
||||
" │ ( 2 ) or ( 3 ) select previous panel │ ",
|
||||
" │ ( q ) or ( r ) scroll list down by one │ ",
|
||||
" │ ( y ) or ( z ) scroll list up by one │ ",
|
||||
" │ ( o ) scroll list down by many │ ",
|
||||
" │ ( w ) scroll list by up many │ ",
|
||||
" │ ( s ) scroll list to end │ ",
|
||||
" │ ( u ) or ( v ) scroll list to start │ ",
|
||||
" │ ( enter ) send docker container command │ ",
|
||||
" │ ( g ) exec into a container │ ",
|
||||
" │ ( Home ) toggle this help information - or click heading │ ",
|
||||
" │ ( Home ) save logs to file │ ",
|
||||
" │ ( Page Down ) or ( Page Up ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||
" │ ( i ) or ( j ) enter filter mode │ ",
|
||||
" │ ( Up ) or ( Down ) reset container sorting │ ",
|
||||
" │ ( 4 ) sort containers by name │ ",
|
||||
" │ ( 6 ) or ( 7 ) sort containers by state │ ",
|
||||
" │ ( 8 ) sort containers by status │ ",
|
||||
" │ ( F1 ) or ( F12 ) sort containers by cpu │ ",
|
||||
" │ ( # ) sort containers by memory │ ",
|
||||
" │ ( / ) or ( = ) sort containers by id │ ",
|
||||
" │ ( , ) sort containers by image │ ",
|
||||
" │ ( . ) or ( ] ) sort containers by rx │ ",
|
||||
" │ ( Backspace ) sort containers by tx │ ",
|
||||
" │ ( a ) or ( b ) close dialog │ ",
|
||||
" │ ( k ) quit at any time │ ",
|
||||
" │ │ ",
|
||||
" │ currently an early work in progress, all and any input appreciated │ ",
|
||||
" │ https://github.com/mrjackwills/oxker │ ",
|
||||
" │ │ ",
|
||||
" │ │ ",
|
||||
" ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::Alignment,
|
||||
style::Style,
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
ui::{gui_state::BoxLocation, GuiState},
|
||||
};
|
||||
|
||||
use super::{max_line_width, popup};
|
||||
|
||||
/// Draw info box in one of the 9 BoxLocations
|
||||
// TODO is this broken - I don't think so
|
||||
pub fn draw(
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
instant: &Instant,
|
||||
msg: String,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title("")
|
||||
.title_alignment(Alignment::Center)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_info.background)
|
||||
.fg(colors.popup_info.text),
|
||||
)
|
||||
.borders(Borders::NONE);
|
||||
|
||||
let max_line_width = max_line_width(&msg) + 8;
|
||||
let lines = msg.lines().count() + 2;
|
||||
|
||||
let paragraph = Paragraph::new(msg)
|
||||
.block(block)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_info.background)
|
||||
.fg(colors.popup_info.text),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let area = popup::draw(lines, max_line_width, f.area(), BoxLocation::BottomRight);
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(paragraph, area);
|
||||
if instant.elapsed().as_millis() > 4000 {
|
||||
gui_state.lock().reset_info_box();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use ratatui::style::Color;
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Info box drawn in bottom right
|
||||
fn test_draw_blocks_info() {
|
||||
let (w, h) = (45, 9);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" test ",
|
||||
" ",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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 (w, h) = (45, 9);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.popup_info.background = Color::Red;
|
||||
colors.popup_info.text = Color::Black;
|
||||
let expected = [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" test ",
|
||||
" ",
|
||||
];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&std::time::Instant::now(),
|
||||
"test".to_owned(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Modifier, Style},
|
||||
widgets::{List, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_data::AppData,
|
||||
config::AppColors,
|
||||
ui::{FrameData, GuiState, SelectablePanel, Status},
|
||||
};
|
||||
|
||||
use super::{generate_block, RIGHT_ARROW};
|
||||
|
||||
/// Draw the logs panel
|
||||
pub fn draw(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let block = generate_block(area, colors, fd, gui_state, SelectablePanel::Logs);
|
||||
if fd.status.contains(&Status::Init) {
|
||||
let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
|
||||
.style(Style::default())
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(paragraph, area);
|
||||
} else {
|
||||
let logs = app_data.lock().get_logs();
|
||||
if logs.is_empty() {
|
||||
let paragraph = Paragraph::new("no logs found")
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(paragraph, area);
|
||||
} else {
|
||||
let items = List::new(logs)
|
||||
.block(block)
|
||||
.highlight_symbol(RIGHT_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 ratatui::style::{Color, Modifier};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerImage, ContainerName},
|
||||
ui::{
|
||||
draw_blocks::tests::{
|
||||
expected_to_vec, get_result, insert_logs, test_setup, BORDER_CHARS,
|
||||
},
|
||||
FrameData, Status,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// No logs, panel unselected, then selected, border color changes correctly
|
||||
fn test_draw_blocks_logs_none() {
|
||||
let (w, h) = (35, 6);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let expected = [
|
||||
"╭ Logs - container_1 - image_1 ───╮",
|
||||
"│ no logs found │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰─────────────────────────────────╯",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
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().next_panel();
|
||||
setup.gui_state.lock().next_panel();
|
||||
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 (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
if BORDER_CHARS.contains(&result_cell.symbol()) {
|
||||
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Parsing logs, spinner visible, and then animates by one frame
|
||||
fn test_draw_blocks_logs_parsing() {
|
||||
let (w, h) = (32, 6);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let uuid = Uuid::new_v4();
|
||||
setup.gui_state.lock().next_loading(uuid);
|
||||
|
||||
let expected = [
|
||||
"╭ Logs - container_1 - image_1 ╮",
|
||||
"│ parsing logs ⠙ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰──────────────────────────────╯",
|
||||
];
|
||||
|
||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
fd.status.insert(Status::Init);
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animation moved by one frame
|
||||
setup.gui_state.lock().next_loading(uuid);
|
||||
|
||||
let expected = [
|
||||
"╭ Logs - container_1 - image_1 ╮",
|
||||
"│ parsing logs ⠹ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰──────────────────────────────╯",
|
||||
];
|
||||
|
||||
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,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
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() {
|
||||
let (w, h) = (36, 6);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
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();
|
||||
let expected = [
|
||||
"╭ Logs 3/3 - container_1 - image_1 ╮",
|
||||
"│ line 1 │",
|
||||
"│ line 2 │",
|
||||
"│▶ line 3 │",
|
||||
"│ │",
|
||||
"╰──────────────────────────────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Change selected log line
|
||||
setup.app_data.lock().log_previous();
|
||||
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();
|
||||
|
||||
let expected = [
|
||||
"╭ Logs 2/3 - container_1 - image_1 ╮",
|
||||
"│ line 1 │",
|
||||
"│▶ line 2 │",
|
||||
"│ line 3 │",
|
||||
"│ │",
|
||||
"╰──────────────────────────────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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 == 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 (w, h) = (80, 6);
|
||||
let mut setup = test_setup(w, h, 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 expected = [
|
||||
"╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test - a_long_image╮",
|
||||
"│ line 1 │",
|
||||
"│ line 2 │",
|
||||
"│▶ line 3 │",
|
||||
"│ │",
|
||||
"╰──────────────────────────────────────────────────────────────────────────────╯",
|
||||
];
|
||||
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
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::{gui_state::Region, FrameData, GuiState, SelectablePanel, Status};
|
||||
|
||||
pub mod charts;
|
||||
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 logs;
|
||||
pub mod popup;
|
||||
pub mod ports;
|
||||
|
||||
pub const NAME_TEXT: &str = r#"
|
||||
88
|
||||
88
|
||||
88
|
||||
,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba,
|
||||
a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8
|
||||
8b d8 )888( 8888[ 8PP""""""" 88
|
||||
"8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88
|
||||
`"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 "#;
|
||||
|
||||
pub const NAME: &str = env!("CARGO_PKG_NAME");
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||
pub const MARGIN: &str = " ";
|
||||
pub const RIGHT_ARROW: &str = "▶ ";
|
||||
pub const CIRCLE: &str = "⚪ ";
|
||||
|
||||
pub const CONSTRAINT_50_50: [Constraint; 2] =
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)];
|
||||
pub const CONSTRAINT_100: [Constraint; 1] = [Constraint::Percentage(100)];
|
||||
pub const CONSTRAINT_POPUP: [Constraint; 5] = [
|
||||
Constraint::Min(2),
|
||||
Constraint::Max(1),
|
||||
Constraint::Max(1),
|
||||
Constraint::Max(3),
|
||||
Constraint::Min(1),
|
||||
];
|
||||
|
||||
pub const CONSTRAINT_BUTTONS: [Constraint; 5] = [
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(35),
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(35),
|
||||
Constraint::Percentage(10),
|
||||
];
|
||||
|
||||
/// From a given &str, return the maximum number of chars on a single line
|
||||
pub fn max_line_width(text: &str) -> usize {
|
||||
text.lines()
|
||||
.map(|i| i.chars().count())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Generate block, add a border if is the selected panel,
|
||||
/// add custom title based on state of each panel
|
||||
fn generate_block<'a>(
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
panel: SelectablePanel,
|
||||
) -> Block<'a> {
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Panel(panel), area);
|
||||
|
||||
let mut title = match panel {
|
||||
SelectablePanel::Containers => {
|
||||
format!("{}{}", panel.title(), fd.container_title)
|
||||
}
|
||||
SelectablePanel::Logs => {
|
||||
format!("{}{}", panel.title(), fd.log_title)
|
||||
}
|
||||
SelectablePanel::Commands => String::new(),
|
||||
};
|
||||
if !title.is_empty() {
|
||||
title = format!(" {title} ");
|
||||
}
|
||||
let mut block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(title);
|
||||
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 parking_lot::Mutex;
|
||||
use ratatui::{backend::TestBackend, layout::Rect, style::Color, Terminal};
|
||||
|
||||
use crate::{
|
||||
app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts},
|
||||
tests::{gen_appdata, gen_containers},
|
||||
ui::{draw_frame, GuiState},
|
||||
};
|
||||
|
||||
use super::FrameData;
|
||||
|
||||
pub struct TuiTestSetup {
|
||||
pub app_data: Arc<Mutex<AppData>>,
|
||||
pub gui_state: Arc<Mutex<GuiState>>,
|
||||
pub fd: FrameData,
|
||||
pub area: Rect,
|
||||
pub terminal: Terminal<TestBackend>,
|
||||
pub ids: Vec<ContainerId>,
|
||||
}
|
||||
|
||||
pub const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"];
|
||||
pub const COLOR_RX: Color = Color::Rgb(255, 233, 193);
|
||||
pub const COLOR_TX: Color = Color::Rgb(205, 140, 140);
|
||||
pub const COLOR_ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||
|
||||
impl From<(&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)> for FrameData {
|
||||
fn from(data: (&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)) -> Self {
|
||||
let (app_data, gui_data) = (data.0.lock(), data.1.lock());
|
||||
|
||||
// set max height for container section, needs +5 to deal with docker commands list and borders
|
||||
let height = app_data.get_container_len();
|
||||
let height = if height < 12 {
|
||||
u16::try_from(height + 5).unwrap_or_default()
|
||||
} else {
|
||||
12
|
||||
};
|
||||
|
||||
let (filter_by, filter_term) = app_data.get_filter();
|
||||
Self {
|
||||
chart_data: app_data.get_chart_data(),
|
||||
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(),
|
||||
height,
|
||||
ports: app_data.get_selected_ports(),
|
||||
port_max_lens: app_data.get_longest_port(),
|
||||
info_text: gui_data.info_box_text.clone(),
|
||||
is_loading: gui_data.is_loading(),
|
||||
loading_icon: gui_data.get_loading().to_string(),
|
||||
log_title: app_data.get_log_title(),
|
||||
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 gui_state = GuiState::default();
|
||||
|
||||
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);
|
||||
TuiTestSetup {
|
||||
app_data,
|
||||
gui_state,
|
||||
fd,
|
||||
area,
|
||||
terminal,
|
||||
ids,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a single row of String's from the expected data
|
||||
pub fn expected_to_vec(expected: &[&str], row_index: usize) -> Vec<String> {
|
||||
expected[row_index]
|
||||
.chars()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn get_result(
|
||||
setup: &TuiTestSetup,
|
||||
w: u16,
|
||||
) -> std::iter::Enumerate<std::slice::Chunks<ratatui::buffer::Cell>> {
|
||||
setup
|
||||
.terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.chunks(usize::from(w))
|
||||
.enumerate()
|
||||
}
|
||||
|
||||
/// Insert some logs into the first container
|
||||
pub fn insert_logs(setup: &TuiTestSetup) {
|
||||
let logs = (1..=3).map(|i| format!("{i} line {i}")).collect::<Vec<_>>();
|
||||
setup.app_data.lock().update_log_by_id(logs, &setup.ids[0]);
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
// Add fixed data to the cpu & mem vecdeques
|
||||
pub fn insert_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 (w, h) = (160, 30);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
insert_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
|
||||
let expected = [
|
||||
" 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 │",
|
||||
"│ 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 │",
|
||||
"│ │•• ••• ││ │•• ••• ││127.0.0.1 8003 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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
/// Check that the whole layout is drawn correctly
|
||||
fn test_draw_blocks_whole_layout_with_filter() {
|
||||
let (w, h) = (160, 30);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
insert_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
|
||||
setup.app_data.lock().containers.items[1]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
|
||||
let expected = [
|
||||
" 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 │",
|
||||
"│ 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 │",
|
||||
"│ │•• ••• ││ │•• ••• ││ │",
|
||||
"│ │ ││ │ ││ │",
|
||||
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
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 expected = [
|
||||
" 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 │",
|
||||
"│ ││ 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 ",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 (w, h) = (190, 30);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
insert_chart_data(&setup);
|
||||
insert_logs(&setup);
|
||||
setup.app_data.lock().containers.items[0]
|
||||
.ports
|
||||
.push(ContainerPorts {
|
||||
ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
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 expected = [
|
||||
" 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 │",
|
||||
"│ 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 ───────────────────────────────╮╭────────── ports ───────────╮",
|
||||
"│10.00%│ •••• ││100.00 kB│ ••••• ││ ip private public│",
|
||||
"│ │ •••• • ││ │ ••• • ││ 8001 │",
|
||||
"│ │••• •••• ││ │••• ••• ││127.0.0.1 8003 8003│",
|
||||
"│ │ ││ │ ││ │",
|
||||
"╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
||||
];
|
||||
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();
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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]
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
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))
|
||||
// .bg(colors.chart_ports.border))
|
||||
.title_alignment(Alignment::Center)
|
||||
.title(Span::styled(
|
||||
" ports ",
|
||||
Style::default()
|
||||
.fg(get_port_title_color(colors, ports.1))
|
||||
.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);
|
||||
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);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerPorts, State},
|
||||
ui::{
|
||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
FrameData,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// Port section when container has no ports
|
||||
fn test_draw_blocks_ports_no_ports() {
|
||||
let (w, h) = (30, 8);
|
||||
let mut setup = test_setup(w, h, 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();
|
||||
|
||||
let expected = [
|
||||
"╭────────── ports ───────────╮",
|
||||
"│ no ports │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰────────────────────────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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]);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When state is "State::Running | State::Paused | State::Restarting, won't show "no ports"
|
||||
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();
|
||||
|
||||
let expected = [
|
||||
"╭────────── ports ───────────╮",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰────────────────────────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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 (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 (w, h) = (32, 8);
|
||||
let mut setup = test_setup(w, h, 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::new(127, 0, 0, 1))),
|
||||
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();
|
||||
|
||||
let expected = [
|
||||
"╭─────────── ports ────────────╮",
|
||||
"│ ip private public │",
|
||||
"│ 8001 │",
|
||||
"│ 8002 │",
|
||||
"│127.0.0.1 8003 8003 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰──────────────────────────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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);
|
||||
|
||||
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 (w, h) = (32, 8);
|
||||
let mut setup = test_setup(w, h, 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();
|
||||
|
||||
let expected = [
|
||||
"╭─────────── ports ────────────╮",
|
||||
"│ ip private public │",
|
||||
"│ 8001 │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰──────────────────────────────╯",
|
||||
];
|
||||
|
||||
for (row_index, result_row) in get_result(&setup, w) {
|
||||
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 (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, w) {
|
||||
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 (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, w) {
|
||||
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 (0, 12..=18) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-23
@@ -49,13 +49,14 @@ impl SelectablePanel {
|
||||
pub enum Region {
|
||||
Panel(SelectablePanel),
|
||||
Header(Header),
|
||||
HelpPanel,
|
||||
Delete(DeleteButton),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum DeleteButton {
|
||||
Yes,
|
||||
No,
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -173,21 +174,22 @@ pub enum Status {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GuiState {
|
||||
delete_container: Option<ContainerId>,
|
||||
delete_map: HashMap<DeleteButton, Rect>,
|
||||
heading_map: HashMap<Header, Rect>,
|
||||
exec_mode: Option<ExecMode>,
|
||||
loading_handle: Option<JoinHandle<()>>,
|
||||
loading_index: u8,
|
||||
loading_set: HashSet<Uuid>,
|
||||
panel_map: HashMap<SelectablePanel, Rect>,
|
||||
intersect_delete: HashMap<DeleteButton, Rect>,
|
||||
intersect_heading: HashMap<Header, Rect>,
|
||||
intersect_help: Option<Rect>,
|
||||
intersect_panel: HashMap<SelectablePanel, Rect>,
|
||||
selected_panel: SelectablePanel,
|
||||
status: HashSet<Status>,
|
||||
exec_mode: Option<ExecMode>,
|
||||
pub info_box_text: Option<(String, Instant)>,
|
||||
}
|
||||
impl GuiState {
|
||||
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||
pub fn clear_area_map(&mut self) {
|
||||
self.panel_map.clear();
|
||||
self.intersect_panel.clear();
|
||||
}
|
||||
|
||||
/// Get the currently selected panel
|
||||
@@ -196,9 +198,9 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||
pub fn panel_intersect(&mut self, rect: Rect) {
|
||||
pub fn get_intersect_panel(&mut self, rect: Rect) {
|
||||
if let Some(data) = self
|
||||
.panel_map
|
||||
.intersect_panel
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -209,8 +211,8 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button
|
||||
pub fn button_intersect(&self, rect: Rect) -> Option<DeleteButton> {
|
||||
self.delete_map
|
||||
pub fn get_intersect_button(&self, rect: Rect) -> Option<DeleteButton> {
|
||||
self.intersect_delete
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -219,8 +221,8 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||
pub fn header_intersect(&self, rect: Rect) -> Option<Header> {
|
||||
self.heading_map
|
||||
pub fn get_intersect_header(&self, rect: Rect) -> Option<Header> {
|
||||
self.intersect_heading
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -228,24 +230,37 @@ impl GuiState {
|
||||
.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
|
||||
.heading_map
|
||||
Region::Header(header) => {
|
||||
self.intersect_heading
|
||||
.entry(header)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area),
|
||||
Region::Panel(panel) => self
|
||||
.panel_map
|
||||
.or_insert(area);
|
||||
}
|
||||
Region::Panel(panel) => {
|
||||
self.intersect_panel
|
||||
.entry(panel)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area),
|
||||
Region::Delete(button) => self
|
||||
.delete_map
|
||||
.or_insert(area);
|
||||
}
|
||||
Region::Delete(button) => {
|
||||
self.intersect_delete
|
||||
.entry(button)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area),
|
||||
.or_insert(area);
|
||||
}
|
||||
Region::HelpPanel => {
|
||||
self.intersect_help = Some(area);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -260,7 +275,7 @@ impl GuiState {
|
||||
if id.is_some() {
|
||||
self.status.insert(Status::DeleteConfirm);
|
||||
} else {
|
||||
self.delete_map.clear();
|
||||
self.intersect_delete.clear();
|
||||
self.status.remove(&Status::DeleteConfirm);
|
||||
}
|
||||
self.delete_container = id;
|
||||
|
||||
+36
-17
@@ -32,11 +32,11 @@ use crate::{
|
||||
SortedOrder, State,
|
||||
},
|
||||
app_error::AppError,
|
||||
config::{AppColors, Keymap},
|
||||
exec::TerminalSize,
|
||||
input_handler::InputMessages,
|
||||
};
|
||||
|
||||
pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36);
|
||||
const POLL_RATE: Duration = std::time::Duration::from_millis(100);
|
||||
|
||||
pub struct Ui {
|
||||
@@ -124,6 +124,8 @@ impl Ui {
|
||||
/// Draw the the error message ui, for 5 seconds, with a countdown
|
||||
fn err_loop(&mut self) -> Result<(), AppError> {
|
||||
let mut seconds = 5;
|
||||
let colors = self.app_data.lock().config.app_colors;
|
||||
let keymap = self.app_data.lock().config.keymap.clone();
|
||||
loop {
|
||||
if self.now.elapsed() >= std::time::Duration::from_secs(1) {
|
||||
seconds -= 1;
|
||||
@@ -135,7 +137,15 @@ impl Ui {
|
||||
|
||||
if self
|
||||
.terminal
|
||||
.draw(|f| draw_blocks::error(f, AppError::DockerConnect, Some(seconds)))
|
||||
.draw(|f| {
|
||||
draw_blocks::error::draw(
|
||||
f,
|
||||
&AppError::DockerConnect,
|
||||
&keymap,
|
||||
Some(seconds),
|
||||
colors,
|
||||
);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return Err(AppError::Terminal);
|
||||
@@ -165,6 +175,8 @@ impl Ui {
|
||||
|
||||
/// 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();
|
||||
while self.is_running.load(Ordering::SeqCst) {
|
||||
let fd = FrameData::from(&*self);
|
||||
let exec = fd.status.contains(&Status::Exec);
|
||||
@@ -174,7 +186,9 @@ impl Ui {
|
||||
|
||||
if self
|
||||
.terminal
|
||||
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state, &fd))
|
||||
.draw(|frame| {
|
||||
draw_frame(&self.app_data, colors, &keymap, frame, &fd, &self.gui_state);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return Err(AppError::Terminal);
|
||||
@@ -223,6 +237,7 @@ impl Ui {
|
||||
/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FrameData {
|
||||
// app_colors: AppColors,
|
||||
chart_data: Option<(CpuTuple, MemTuple)>,
|
||||
columns: Columns,
|
||||
container_title: String,
|
||||
@@ -257,6 +272,7 @@ impl From<&Ui> for FrameData {
|
||||
|
||||
let (filter_by, filter_term) = app_data.get_filter();
|
||||
Self {
|
||||
// app_colors: app_data.config.app_colors,
|
||||
chart_data: app_data.get_chart_data(),
|
||||
columns: app_data.get_width(),
|
||||
container_title: app_data.get_container_title(),
|
||||
@@ -281,10 +297,13 @@ impl From<&Ui> for FrameData {
|
||||
|
||||
/// Draw the main ui to a frame of the terminal
|
||||
fn draw_frame(
|
||||
f: &mut Frame,
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
colors: AppColors,
|
||||
keymap: &Keymap,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
// should pass in the colors here, then I only need to get it once from app+data
|
||||
) {
|
||||
let whole_constraints = if fd.status.contains(&Status::Filter) {
|
||||
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
|
||||
@@ -326,15 +345,15 @@ fn draw_frame(
|
||||
.constraints(lower_split)
|
||||
.split(upper_main[1]);
|
||||
|
||||
draw_blocks::containers(app_data, top_panel[0], f, fd, gui_state);
|
||||
draw_blocks::containers::draw(app_data, top_panel[0], colors, f, fd, gui_state);
|
||||
|
||||
draw_blocks::logs(app_data, lower_main[0], f, fd, gui_state);
|
||||
draw_blocks::logs::draw(app_data, lower_main[0], colors, f, fd, gui_state);
|
||||
|
||||
draw_blocks::heading_bar(whole_layout[0], f, fd, gui_state);
|
||||
draw_blocks::headers::draw(whole_layout[0], colors, f, fd, gui_state, keymap);
|
||||
|
||||
// Draw filter bar
|
||||
if let Some(rect) = whole_layout.get(2) {
|
||||
draw_blocks::filter_bar(*rect, f, fd);
|
||||
draw_blocks::filter::draw(*rect, f, fd);
|
||||
}
|
||||
|
||||
if let Some(id) = fd.delete_confirm.as_ref() {
|
||||
@@ -345,14 +364,14 @@ fn draw_frame(
|
||||
gui_state.lock().set_delete_container(None);
|
||||
},
|
||||
|name| {
|
||||
draw_blocks::delete_confirm(f, gui_state, name);
|
||||
draw_blocks::delete_confirm::draw(colors, f, gui_state, keymap, name);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// only draw commands + charts if there are containers
|
||||
if let Some(rect) = top_panel.get(1) {
|
||||
draw_blocks::commands(app_data, *rect, f, fd, gui_state);
|
||||
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 =
|
||||
@@ -364,20 +383,20 @@ fn draw_frame(
|
||||
.constraints([Constraint::Min(1), Constraint::Max(ports_len)])
|
||||
.split(lower_main[1]);
|
||||
|
||||
draw_blocks::chart(f, lower[0], fd);
|
||||
draw_blocks::ports(f, lower[1], fd);
|
||||
draw_blocks::charts::draw(lower[0], colors, f, fd);
|
||||
draw_blocks::ports::draw(lower[1], colors, f, fd);
|
||||
}
|
||||
|
||||
if let Some((text, instant)) = fd.info_text.as_ref() {
|
||||
draw_blocks::info(f, text.to_owned(), instant, gui_state);
|
||||
draw_blocks::info::draw(colors, f, gui_state, instant, text.to_owned());
|
||||
}
|
||||
|
||||
// Check if error, and show popup if so
|
||||
if fd.status.contains(&Status::Help) {
|
||||
draw_blocks::help_box(f);
|
||||
draw_blocks::help::draw(f, colors, keymap);
|
||||
}
|
||||
|
||||
if let Some(error) = fd.has_error {
|
||||
draw_blocks::error(f, error, None);
|
||||
if let Some(error) = fd.has_error.as_ref() {
|
||||
draw_blocks::error::draw(f, error, keymap, None, colors);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user