chore: merge release-v0.9.0 into main

This commit is contained in:
Jack Wills
2024-12-05 20:46:28 +00:00
19 changed files with 1559 additions and 1102 deletions
+34 -6
View File
@@ -1,14 +1,42 @@
### 2024-10-22
### 2024-12-05
### Chores
+ dependencies updated, [ea877d23711b98ffd1108a74206d93d43482d44d], [af609c0dbf0caab4a073f822166de34999afb41b]
+ .devcontainer updated, [a9844436d003b84a3e9d8b600ea029b232566f3a]
+ create_release.sh updated, [c4943370f4a67f6c01c75a8a7f825912427666a2], [1389d8adbba75fef480eb1de09337eb7beb10ba3]
+ dependencies updated, [b78713579c4706d605e5b35fcd832610a0152294], [c6200e8f77f8bb1f0152cb9374029d15cc45df9d]
+ Rust 1.83 linting, [751d997a3dac823e144ae62e6c1455676e50ddb8]
### Features
+ Add Stderr output to logs, thanks [vincentmasse](https://github.com/vincentmasse), closes #48, merges #49, [b95c9311416cd0dbcfa5de90c23f3065bc2d6b17], [9936ad45e186ee431aade920674a2dc283937355], [289ede3f2531feeec56094a76bf34f4c69431bbe]
+ `--no-stderr` cli arg, removes Standard error output from logs, closes #52, [c739637b91c8fa742a69f4d888678d7b3964678c]
+ ContainerPorts use ipaddr, [1b26997d25f748e0d452f41fe41791533046ecdf]
### Fixes
+ update containerised Dockerfile, [0c6f53228f01196e352c2069383ba1e7a10950a8]
+ calculate_usage overflow, [5106a01f3dcb87ce5a8f1fb7bf49dc6b3c25d03e]
+ DockerData spawns insertion error, [d4906d33c26b75d92e7d80040c488faa90a257c6]
### Refactors
+ Rust 1.82 linting, [c058c5a301cfd4e8d7a0079c4c3f8fdeae2803e5]
+ speed up docker logs init process, [8b9fe4246865441704ae12dff0938868a4fe6f81]
+ remove docker sleep, [f1562d1084336fe5be39894c93cb49107f0a4a6d]
+ dead code removed, [5ee48d5708fa6de0206c021db0bb611196e66fba], [ba6a95241389f99d504ee4bf3e87e19006f12e49], [f0b1145651625ad4e577d79baaf902d4d3bc0579]
+ input_handler, [7f4238349525c01ae9fb8b1f6c0946e5364dd55e]
+ statefulList get_state_title, [2d540b0e2210cc04d73035ec59211ffc739174f6]
+ statefulList next/previous, [7bb2bef28d90ebc58da86a0365a1904a0c32dffe]
+ help_box closure fn, [2860426d57a4458fcee49a2fd20e8e7bb9e71fb5]
+ use check_sub for sleep calculations, [fe3696e5576739d8b033d9e748b5ea696c4b4e4f]
+ rename scheduler to heartbeat, [68a6551ed038a36330b2f098112829465a1c3c7a]
+ remove unnecessary is_running load, [76ccf7c00691f815c3ab0bede838c99252ba84f0]
+ execute_command(), [2a834d6c2fa4a15124d24ddbd12f667829e148ad]
+ Remove numerous clones(), [e5927f781a7e9517b9fa00a2d1a835d2774a9d26]
+ remove app_data param from generate_lock(), [1a8dab654a1fdbf351a72dc54fe3d1943355bba6]
+ combine get_filter methods, [356ea5549bb4877e9893fe0e1053e73c5a62e806]
+ FrameData refactors, [57781701ff14c553dfbafb965ee8a33ab44dd36f], [6e2f82db81caaa98ce4781fa15928eb9e246ace6]
+ update_container_stat combine is_alive(), [55cc746736f6863aedc5ad838744a983796244d8]
+ remove `input_poll_rate` from `Ui`, instead use const `POLL_RATE`, [69f6c96b700b9fde5578ae204992a67986d456ab]
+ pass `&FrameDate` into `draw_frame()`, [35aec5060fdbe606267be26656b4aeee43d50c02]
+ dead code removed, [caf23be4a7faff99aaca80b081a02e4e0a372009]
+ input_handler, [9c4f8910381b90b563da12eaba4b79cb60c40129]
+ draw_block, [de76bc22936b124dcb9646f302f6cc14691dbb63]
### Tests
+ fix logs tests, [9b22f5da18e4bf92766a68a7f4cd61ad72724cfd]
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
+42
View File
@@ -1,3 +1,45 @@
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.9.0'>v0.9.0</a>
### 2024-12-05
### Chores
+ dependencies updated, [b7871357](https://github.com/mrjackwills/oxker/commit/b78713579c4706d605e5b35fcd832610a0152294), [c6200e8f](https://github.com/mrjackwills/oxker/commit/c6200e8f77f8bb1f0152cb9374029d15cc45df9d)
+ Rust 1.83 linting, [751d997a](https://github.com/mrjackwills/oxker/commit/751d997a3dac823e144ae62e6c1455676e50ddb8)
### Features
+ `--no-stderr` cli arg, removes Standard error output from logs, closes [#52](https://github.com/mrjackwills/oxker/issues/52), [c739637b](https://github.com/mrjackwills/oxker/commit/c739637b91c8fa742a69f4d888678d7b3964678c)
+ ContainerPorts use ipaddr, [1b26997d](https://github.com/mrjackwills/oxker/commit/1b26997d25f748e0d452f41fe41791533046ecdf)
### Fixes
+ update containerised Dockerfile, [0c6f5322](https://github.com/mrjackwills/oxker/commit/0c6f53228f01196e352c2069383ba1e7a10950a8)
+ calculate_usage overflow, [5106a01f](https://github.com/mrjackwills/oxker/commit/5106a01f3dcb87ce5a8f1fb7bf49dc6b3c25d03e)
+ DockerData spawns insertion error, [d4906d33](https://github.com/mrjackwills/oxker/commit/d4906d33c26b75d92e7d80040c488faa90a257c6)
### Refactors
+ speed up docker logs init process, [8b9fe424](https://github.com/mrjackwills/oxker/commit/8b9fe4246865441704ae12dff0938868a4fe6f81)
+ remove docker sleep, [f1562d10](https://github.com/mrjackwills/oxker/commit/f1562d1084336fe5be39894c93cb49107f0a4a6d)
+ dead code removed, [5ee48d57](https://github.com/mrjackwills/oxker/commit/5ee48d5708fa6de0206c021db0bb611196e66fba), [ba6a9524](https://github.com/mrjackwills/oxker/commit/ba6a95241389f99d504ee4bf3e87e19006f12e49), [f0b11456](https://github.com/mrjackwills/oxker/commit/f0b1145651625ad4e577d79baaf902d4d3bc0579)
+ input_handler, [7f423834](https://github.com/mrjackwills/oxker/commit/7f4238349525c01ae9fb8b1f6c0946e5364dd55e)
+ statefulList get_state_title, [2d540b0e](https://github.com/mrjackwills/oxker/commit/2d540b0e2210cc04d73035ec59211ffc739174f6)
+ statefulList next/previous, [7bb2bef2](https://github.com/mrjackwills/oxker/commit/7bb2bef28d90ebc58da86a0365a1904a0c32dffe)
+ help_box closure fn, [2860426d](https://github.com/mrjackwills/oxker/commit/2860426d57a4458fcee49a2fd20e8e7bb9e71fb5)
+ use check_sub for sleep calculations, [fe3696e5](https://github.com/mrjackwills/oxker/commit/fe3696e5576739d8b033d9e748b5ea696c4b4e4f)
+ rename scheduler to heartbeat, [68a6551e](https://github.com/mrjackwills/oxker/commit/68a6551ed038a36330b2f098112829465a1c3c7a)
+ remove unnecessary is_running load, [76ccf7c0](https://github.com/mrjackwills/oxker/commit/76ccf7c00691f815c3ab0bede838c99252ba84f0)
+ execute_command(), [2a834d6c](https://github.com/mrjackwills/oxker/commit/2a834d6c2fa4a15124d24ddbd12f667829e148ad)
+ Remove numerous clones(), [e5927f78](https://github.com/mrjackwills/oxker/commit/e5927f781a7e9517b9fa00a2d1a835d2774a9d26)
+ remove app_data param from generate_lock(), [1a8dab65](https://github.com/mrjackwills/oxker/commit/1a8dab654a1fdbf351a72dc54fe3d1943355bba6)
+ combine get_filter methods, [356ea554](https://github.com/mrjackwills/oxker/commit/356ea5549bb4877e9893fe0e1053e73c5a62e806)
+ FrameData refactors, [57781701](https://github.com/mrjackwills/oxker/commit/57781701ff14c553dfbafb965ee8a33ab44dd36f), [6e2f82db](https://github.com/mrjackwills/oxker/commit/6e2f82db81caaa98ce4781fa15928eb9e246ace6)
+ update_container_stat combine is_alive(), [55cc7467](https://github.com/mrjackwills/oxker/commit/55cc746736f6863aedc5ad838744a983796244d8)
+ remove `input_poll_rate` from `Ui`, instead use const `POLL_RATE`, [69f6c96b](https://github.com/mrjackwills/oxker/commit/69f6c96b700b9fde5578ae204992a67986d456ab)
+ pass `&FrameDate` into `draw_frame()`, [35aec506](https://github.com/mrjackwills/oxker/commit/35aec5060fdbe606267be26656b4aeee43d50c02)
+ dead code removed, [caf23be4](https://github.com/mrjackwills/oxker/commit/caf23be4a7faff99aaca80b081a02e4e0a372009)
+ input_handler, [9c4f8910](https://github.com/mrjackwills/oxker/commit/9c4f8910381b90b563da12eaba4b79cb60c40129)
+ draw_block, [de76bc22](https://github.com/mrjackwills/oxker/commit/de76bc22936b124dcb9646f302f6cc14691dbb63)
### Tests
+ fix logs tests, [9b22f5da](https://github.com/mrjackwills/oxker/commit/9b22f5da18e4bf92766a68a7f4cd61ad72724cfd)
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.8.0'>v0.8.0</a>
### 2024-10-22
Generated
+476 -152
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "oxker"
version = "0.8.0"
version = "0.9.0"
edition = "2021"
authors = ["Jack Wills <email@mrjackwills.com>"]
description = "A simple tui to view & control docker containers"
@@ -27,7 +27,7 @@ similar_names = "allow"
[dependencies]
anyhow = "1.0"
bollard = "0.17"
bollard = "0.18"
cansi = "2.2"
clap = { version = "4.5", features = ["color", "derive", "unicode"] }
crossterm = "0.28"
@@ -35,7 +35,7 @@ directories = "5.0"
futures-util = "0.3"
parking_lot = { version = "0.12" }
ratatui = "0.29"
tokio = { version = "1.41", features = ["full"] }
tokio = { version = "1.42", features = ["full"] }
tokio-util = "0.7"
tracing = "0.1"
tracing-subscriber = "0.3"
+1
View File
@@ -127,6 +127,7 @@ Available command line arguments
|```-s```| If running via Docker, will display the oxker container.|
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|```--host [string]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.|
|```--no-stderr```| Do not include stderr output in logs.|
|```--save-dir [string]```| Save exported logs into a custom directory. Defaults to `$HOME`.|
|```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.|
+7 -7
View File
@@ -2,7 +2,7 @@
## Builder ##
#############
FROM --platform=linux/amd64 rust:slim AS builder
FROM --platform=$BUILDPLATFORM rust:slim AS builder
ARG TARGETARCH
@@ -11,9 +11,9 @@ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="aarch64-linux-gnu-gcc"
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-lgcc"
ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_LINKER="arm-linux-gnueabihf-ld"
COPY ./containerised/platform.sh .
COPY ./containerised/target.sh .
RUN chmod +x ./platform.sh && ./platform.sh
RUN chmod +x ./target.sh && ./target.sh
RUN apt-get update && apt-get install $(cat /.compiler) -y
@@ -29,10 +29,10 @@ COPY Cargo.* /usr/src/oxker/
WORKDIR /usr/src/oxker
# Install target platform (Cross-Compilation)
RUN rustup target add $(cat /.platform)
RUN rustup target add $(cat /.target)
# This is a dummy build to get the dependencies cached - probably not needed - as run via a github action
RUN cargo build --target $(cat /.platform) --release
RUN cargo build --target $(cat /.target) --release
# Now copy in the rest of the sources
COPY src /usr/src/oxker/src/
@@ -41,9 +41,9 @@ COPY src /usr/src/oxker/src/
RUN touch /usr/src/oxker/src/main.rs
# This is the actual application build
RUN cargo build --release --target $(cat /.platform)
RUN cargo build --release --target $(cat /.target)
RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
RUN cp /usr/src/oxker/target/$(cat /.target)/release/oxker /
#############
## Runtime ##
@@ -4,15 +4,15 @@
case $TARGETARCH in
"amd64")
echo "x86_64-unknown-linux-musl" >/.platform
echo "x86_64-unknown-linux-musl" >/.target
echo "" >/.compiler
;;
"arm64")
echo "aarch64-unknown-linux-musl" >/.platform
echo "aarch64-unknown-linux-musl" >/.target
echo "gcc-aarch64-linux-gnu" >/.compiler
;;
"arm")
echo "arm-unknown-linux-musleabihf" >/.platform
echo "arm-unknown-linux-musleabihf" >/.target
echo "gcc-arm-linux-gnueabihf" >/.compiler
;;
esac
+35 -30
View File
@@ -2,6 +2,7 @@ use std::{
cmp::Ordering,
collections::{HashSet, VecDeque},
fmt,
net::IpAddr,
};
use bollard::service::Port;
@@ -103,17 +104,17 @@ macro_rules! unit_struct {
unit_struct!(ContainerName);
unit_struct!(ContainerImage);
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContainerPorts {
pub ip: Option<String>,
pub ip: Option<IpAddr>,
pub private: u16,
pub public: Option<u16>,
}
impl From<&Port> for ContainerPorts {
fn from(value: &Port) -> Self {
impl From<Port> for ContainerPorts {
fn from(value: Port) -> Self {
Self {
ip: value.ip.clone(),
ip: value.ip.and_then(|i| i.parse::<IpAddr>().ok()),
private: value.private_port,
public: value.public_port,
}
@@ -122,7 +123,9 @@ impl From<&Port> for ContainerPorts {
impl ContainerPorts {
pub fn len_ip(&self) -> usize {
self.ip.as_ref().unwrap_or(&String::new()).chars().count()
self.ip
.as_ref()
.map_or(0, |i| i.to_string().chars().count())
}
pub fn len_private(&self) -> usize {
format!("{}", self.private).chars().count()
@@ -133,11 +136,12 @@ impl ContainerPorts {
.count()
}
pub fn print(&self) -> (String, String, String) {
/// Return as tuple of Strings, ip address, private port, and public port
pub fn get_all(&self) -> (String, String, String) {
(
self.ip
.as_ref()
.map_or(String::new(), std::borrow::ToOwned::to_owned),
.map_or(String::new(), std::string::ToString::to_string),
format!("{}", self.private),
self.public.map_or(String::new(), |s| s.to_string()),
)
@@ -171,27 +175,25 @@ impl<T> StatefulList<T> {
pub fn next(&mut self) {
if !self.items.is_empty() {
let i = match self.state.selected() {
Some(i) => {
if i < self.items.len() - 1 {
i + 1
} else {
i
}
self.state.select(Some(self.state.selected().map_or(0, |i| {
if i < self.items.len() - 1 {
i + 1
} else {
i
}
None => 0,
};
self.state.select(Some(i));
})));
}
}
pub fn previous(&mut self) {
if !self.items.is_empty() {
let i = self
.state
.selected()
.map_or(0, |i| if i == 0 { 0 } else { i - 1 });
self.state.select(Some(i));
self.state.select(Some(self.state.selected().map_or(0, |i| {
if i == 0 {
0
} else {
i - 1
}
})));
}
}
@@ -201,11 +203,11 @@ impl<T> StatefulList<T> {
String::new()
} else {
let len = self.items.len();
let c = self
let count = self
.state
.selected()
.map_or(0, |value| if len > 0 { value + 1 } else { value });
format!(" {c}/{}", self.items.len())
format!(" {count}/{len}")
}
}
}
@@ -259,9 +261,12 @@ pub enum State {
}
impl State {
/// The container is alive if the start is Running, either healthy or unhealthy
pub const fn is_alive(self) -> bool {
matches!(self, Self::Running(_))
}
/// Color of the state for the containers section
/// TODO allow usable editable colours
pub const fn get_color(self) -> Color {
match self {
Self::Paused => Color::Yellow,
@@ -333,7 +338,7 @@ impl fmt::Display for State {
/// Items for the container control list
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DockerControls {
pub enum DockerCommand {
Pause,
Restart,
Start,
@@ -342,7 +347,7 @@ pub enum DockerControls {
Delete,
}
impl DockerControls {
impl DockerCommand {
pub const fn get_color(self) -> Color {
match self {
Self::Pause => Color::Yellow,
@@ -366,7 +371,7 @@ impl DockerControls {
}
}
impl fmt::Display for DockerControls {
impl fmt::Display for DockerCommand {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = match self {
Self::Pause => "pause",
@@ -577,7 +582,7 @@ impl Logs {
pub struct ContainerItem {
pub cpu_stats: VecDeque<CpuStats>,
pub created: u64,
pub docker_controls: StatefulList<DockerControls>,
pub docker_controls: StatefulList<DockerCommand>,
pub id: ContainerId,
pub image: ContainerImage,
pub is_oxker: bool,
@@ -620,7 +625,7 @@ impl ContainerItem {
state: State,
status: ContainerStatus,
) -> Self {
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
let mut docker_controls = StatefulList::new(DockerCommand::gen_vec(state));
docker_controls.start();
Self {
+95 -97
View File
@@ -143,10 +143,10 @@ impl AppData {
Self {
args,
containers: StatefulList::new(vec![]),
hidden_containers: vec![],
error: None,
sorted_by: None,
filter: Filter::new(),
hidden_containers: vec![],
sorted_by: None,
}
}
@@ -160,15 +160,9 @@ impl AppData {
}
/// Filter related methods
/// Get the current filter term
pub const fn get_filter_term(&self) -> Option<&String> {
self.filter.term.as_ref()
}
/// Get the current filter by choice
pub const fn get_filter_by(&self) -> FilterBy {
self.filter.by
/// Get the filterby and filter_term
pub const fn get_filter(&self) -> (FilterBy, Option<&String>) {
(self.filter.by, self.filter.term.as_ref())
}
/// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by
@@ -252,7 +246,7 @@ impl AppData {
self.filter_containers();
}
// change the filter_by option
/// change the filter_by option
pub fn filter_by_next(&mut self) {
if let Some(by) = self.filter.by.next() {
self.filter.by = by;
@@ -260,7 +254,7 @@ impl AppData {
}
}
// change the filter_by option
/// change the filter_by option
pub fn filter_by_prev(&mut self) {
if let Some(by) = self.filter.by.prev() {
self.filter.by = by;
@@ -280,7 +274,6 @@ impl AppData {
}
/// Container sort related methods
/// Change the sorted order, also set the selected container state to match new order
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
self.sorted_by = x;
@@ -350,7 +343,6 @@ impl AppData {
.back()
.cmp(&item_ord.1.mem_stats.back())
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
Header::Id => item_ord
.0
.id
@@ -372,7 +364,6 @@ impl AppData {
.tx
.cmp(&item_ord.1.tx)
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
Header::Name => item_ord
.0
.name
@@ -392,19 +383,26 @@ impl AppData {
}
/// Container state methods
/// Get the total number of none "hidden" containers
pub fn get_container_len(&self) -> usize {
self.containers.items.len()
}
pub fn get_all_id_state(&self) -> Vec<(State, ContainerId)> {
self.containers
.items
.iter()
.map(|i| (i.state, i.id.clone()))
.collect::<Vec<_>>()
}
/// Get all the ContainerItems
pub fn get_container_items(&self) -> &[ContainerItem] {
&self.containers.items
}
/// Get title for containers section, add a suffix indicating if the containers are currently under filter
pub fn container_title(&self) -> String {
pub fn get_container_title(&self) -> String {
let suffix = if !self.hidden_containers.is_empty() && !self.containers.items.is_empty() {
" - filtered"
} else {
@@ -447,43 +445,41 @@ impl AppData {
}
/// Find the longest port when it's transformed into a string, defaults are header lens (ip, private, public)
///display like this: "│ ip, private, public│", so (5,10,9) are the minimum lengths required
pub fn get_longest_port(&self) -> (usize, usize, usize) {
let mut longest_ip = 5;
let mut longest_private = 10;
let mut longest_public = 9;
let mut output = (5, 10, 9);
for item in [&self.containers.items, &self.hidden_containers] {
for item in item {
longest_ip = longest_ip.max(
output.0 = output.0.max(
item.ports
.iter()
.map(ContainerPorts::len_ip)
.max()
.unwrap_or(3),
.unwrap_or(output.0),
);
longest_private = longest_private.max(
output.1 = output.1.max(
item.ports
.iter()
.map(ContainerPorts::len_private)
.max()
.unwrap_or(8),
.unwrap_or(output.1),
);
longest_public = longest_public.max(
output.2 = output.2.max(
item.ports
.iter()
.map(ContainerPorts::len_public)
.max()
.unwrap_or(6),
.unwrap_or(output.2),
);
}
}
(longest_ip, longest_private, longest_public)
output
}
/// Get Option of the current selected container's ports, sorted by private port
pub fn get_selected_ports(&mut self) -> Option<(Vec<ContainerPorts>, State)> {
if let Some(item) = self.get_mut_selected_container() {
pub fn get_selected_ports(&self) -> Option<(Vec<ContainerPorts>, State)> {
if let Some(item) = self.get_selected_container() {
let mut ports = item.ports.clone();
ports.sort_by(|a, b| a.private.cmp(&b.private));
return Some((ports, item.state));
@@ -510,12 +506,12 @@ impl AppData {
}
/// Get the ContainerName of by ID
pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<ContainerName> {
pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<&ContainerName> {
self.containers
.items
.iter_mut()
.find(|i| &i.id == id)
.map(|i| i.name.clone())
.map(|i| &i.name)
}
/// Find the id of the currently selected container.
@@ -532,10 +528,9 @@ impl AppData {
}
/// Selected DockerCommand methods
/// Get the current selected docker command
/// So know which command to execute
pub fn selected_docker_controls(&self) -> Option<DockerControls> {
pub fn selected_docker_controls(&self) -> Option<DockerCommand> {
self.get_selected_container().and_then(|i| {
i.docker_controls.state.selected().and_then(|x| {
i.docker_controls
@@ -574,21 +569,19 @@ impl AppData {
}
}
/// Get mutable Option of the currently selected container DockerControls state
/// Get mutable Option of the currently selected container DockerCommand state
pub fn get_control_state(&mut self) -> Option<&mut ListState> {
self.get_mut_selected_container()
.map(|i| &mut i.docker_controls.state)
}
/// Get mutable Option of the currently selected container DockerControls items
/// TODO command or control, need a uniform name across the application
pub fn get_control_items(&mut self) -> Option<&mut Vec<DockerControls>> {
/// Get mutable Option of the currently selected container DockerConmand items
pub fn get_control_items(&mut self) -> Option<&mut Vec<DockerCommand>> {
self.get_mut_selected_container()
.map(|i| &mut i.docker_controls.items)
}
/// Logs related methods
/// Get the title for log panel for selected container, will be either
/// 1) "logs x/x - container_name - container_image"
/// 2) "logs - container_name - container_image" when no logs found
@@ -635,11 +628,11 @@ impl AppData {
}
/// Get mutable Vec of current containers logs
pub fn get_logs(&mut self) -> Vec<ListItem<'static>> {
pub fn get_logs(&self) -> Vec<ListItem<'static>> {
self.containers
.state
.selected()
.and_then(|i| self.containers.items.get_mut(i))
.and_then(|i| self.containers.items.get(i))
.map_or(vec![], |i| i.logs.to_vec())
}
@@ -653,18 +646,16 @@ impl AppData {
}
/// Chart data related methods
/// Get mutable Option of the currently selected container chart data
pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> {
pub fn get_chart_data(&self) -> Option<(CpuTuple, MemTuple)> {
self.containers
.state
.selected()
.and_then(|i| self.containers.items.get_mut(i))
.map(|i| i.get_chart_data())
.and_then(|i| self.containers.items.get(i))
.map(container_state::ContainerItem::get_chart_data)
}
/// Error related methods
/// Get single app_state error
pub const fn get_error(&self) -> Option<AppError> {
self.error
@@ -701,7 +692,6 @@ impl AppData {
let mut columns = Columns::new();
let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12);
// Should probably find a refactor here somewhere
for container in [&self.containers.items, &self.hidden_containers] {
for container in container {
let cpu_count = container.cpu_stats.back().map_or_else(
@@ -729,7 +719,6 @@ impl AppData {
}
/// Update related methods
/// Get mutable reference to a container in the containers vec & the hidden_containers vec
fn get_any_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
if self.get_hidden_container_by_id(id).is_some() {
@@ -769,12 +758,11 @@ impl AppData {
container.tx.update(tx);
container.mem_limit.update(mem_limit);
}
// need to benchmark this?
self.sort_containers();
}
/// Update, or insert, containers
pub fn update_containers(&mut self, all_containers: &mut [ContainerSummary]) {
pub fn update_containers(&mut self, mut all_containers: Vec<ContainerSummary>) {
let all_ids = self
.containers
.items
@@ -809,7 +797,7 @@ impl AppData {
}
}
for i in all_containers {
for mut i in all_containers {
if let Some(id) = i.id.as_ref() {
let name = i.names.as_mut().map_or(String::new(), |names| {
names.first_mut().map_or(String::new(), |f| {
@@ -820,8 +808,8 @@ impl AppData {
})
});
let ports = i.ports.as_ref().map_or(vec![], |i| {
i.iter().map(ContainerPorts::from).collect::<Vec<_>>()
let ports = i.ports.map_or(vec![], |i| {
i.into_iter().map(ContainerPorts::from).collect::<Vec<_>>()
});
let id = ContainerId::from(id.as_str());
@@ -855,7 +843,7 @@ impl AppData {
item.status = status;
};
if item.state != state {
item.docker_controls.items = DockerControls::gen_vec(state);
item.docker_controls.items = DockerCommand::gen_vec(state);
// Update the list state, needs to be None if the gen_vec returns an empty vec
match state {
State::Removing | State::Restarting | State::Unknown => {
@@ -1476,7 +1464,7 @@ mod tests {
let mut app_data = gen_appdata(&containers);
let result = app_data.get_container_name_by_id(&ContainerId::from("2"));
assert_eq!(result, Some(ContainerName::from("container_2")));
assert_eq!(result, Some(&ContainerName::from("container_2")));
}
#[test]
@@ -1526,7 +1514,7 @@ mod tests {
app_data.docker_controls_start();
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerControls::Pause));
assert_eq!(result, Some(DockerCommand::Pause));
}
#[test]
@@ -1539,7 +1527,7 @@ mod tests {
app_data.docker_controls_next();
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerControls::Restart));
assert_eq!(result, Some(DockerCommand::Restart));
}
#[test]
@@ -1551,12 +1539,12 @@ mod tests {
app_data.docker_controls_end();
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerControls::Delete));
assert_eq!(result, Some(DockerCommand::Delete));
// Next has no effect when at end
app_data.docker_controls_next();
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerControls::Delete));
assert_eq!(result, Some(DockerCommand::Delete));
}
#[test]
@@ -1569,19 +1557,19 @@ mod tests {
app_data.docker_controls_previous();
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerControls::Stop));
assert_eq!(result, Some(DockerCommand::Stop));
// previous has no effect when at start
app_data.docker_controls_start();
app_data.docker_controls_previous();
let result = app_data.selected_docker_controls();
assert_eq!(result, Some(DockerControls::Pause));
assert_eq!(result, Some(DockerCommand::Pause));
}
#[test]
/// DockerCommands get correct controls dependant on container state
fn test_app_data_get_control_items() {
let test_state = |state: State, expected: &mut Vec<DockerControls>| {
let test_state = |state: State, expected: &mut Vec<DockerCommand>| {
let gen_item_state = |state: State| {
ContainerItem::new(
1,
@@ -1605,42 +1593,42 @@ mod tests {
test_state(
State::Dead,
&mut vec![
DockerControls::Start,
DockerControls::Restart,
DockerControls::Delete,
DockerCommand::Start,
DockerCommand::Restart,
DockerCommand::Delete,
],
);
test_state(
State::Exited,
&mut vec![
DockerControls::Start,
DockerControls::Restart,
DockerControls::Delete,
DockerCommand::Start,
DockerCommand::Restart,
DockerCommand::Delete,
],
);
test_state(
State::Paused,
&mut vec![
DockerControls::Resume,
DockerControls::Stop,
DockerControls::Delete,
DockerCommand::Resume,
DockerCommand::Stop,
DockerCommand::Delete,
],
);
test_state(State::Removing, &mut vec![DockerControls::Delete]);
test_state(State::Removing, &mut vec![DockerCommand::Delete]);
test_state(
State::Restarting,
&mut vec![DockerControls::Stop, DockerControls::Delete],
&mut vec![DockerCommand::Stop, DockerCommand::Delete],
);
test_state(
State::Running(RunningState::Healthy),
&mut vec![
DockerControls::Pause,
DockerControls::Restart,
DockerControls::Stop,
DockerControls::Delete,
DockerCommand::Pause,
DockerCommand::Restart,
DockerCommand::Stop,
DockerCommand::Delete,
],
);
test_state(State::Unknown, &mut vec![DockerControls::Delete]);
test_state(State::Unknown, &mut vec![DockerCommand::Delete]);
}
// ****** //
@@ -1654,13 +1642,13 @@ mod tests {
let mut app_data = gen_appdata(&containers);
assert!(app_data.get_filter_term().is_none());
assert!(app_data.get_filter().1.is_none());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('_');
app_data.filter_term_push('2');
assert_eq!(app_data.get_filter_term(), Some(&"_2".to_string()));
assert_eq!(app_data.get_filter().1, Some(&"_2".to_string()));
app_data.filter_containers();
let post_len = app_data.containers.items.len();
@@ -1680,7 +1668,7 @@ mod tests {
let mut app_data = gen_appdata(&containers);
assert!(app_data.get_filter_term().is_none());
assert!(app_data.get_filter().1.is_none());
let pre_len = app_data.containers.items.len();
for c in ['i', 'm', 'a', 'g', 'e', '_', '2'] {
@@ -1689,8 +1677,10 @@ mod tests {
// app_data.filter_term_push('2');
app_data.filter_by_next();
assert_eq!(app_data.get_filter_by(), FilterBy::Image);
assert_eq!(app_data.get_filter_term(), Some(&"image_2".to_string()));
assert_eq!(
app_data.get_filter(),
(FilterBy::Image, Some(&"image_2".to_string()))
);
app_data.filter_containers();
let post_len = app_data.containers.items.len();
@@ -1709,7 +1699,7 @@ mod tests {
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
let mut app_data = gen_appdata(&containers);
assert!(app_data.get_filter_term().is_none());
assert!(app_data.get_filter().1.is_none());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('x');
@@ -1717,8 +1707,10 @@ mod tests {
app_data.filter_by_next();
app_data.filter_by_next();
assert_eq!(app_data.get_filter_by(), FilterBy::Status);
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
assert_eq!(
app_data.get_filter(),
(FilterBy::Status, Some(&"x".to_string()))
);
app_data.filter_containers();
let post_len = app_data.containers.items.len();
@@ -1737,7 +1729,7 @@ mod tests {
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
let mut app_data = gen_appdata(&containers);
assert!(app_data.get_filter_term().is_none());
assert!(app_data.get_filter().1.is_none());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('x');
@@ -1746,8 +1738,10 @@ mod tests {
app_data.filter_by_next();
app_data.filter_by_next();
assert_eq!(app_data.get_filter_by(), FilterBy::All);
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
assert_eq!(
app_data.get_filter(),
(FilterBy::All, Some(&"x".to_string()))
);
app_data.filter_containers();
let post_len = app_data.containers.items.len();
@@ -1766,7 +1760,7 @@ mod tests {
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
let mut app_data = gen_appdata(&containers);
assert!(app_data.get_filter_term().is_none());
assert!(app_data.get_filter().1.is_none());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('x');
@@ -1774,8 +1768,10 @@ mod tests {
app_data.filter_by_next();
app_data.filter_by_next();
assert_eq!(app_data.get_filter_by(), FilterBy::Status);
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
assert_eq!(
app_data.get_filter(),
(FilterBy::Status, Some(&"x".to_string()))
);
app_data.filter_containers();
let post_len = app_data.containers.items.len();
@@ -1787,8 +1783,10 @@ mod tests {
assert!(!app_data.can_insert(&containers[2]));
app_data.filter_by_prev();
assert_eq!(app_data.get_filter_by(), FilterBy::Image);
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
assert_eq!(
app_data.get_filter(),
(FilterBy::Image, Some(&"x".to_string()))
);
app_data.filter_containers();
let post_len = app_data.containers.items.len();
@@ -2230,12 +2228,12 @@ mod tests {
let (_ids, containers) = gen_containers();
let mut app_data = gen_appdata(&containers);
let result_pre = app_data.get_container_items().to_owned();
let mut input = [
let input = vec![
gen_container_summary(1, "paused"),
gen_container_summary(2, "dead"),
];
app_data.update_containers(&mut input);
app_data.update_containers(input);
let result_post = app_data.get_container_items().to_owned();
assert_ne!(result_pre, result_post);
assert_eq!(result_post[0].state, State::Paused);
+2 -7
View File
@@ -1,16 +1,13 @@
use crate::app_data::DockerControls;
use crate::app_data::DockerCommand;
use std::fmt;
/// app errors to set in global state
#[allow(unused)]
#[derive(Debug, Clone, Copy)]
pub enum AppError {
DockerCommand(DockerControls),
DockerCommand(DockerCommand),
DockerExec,
DockerLogs,
DockerConnect,
DockerInterval,
InputPoll,
MouseCapture(bool),
Terminal,
}
@@ -23,8 +20,6 @@ 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::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
Self::InputPoll => write!(f, "Unable to poll user input"),
Self::MouseCapture(x) => {
let reason = if *x { "en" } else { "dis" };
write!(f, "Unable to {reason}able mouse capture")
+2 -8
View File
@@ -1,19 +1,13 @@
use std::sync::Arc;
use crate::app_data::ContainerId;
use crate::app_data::{ContainerId, DockerCommand};
use bollard::Docker;
use tokio::sync::oneshot::Sender;
#[derive(Debug)]
pub enum DockerMessage {
ConfirmDelete(ContainerId),
Delete(ContainerId),
Control((DockerCommand, ContainerId)),
Exec(Sender<Arc<Docker>>),
Pause(ContainerId),
Quit,
Restart(ContainerId),
Start(ContainerId),
Stop(ContainerId),
Resume(ContainerId),
Update,
}
+390 -317
View File
@@ -10,10 +10,7 @@ use futures_util::StreamExt;
use parking_lot::Mutex;
use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, AtomicUsize},
Arc,
},
sync::{atomic::AtomicUsize, Arc},
};
use tokio::{
sync::mpsc::{Receiver, Sender},
@@ -22,7 +19,7 @@ use tokio::{
use uuid::Uuid;
use crate::{
app_data::{AppData, ContainerId, ContainerStatus, DockerControls, State},
app_data::{AppData, ContainerId, DockerCommand, State},
app_error::AppError,
parse_args::CliArgs,
ui::{GuiState, Status},
@@ -37,6 +34,15 @@ enum SpawnId {
Log(ContainerId),
}
impl SpawnId {
/// Extract the &ContainerId out of self
const fn get_id(&self) -> &ContainerId {
match self {
Self::Log(id) | Self::Stats((id, _)) => id,
}
}
}
/// Cpu & Mem stats take twice as long as the update interval to get a value, so will have two being executed at the same time
/// SpawnId::Stats takes container_id and binate value to enable both cycles of the same container_id to be inserted into the hashmap
/// Binate value is toggled when all handles have been spawned off
@@ -62,8 +68,6 @@ pub struct DockerData {
binate: Binate,
docker: Arc<Docker>,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
init: Option<Arc<AtomicUsize>>,
receiver: Receiver<DockerMessage>,
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
}
@@ -71,24 +75,30 @@ pub struct DockerData {
impl DockerData {
/// Use docker stats to calculate current cpu usage
#[allow(clippy::cast_precision_loss)]
// TODO FIX: this can overflow
fn calculate_usage(stats: &Stats) -> f64 {
let mut cpu_percentage = 0.0;
let previous_cpu = stats.precpu_stats.cpu_usage.total_usage;
let cpu_delta = stats.cpu_stats.cpu_usage.total_usage as f64 - previous_cpu as f64;
let cpu_delta = stats
.cpu_stats
.cpu_usage
.total_usage
.saturating_sub(stats.precpu_stats.cpu_usage.total_usage)
as f64;
if let (Some(cpu_stats_usage), Some(precpu_stats_usage)) = (
stats.cpu_stats.system_cpu_usage,
stats.precpu_stats.system_cpu_usage,
) {
let system_delta = (cpu_stats_usage - precpu_stats_usage) as f64;
let system_delta = cpu_stats_usage.saturating_sub(precpu_stats_usage) as f64;
let online_cpus = stats.cpu_stats.online_cpus.unwrap_or_else(|| {
stats
.cpu_stats
.cpu_usage
.percpu_usage
.as_ref()
.map_or(0, std::vec::Vec::len) as u64
u64::try_from(
stats
.cpu_stats
.cpu_usage
.percpu_usage
.as_ref()
.map_or(0, std::vec::Vec::len),
)
.unwrap_or_default()
}) as f64;
if system_delta > 0.0 && cpu_delta > 0.0 {
cpu_percentage = (cpu_delta / system_delta) * online_cpus * 100.0;
@@ -103,97 +113,86 @@ impl DockerData {
async fn update_container_stat(
app_data: Arc<Mutex<AppData>>,
docker: Arc<Docker>,
id: ContainerId,
init: Option<(Arc<AtomicUsize>, usize)>,
state: State,
spawn_id: SpawnId,
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
) {
if state.is_alive() || init.is_some() {
let mut stream = docker
.stats(
id.get(),
Some(StatsOptions {
stream: false,
one_shot: false,
}),
)
.take(1);
let id = spawn_id.get_id();
let mut stream = docker
.stats(
id.get(),
Some(StatsOptions {
stream: false,
one_shot: false,
}),
)
.take(1);
while let Some(Ok(stats)) = stream.next().await {
// Memory stats are only collected if the container is alive - is this the behaviour we want?
let mem_stat = if state.is_alive() {
let mem_cache = stats.memory_stats.stats.map_or(0, |i| match i {
MemoryStatsStats::V1(x) => x.inactive_file,
MemoryStatsStats::V2(x) => x.inactive_file,
});
while let Some(Ok(stats)) = stream.next().await {
// Memory stats are only collected if the container is alive - is this the behaviour we want?
let (mem_stat, cpu_stats) = if state.is_alive() {
let mem_cache = stats.memory_stats.stats.map_or(0, |i| match i {
MemoryStatsStats::V1(x) => x.inactive_file,
MemoryStatsStats::V2(x) => x.inactive_file,
});
(
Some(
stats
.memory_stats
.usage
.unwrap_or_default()
.saturating_sub(mem_cache),
)
} else {
None
};
),
Some(Self::calculate_usage(&stats)),
)
} else {
(None, None)
};
let mem_limit = stats.memory_stats.limit.unwrap_or_default();
let op_key = stats
.networks
.as_ref()
.and_then(|networks| networks.keys().next().cloned());
let op_key = stats
let (rx, tx) = if let Some(key) = op_key {
stats
.networks
.as_ref()
.and_then(|networks| networks.keys().next().cloned());
.unwrap_or_default()
.get(&key)
.map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
} else {
(0, 0)
};
let cpu_stats = if state.is_alive() {
Some(Self::calculate_usage(&stats))
} else {
None
};
let (rx, tx) = if let Some(key) = op_key {
stats
.networks
.unwrap_or_default()
.get(&key)
.map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
} else {
(0, 0)
};
app_data
.lock()
.update_stats_by_id(&id, cpu_stats, mem_stat, mem_limit, rx, tx);
}
app_data.lock().update_stats_by_id(
id,
cpu_stats,
mem_stat,
stats.memory_stats.limit.unwrap_or_default(),
rx,
tx,
);
}
spawns.lock().remove(&spawn_id);
if let Some((target, _)) = init {
target.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
/// Update all stats, spawn each container into own tokio::spawn thread
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
fn update_all_container_stats(&mut self) {
let all_ids = self.app_data.lock().get_all_id_state();
for (state, id) in all_ids {
let docker = Arc::clone(&self.docker);
let app_data = Arc::clone(&self.app_data);
let spawns = Arc::clone(&self.spawns);
let spawn_id = SpawnId::Stats((id.clone(), self.binate));
let spawn_id = SpawnId::Stats((id, self.binate));
let init = self.init.as_ref().map(|i| (Arc::clone(i), all_ids.len()));
self.spawns
.lock()
.entry(spawn_id.clone())
.or_insert_with(|| {
tokio::spawn(Self::update_container_stat(
app_data,
docker,
id.clone(),
init,
*state,
spawn_id,
spawns,
))
});
if let std::collections::hash_map::Entry::Vacant(spawns) =
self.spawns.lock().entry(spawn_id.clone())
{
spawns.insert(tokio::spawn(Self::update_container_stat(
Arc::clone(&self.app_data),
Arc::clone(&self.docker),
state,
spawn_id,
Arc::clone(&self.spawns),
)));
}
}
self.binate = self.binate.toggle();
}
@@ -201,7 +200,7 @@ impl DockerData {
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here
/// Just make sure that items sent are guaranteed to have an id
/// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set
pub async fn update_all_containers(&self) -> Vec<(State, ContainerId)> {
async fn update_all_containers(&self) {
let containers = self
.docker
.list_containers(Some(ListContainersOptions::<String> {
@@ -211,7 +210,7 @@ impl DockerData {
.await
.unwrap_or_default();
let mut output = containers
let output = containers
.into_iter()
.filter_map(|f| match f.id {
Some(_) => {
@@ -230,23 +229,7 @@ impl DockerData {
})
.collect::<Vec<ContainerSummary>>();
self.app_data.lock().update_containers(&mut output);
// Just get the containers that are currently running, or being restarted, no point updating info on paused or dead containers
output
.into_iter()
.filter_map(|i| {
i.id.map(|id| {
(
State::from((
i.state,
&ContainerStatus::from(i.status.map_or_else(String::new, |i| i)),
)),
ContainerId::from(id.as_str()),
)
})
})
.collect::<Vec<_>>()
self.app_data.lock().update_containers(output);
}
/// Update single container logs
@@ -257,10 +240,11 @@ impl DockerData {
id: ContainerId,
since: u64,
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
stderr: bool,
) {
let options = Some(LogsOptions::<String> {
stdout: true,
stderr: true,
stderr,
timestamps: true,
since: i64::try_from(since).unwrap_or_default(),
..Default::default()
@@ -275,44 +259,29 @@ impl DockerData {
output.push(data);
}
}
spawns.lock().remove(&SpawnId::Log(id.clone()));
app_data.lock().update_log_by_id(output, &id);
spawns.lock().remove(&SpawnId::Log(id));
}
/// Update all logs, spawn each container into own tokio::spawn thread
fn init_all_logs(&self, all_ids: &[(State, ContainerId)]) {
fn init_all_logs(&self, all_ids: Vec<(State, ContainerId)>) -> Arc<AtomicUsize> {
let init = Arc::new(AtomicUsize::new(0));
for (_, id) in all_ids {
let app_data: Arc<parking_lot::lock_api::Mutex<parking_lot::RawMutex, AppData>> =
Arc::clone(&self.app_data);
let docker = Arc::clone(&self.docker);
let app_data = Arc::clone(&self.app_data);
let spawns = Arc::clone(&self.spawns);
let key = SpawnId::Log(id.clone());
let std_err = self.args.std_err;
let init = Arc::clone(&init);
self.spawns.lock().insert(
key,
tokio::spawn(Self::update_log(app_data, docker, id.clone(), 0, spawns)),
SpawnId::Log(id.clone()),
tokio::spawn(async move {
Self::update_log(app_data, docker, id, 0, spawns, std_err).await;
init.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}),
);
}
}
/// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed)
async fn update_everything(&mut self) {
let all_ids = self.update_all_containers().await;
if let Some(container) = self.app_data.lock().get_selected_container() {
let last_updated = container.last_updated;
self.spawns
.lock()
.entry(SpawnId::Log(container.id.clone()))
.or_insert_with(|| {
// MAYBE make a struct that can create this data?
let app_data = Arc::clone(&self.app_data);
let docker = Arc::clone(&self.docker);
let id = container.id.clone();
let spawns = Arc::clone(&self.spawns);
tokio::spawn(Self::update_log(app_data, docker, id, last_updated, spawns))
});
};
self.update_all_container_stats(&all_ids);
self.app_data.lock().sort_containers();
self.gui_state.lock().stop_loading_animation(Uuid::nil());
init
}
/// Initialize docker container data, before any messages are received
@@ -320,27 +289,48 @@ impl DockerData {
self.gui_state.lock().status_push(Status::Init);
let loading_uuid = Uuid::new_v4();
GuiState::start_loading_animation(&self.gui_state, loading_uuid);
let all_ids = self.update_all_containers().await;
self.update_all_containers().await;
let all_ids = self.app_data.lock().get_all_id_state();
let all_ids_len = all_ids.len();
let init = self.init_all_logs(all_ids);
self.update_all_container_stats();
self.update_all_container_stats(&all_ids);
self.init_all_logs(&all_ids);
while let Some(x) = self.init.as_ref() {
while init.load(std::sync::atomic::Ordering::SeqCst) != all_ids_len {
self.app_data.lock().sort_containers();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
if x.load(std::sync::atomic::Ordering::SeqCst) == all_ids.len() {
self.init = None;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
self.gui_state.lock().stop_loading_animation(loading_uuid);
self.gui_state.lock().status_del(Status::Init);
}
/// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed)
async fn update_everything(&mut self) {
self.update_all_containers().await;
if let Some(container) = self.app_data.lock().get_selected_container() {
let last_updated = container.last_updated;
let spawn_id = SpawnId::Log(container.id.clone());
// Only spawn if not already spawned with a given id/binate pair
if let std::collections::hash_map::Entry::Vacant(spawns) =
self.spawns.lock().entry(spawn_id)
{
spawns.insert(tokio::spawn(Self::update_log(
Arc::clone(&self.app_data),
Arc::clone(&self.docker),
container.id.clone(),
last_updated,
Arc::clone(&self.spawns),
self.args.std_err,
)));
}
};
self.update_all_container_stats();
self.app_data.lock().sort_containers();
}
/// Set the global error as the docker error, and set gui_state to error
fn set_error(
app_data: &Arc<Mutex<AppData>>,
error: DockerControls,
error: DockerCommand,
gui_state: &Arc<Mutex<GuiState>>,
) {
app_data
@@ -348,150 +338,102 @@ impl DockerData {
.set_error(AppError::DockerCommand(error), gui_state, Status::Error);
}
/// Execute docker commands (start, stop etc) on it's own tokio thread
async fn execute_command(&mut self, control: DockerCommand, id: ContainerId) {
let (app_data, docker, gui_state) = (
Arc::clone(&self.app_data),
Arc::clone(&self.docker),
Arc::clone(&self.gui_state),
);
tokio::spawn(async move {
let uuid = Uuid::new_v4();
GuiState::start_loading_animation(&gui_state, uuid);
if match control {
DockerCommand::Delete => {
docker
.remove_container(
id.get(),
Some(RemoveContainerOptions {
v: false,
force: true,
link: false,
}),
)
.await
}
DockerCommand::Pause => docker.pause_container(id.get()).await,
DockerCommand::Restart => docker.restart_container(id.get(), None).await,
DockerCommand::Resume => docker.unpause_container(id.get()).await,
DockerCommand::Start => {
docker
.start_container(id.get(), None::<StartContainerOptions<String>>)
.await
}
DockerCommand::Stop => docker.stop_container(id.get(), None).await,
}
.is_err()
{
Self::set_error(&app_data, control, &gui_state);
}
gui_state.lock().stop_loading_animation(uuid);
});
self.update_everything().await;
}
/// Handle incoming messages, container controls & all container information update
/// Spawn Docker commands off into own thread
#[allow(clippy::too_many_lines)]
async fn message_handler(&mut self) {
while let Some(message) = self.receiver.recv().await {
let docker = Arc::clone(&self.docker);
let gui_state = Arc::clone(&self.gui_state);
let app_data = Arc::clone(&self.app_data);
let uuid = Uuid::new_v4();
// TODO need to refactor these
match message {
DockerMessage::Exec(docker_tx) => {
docker_tx.send(Arc::clone(&self.docker)).ok();
}
DockerMessage::Pause(id) => {
tokio::spawn(async move {
GuiState::start_loading_animation(&gui_state, uuid);
if docker.pause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Pause, &gui_state);
}
gui_state.lock().stop_loading_animation(uuid);
});
self.update_everything().await;
}
DockerMessage::Restart(id) => {
tokio::spawn(async move {
GuiState::start_loading_animation(&gui_state, uuid);
if docker.restart_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Restart, &gui_state);
}
gui_state.lock().stop_loading_animation(uuid);
});
self.update_everything().await;
}
DockerMessage::Start(id) => {
tokio::spawn(async move {
GuiState::start_loading_animation(&gui_state, uuid);
if docker
.start_container(id.get(), None::<StartContainerOptions<String>>)
.await
.is_err()
{
Self::set_error(&app_data, DockerControls::Start, &gui_state);
}
gui_state.lock().stop_loading_animation(uuid);
});
self.update_everything().await;
}
DockerMessage::Stop(id) => {
tokio::spawn(async move {
GuiState::start_loading_animation(&gui_state, uuid);
if docker.stop_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
}
gui_state.lock().stop_loading_animation(uuid);
});
self.update_everything().await;
}
DockerMessage::Resume(id) => {
tokio::spawn(async move {
GuiState::start_loading_animation(&gui_state, uuid);
if docker.unpause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Resume, &gui_state);
}
gui_state.lock().stop_loading_animation(uuid);
});
self.update_everything().await;
}
DockerMessage::Delete(id) => {
tokio::spawn(async move {
GuiState::start_loading_animation(&gui_state, uuid);
if docker
.remove_container(
id.get(),
Some(RemoveContainerOptions {
v: false,
force: true,
link: false,
}),
)
.await
.is_err()
{
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
}
gui_state.lock().stop_loading_animation(uuid);
});
self.update_everything().await;
self.gui_state.lock().set_delete_container(None);
}
DockerMessage::ConfirmDelete(id) => {
self.gui_state.lock().set_delete_container(Some(id));
}
DockerMessage::Update => self.update_everything().await,
DockerMessage::Quit => {
self.spawns
.lock()
.values()
.for_each(tokio::task::JoinHandle::abort);
self.is_running
.store(false, std::sync::atomic::Ordering::SeqCst);
DockerMessage::Control((command, id)) => self.execute_command(command, id).await,
DockerMessage::Exec(docker_tx) => {
docker_tx.send(Arc::clone(&self.docker)).ok();
}
DockerMessage::Update => self.update_everything().await,
}
}
}
/// Send an update message every x ms, where x is the args.docker_interval
fn scheduler(args: &CliArgs, docker_tx: Sender<DockerMessage>) {
fn heartbeat(args: &CliArgs, docker_tx: Sender<DockerMessage>) {
let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
let mut now = std::time::Instant::now();
tokio::spawn(async move {
loop {
let to_sleep = update_duration.saturating_sub(now.elapsed());
tokio::time::sleep(to_sleep).await;
docker_tx.send(DockerMessage::Update).await.ok();
if let Some(to_sleep) = update_duration.checked_sub(now.elapsed()) {
tokio::time::sleep(to_sleep).await;
}
now = std::time::Instant::now();
}
});
}
/// Initialise self, and start the message receiving loop
pub async fn init(
pub async fn start(
app_data: Arc<Mutex<AppData>>,
docker: Docker,
docker_rx: Receiver<DockerMessage>,
docker_tx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
) {
let args = app_data.lock().args.clone();
if app_data.lock().get_error().is_none() {
let mut inner = Self {
app_data,
args: args.clone(),
args,
binate: Binate::One,
docker: Arc::new(docker),
gui_state,
init: Some(Arc::new(AtomicUsize::new(0))),
is_running,
receiver: docker_rx,
spawns: Arc::new(Mutex::new(HashMap::new())),
};
inner.initialise_container_data().await;
Self::scheduler(&args, docker_tx);
Self::heartbeat(&inner.args, docker_tx);
inner.message_handler().await;
}
}
@@ -499,19 +441,19 @@ impl DockerData {
// tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use bollard::container::{
BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData,
BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, Stats, StorageStats, ThrottlingData,
};
use super::*;
#[allow(clippy::too_many_lines)]
fn gen_stats(x: u64, y: u64) -> Stats {
fn gen_stats() -> Stats {
Stats {
read: String::new(),
preread: String::new(),
num_procs: 0,
num_procs: 1,
pids_stats: PidsStats {
current: None,
limit: None,
@@ -542,33 +484,12 @@ mod tests {
},
cpu_stats: CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![
291_593_800,
182_192_900,
195_048_700,
23_032_300,
132_928_700,
235_555_600,
120_225_700,
175_752_000,
213_060_300,
95_321_600,
226_821_000,
0,
109_151_300,
0,
86_240_200,
1_884_400,
59_077_300,
23_224_900,
95_386_300,
144_987_400,
]),
total_usage: 250_000_000,
usage_in_usermode: 1_020_000_000,
usage_in_kernelmode: 1_030_000_000,
percpu_usage: Some(vec![50]),
usage_in_usermode: 10,
total_usage: 100,
usage_in_kernelmode: 20,
},
system_cpu_usage: Some(x),
system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
@@ -578,33 +499,12 @@ mod tests {
},
precpu_stats: CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![
291_593_800,
182_192_900,
195_048_700,
23_032_300,
132_928_700,
235_555_600,
120_225_700,
175_752_000,
213_060_300,
95_321_600,
226_821_000,
0,
109_151_300,
0,
86_240_200,
1_884_400,
59_077_300,
23_224_900,
93_831_100,
144_987_400,
]),
total_usage: 200_000_000,
usage_in_usermode: 1_020_000_000,
usage_in_kernelmode: 1_020_000_000,
percpu_usage: Some(vec![50]),
usage_in_usermode: 10,
total_usage: 100,
usage_in_kernelmode: 20,
},
system_cpu_usage: Some(y),
system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
@@ -618,25 +518,198 @@ mod tests {
write_count_normalized: None,
write_size_bytes: None,
},
name: "/container_1".to_owned(),
id: "1".to_owned(),
name: String::new(),
id: String::new(),
}
}
#[test]
#[allow(clippy::float_cmp)]
/// Test the stats calculator, had to cheat here to get round input/outputs
fn test_calculate_usage_no_previous_cpu() {
let stats = gen_stats(1_000_000_000, 900_000_000);
let result = DockerData::calculate_usage(&stats);
assert_eq!(result, 50.0);
fn test_calculate_usage_50() {
let mut stats = gen_stats();
stats.precpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![50]),
usage_in_usermode: 10,
total_usage: 100,
usage_in_kernelmode: 20,
},
system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
stats.cpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![150]),
usage_in_usermode: 20,
total_usage: 150,
usage_in_kernelmode: 30,
},
system_cpu_usage: Some(500),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
let cpu_percentage = DockerData::calculate_usage(&stats);
assert_eq!(50.0, cpu_percentage);
}
let stats = gen_stats(1_000_000_000, 800_000_000);
let result = DockerData::calculate_usage(&stats);
assert_eq!(result, 25.0);
#[test]
fn test_calculate_usage_25() {
let mut stats = gen_stats();
stats.precpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![50]),
usage_in_usermode: 10,
total_usage: 100,
usage_in_kernelmode: 20,
},
system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
stats.cpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![75]),
usage_in_usermode: 20,
total_usage: 125,
usage_in_kernelmode: 30,
},
system_cpu_usage: Some(500),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
let stats = gen_stats(1_000_000_000, 750_000_000);
let result = DockerData::calculate_usage(&stats);
assert_eq!(result, 20.00);
let cpu_percentage = DockerData::calculate_usage(&stats);
assert_eq!(25.0, cpu_percentage);
}
#[test]
fn test_calculate_usage_75() {
let mut stats = gen_stats();
stats.precpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![50]),
usage_in_usermode: 10,
total_usage: 100,
usage_in_kernelmode: 20,
},
system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
stats.cpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![175]),
usage_in_usermode: 20,
total_usage: 175,
usage_in_kernelmode: 30,
},
system_cpu_usage: Some(500),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
let cpu_percentage = DockerData::calculate_usage(&stats);
assert_eq!(75.0, cpu_percentage);
}
#[test]
fn test_calculate_usage_100() {
let mut stats = gen_stats();
stats.precpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![50]),
usage_in_usermode: 10,
total_usage: 100,
usage_in_kernelmode: 20,
},
system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
stats.cpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![200]),
usage_in_usermode: 20,
total_usage: 200,
usage_in_kernelmode: 30,
},
system_cpu_usage: Some(500),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
let cpu_percentage = DockerData::calculate_usage(&stats);
assert_eq!(100.0, cpu_percentage);
}
#[test]
fn test_calculate_usage_175() {
let mut stats = gen_stats();
stats.precpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![50]),
usage_in_usermode: 10,
total_usage: 100,
usage_in_kernelmode: 20,
},
system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
stats.cpu_stats = CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![275]),
usage_in_usermode: 20,
total_usage: 275,
usage_in_kernelmode: 30,
},
system_cpu_usage: Some(500),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
};
let cpu_percentage = DockerData::calculate_usage(&stats);
assert_eq!(175.0, cpu_percentage);
}
}
+10 -7
View File
@@ -144,9 +144,9 @@ impl TerminalSize {
#[derive(Debug, Clone)]
pub enum ExecMode {
// use Bollard Rust library
Internal((ContainerId, Arc<Docker>)),
Internal((Arc<ContainerId>, Arc<Docker>)),
// use the external `docker-cli`
External(ContainerId),
External(Arc<ContainerId>),
}
impl ExecMode {
@@ -186,7 +186,10 @@ impl ExecMode {
{
if let Some(Ok(msg)) = output.next().await {
if !msg.to_string().starts_with(OCI_ERROR) {
return Some(Self::Internal((id.clone(), Arc::clone(docker))));
return Some(Self::Internal((
Arc::new(id),
Arc::clone(docker),
)));
}
}
}
@@ -199,7 +202,7 @@ impl ExecMode {
{
if let Ok(output) = String::from_utf8(output.stdout) {
if !output.starts_with(OCI_ERROR) {
return Some(Self::External(id.clone()));
return Some(Self::External(Arc::new(id)));
}
}
}
@@ -302,9 +305,9 @@ impl ExecMode {
Ok(())
}
// This is the fix for key pressed not being handled correctly on quit
// It writes a special message to the stdout, and then listens out for a valid response
// afterwhich it's assumes that we're completely done with TTY
/// This is the fix for key pressed not being handled correctly on quit
/// It writes a special message to the stdout, and then listens out for a valid response
/// afterwhich it's assumes that we're completely done with TTY
fn internal_cleanup(&self) -> Result<(), AppError> {
match self {
Self::External(_) => Ok(()),
+128 -174
View File
@@ -1,14 +1,11 @@
use std::{
fs::OpenOptions,
io::{BufWriter, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
sync::{atomic::AtomicBool, Arc},
time::SystemTime,
};
use bollard::{container::LogsOptions, Docker};
use bollard::container::LogsOptions;
use cansi::v3::categorise_text;
use crossterm::{
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
@@ -22,7 +19,7 @@ use uuid::Uuid;
mod message;
use crate::{
app_data::{AppData, DockerControls, Header},
app_data::{AppData, DockerCommand, Header},
app_error::AppError,
docker_data::DockerMessage,
exec::{tty_readable, ExecMode},
@@ -43,7 +40,7 @@ pub struct InputHandler {
impl InputHandler {
/// Initialize self, and running the message handling loop
pub async fn init(
pub async fn start(
app_data: Arc<Mutex<AppData>>,
rec: Receiver<InputMessages>,
docker_tx: Sender<DockerMessage>,
@@ -58,35 +55,30 @@ impl InputHandler {
rec,
mouse_capture: true,
};
inner.start().await;
inner.message_handler().await;
}
/// check for incoming messages
async fn start(&mut self) {
async fn message_handler(&mut self) {
while let Some(message) = self.rec.recv().await {
match message {
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
InputMessages::MouseEvent(mouse_event) => {
if !self.gui_state.lock().status_contains(&[
Status::Error,
Status::Help,
Status::DeleteConfirm,
Status::Filter,
]) {
let status = self.gui_state.lock().get_status();
let contains = |s: Status| status.contains(&s);
if !contains(Status::Error)
| !contains(Status::Help)
| !contains(Status::DeleteConfirm)
| !contains(Status::Filter)
{
self.mouse_press(mouse_event);
}
let delete_confirm = self
.gui_state
.lock()
.status_contains(&[Status::DeleteConfirm]);
if delete_confirm {
if contains(Status::DeleteConfirm) {
self.button_intersect(mouse_event).await;
}
}
}
if !self.is_running.load(Ordering::SeqCst) {
break;
}
}
}
@@ -97,12 +89,10 @@ impl InputHandler {
/// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead
/// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
async fn quit(&self) {
let error_init = self
.gui_state
.lock()
.status_contains(&[Status::Error, Status::Init]);
if error_init || self.docker_tx.send(DockerMessage::Quit).await.is_err() {
fn quit(&self) {
let status = self.gui_state.lock().get_status();
let contains = |s: Status| status.contains(&s);
if !contains(Status::Error) | !contains(Status::Init) {
self.is_running
.store(false, std::sync::atomic::Ordering::SeqCst);
}
@@ -112,7 +102,10 @@ impl InputHandler {
async fn confirm_delete(&self) {
let id = self.gui_state.lock().get_delete_container();
if let Some(id) = id {
self.docker_tx.send(DockerMessage::Delete(id)).await.ok();
self.docker_tx
.send(DockerMessage::Control((DockerCommand::Delete, id)))
.await
.ok();
}
}
@@ -127,7 +120,7 @@ impl InputHandler {
if !is_oxker && tty_readable() {
let uuid = Uuid::new_v4();
GuiState::start_loading_animation(&self.gui_state, uuid);
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
let (sx, rx) = tokio::sync::oneshot::channel();
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
if let Ok(docker) = rx.await {
@@ -150,118 +143,109 @@ impl InputHandler {
/// Toggle the mouse capture (via input of the 'm' key)
fn m_key(&mut self) {
let err = || {
self.app_data.lock().set_error(
AppError::MouseCapture(!self.mouse_capture),
&self.gui_state,
Status::Error,
);
};
if self.mouse_capture {
if execute!(std::io::stdout(), DisableMouseCapture).is_ok() {
self.gui_state
.lock()
.set_info_box("✖ mouse capture disabled");
} else {
self.app_data.lock().set_error(
AppError::MouseCapture(false),
&self.gui_state,
Status::Error,
);
err();
}
} else if Ui::enable_mouse_capture().is_ok() {
self.gui_state
.lock()
.set_info_box("✓ mouse capture enabled");
} else {
self.app_data.lock().set_error(
AppError::MouseCapture(true),
&self.gui_state,
Status::Error,
);
err();
};
self.mouse_capture = !self.mouse_capture;
}
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
async fn s_key(&self) {
/// This is the inner workings, *inlined* here to return a Result
async fn save_logs(
app_data: &Arc<Mutex<AppData>>,
gui_state: &Arc<Mutex<GuiState>>,
docker_tx: &Sender<DockerMessage>,
) -> Result<(), Box<dyn std::error::Error>> {
let args = app_data.lock().args.clone();
let container = app_data.lock().get_selected_container_id_state_name();
if let Some((id, _, name)) = container {
if let Some(log_path) = args.save_dir {
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
docker_tx.send(DockerMessage::Exec(sx)).await?;
async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
let args = self.app_data.lock().args.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 {
let (sx, rx) = tokio::sync::oneshot::channel();
self.docker_tx.send(DockerMessage::Exec(sx)).await?;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |i| i.as_secs());
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |i| i.as_secs());
let path = log_path.join(format!("{name}_{now}.log"));
let path = log_path.join(format!("{name}_{now}.log"));
let docker = rx.await?;
let options = Some(LogsOptions::<String> {
stderr: true,
stdout: true,
timestamps: args.timestamp,
since: 0,
..Default::default()
});
let mut logs = docker.logs(id.get(), options);
let mut output = vec![];
let options = Some(LogsOptions::<String> {
stderr: true,
stdout: true,
timestamps: args.timestamp,
since: 0,
..Default::default()
});
let mut logs = rx.await?.logs(id.get(), options);
let mut output = vec![];
while let Some(Ok(value)) = logs.next().await {
let data = value.to_string();
if !data.trim().is_empty() {
output.push(
categorise_text(&data)
.into_iter()
.map(|i| i.text)
.collect::<String>(),
);
}
}
if !output.is_empty() {
let mut stream = BufWriter::new(
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)?,
while let Some(Ok(value)) = logs.next().await {
let data = value.to_string();
if !data.trim().is_empty() {
output.push(
categorise_text(&data)
.into_iter()
.map(|i| i.text)
.collect::<String>(),
);
for line in &output {
stream.write_all(line.as_bytes())?;
}
stream.flush()?;
gui_state
.lock()
.set_info_box(&format!("saved to {}", path.display()));
}
}
if !output.is_empty() {
let mut stream = BufWriter::new(
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)?,
);
for line in &output {
stream.write_all(line.as_bytes())?;
}
stream.flush()?;
self.gui_state
.lock()
.set_info_box(&format!("saved to {}", path.display()));
}
}
Ok(())
}
Ok(())
}
let log_status = Status::Logs;
let status = self.gui_state.lock().status_contains(&[log_status]);
if !status {
self.gui_state.lock().status_push(log_status);
/// Attempt to save the currently selected container logs to a file
async fn s_key(&self) {
let status = self.gui_state.lock().get_status();
let contains = |s: Status| status.contains(&s);
if !contains(Status::Logs) {
self.gui_state.lock().status_push(Status::Logs);
let uuid = Uuid::new_v4();
GuiState::start_loading_animation(&self.gui_state, uuid);
if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
.await
.is_err()
{
if self.save_logs().await.is_err() {
self.app_data.lock().set_error(
AppError::DockerLogs,
&self.gui_state,
Status::Error,
);
}
self.gui_state.lock().status_del(log_status);
self.gui_state.lock().status_del(Status::Logs);
self.gui_state.lock().stop_loading_animation(uuid);
}
}
@@ -281,26 +265,17 @@ impl InputHandler {
let option_id = self.app_data.lock().get_selected_container_id();
if let Some(id) = option_id {
match command {
DockerControls::Delete => self
DockerCommand::Delete => self
.docker_tx
.send(DockerMessage::ConfirmDelete(id))
.await
.ok(),
DockerControls::Pause => {
self.docker_tx.send(DockerMessage::Pause(id)).await.ok()
}
DockerControls::Resume => {
self.docker_tx.send(DockerMessage::Resume(id)).await.ok()
}
DockerControls::Start => {
self.docker_tx.send(DockerMessage::Start(id)).await.ok()
}
DockerControls::Stop => {
self.docker_tx.send(DockerMessage::Stop(id)).await.ok()
}
DockerControls::Restart => {
self.docker_tx.send(DockerMessage::Restart(id)).await.ok()
}
_ => self
.docker_tx
.send(DockerMessage::Control((command, id)))
.await
.ok(),
};
}
}
@@ -308,50 +283,43 @@ 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) {
let is_containers =
self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers;
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
2
} else {
1
};
for _ in 0..count {
self.gui_state.lock().next_panel();
if self.app_data.lock().get_container_len() == 0
&& self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands
{
self.gui_state.lock().next_panel();
}
}
/// Change to previously selected panel
/// Need to skip the commands planel if there no are current containers running
fn back_tab_key(&self) {
let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
2
} else {
1
};
for _ in 0..count {
self.gui_state.lock().previous_panel();
if self.app_data.lock().get_container_len() == 0
&& self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands
{
self.gui_state.lock().previous_panel();
}
}
fn home_key(&self) {
let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => locked_data.containers_start(),
SelectablePanel::Logs => locked_data.log_start(),
SelectablePanel::Commands => locked_data.docker_controls_start(),
SelectablePanel::Containers => self.app_data.lock().containers_start(),
SelectablePanel::Logs => self.app_data.lock().log_start(),
SelectablePanel::Commands => self.app_data.lock().docker_controls_start(),
}
}
/// Go to end of the list of the currently selected panel
fn end_key(&self) {
let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => locked_data.containers_end(),
SelectablePanel::Logs => locked_data.log_end(),
SelectablePanel::Commands => locked_data.docker_controls_end(),
SelectablePanel::Containers => self.app_data.lock().containers_end(),
SelectablePanel::Logs => self.app_data.lock().log_end(),
SelectablePanel::Commands => self.app_data.lock().docker_controls_end(),
}
}
@@ -455,24 +423,21 @@ impl InputHandler {
}
/// Handle keyboard button events
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
let contains_delete = self
.gui_state
.lock()
.status_contains(&[Status::DeleteConfirm]);
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
let status = self.gui_state.lock().get_status();
let contains = |s: Status| status.contains(&s);
let contains_error = contains(Status::Error);
let contains_help = contains(Status::Help);
let contains_exec = contains(Status::Exec);
let contains_filter: bool = contains(Status::Filter);
let contains_filter = contains(Status::Filter);
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 {
// Always just quit on Ctrl + c/C or q/Q, unless in FIlter status active
self.quit().await;
// Always just quit on Ctrl + c/C or q/Q, unless in Filter status active
self.quit();
}
if contains_error {
@@ -514,22 +479,13 @@ impl InputHandler {
MouseEventKind::ScrollUp => self.previous(),
MouseEventKind::ScrollDown => self.next(),
MouseEventKind::Down(MouseButton::Left) => {
let header = self.gui_state.lock().header_intersect(Rect::new(
mouse_event.column,
mouse_event.row,
1,
1,
));
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
let header = self.gui_state.lock().header_intersect(mouse_point);
if let Some(header) = header {
self.sort(header);
}
self.gui_state.lock().panel_intersect(Rect::new(
mouse_event.column,
mouse_event.row,
1,
1,
));
self.gui_state.lock().panel_intersect(mouse_point);
}
_ => (),
}
@@ -537,23 +493,21 @@ impl InputHandler {
/// Change state to next, depending which panel is currently in focus
fn next(&self) {
let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => locked_data.containers_next(),
SelectablePanel::Logs => locked_data.log_next(),
SelectablePanel::Commands => locked_data.docker_controls_next(),
SelectablePanel::Containers => self.app_data.lock().containers_next(),
SelectablePanel::Logs => self.app_data.lock().log_next(),
SelectablePanel::Commands => self.app_data.lock().docker_controls_next(),
};
}
/// Change state to previous, depending which panel is currently in focus
fn previous(&self) {
let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => locked_data.containers_previous(),
SelectablePanel::Logs => locked_data.log_previous(),
SelectablePanel::Commands => locked_data.docker_controls_previous(),
SelectablePanel::Containers => self.app_data.lock().containers_previous(),
SelectablePanel::Logs => self.app_data.lock().log_previous(),
SelectablePanel::Commands => self.app_data.lock().docker_controls_previous(),
}
}
}
+27 -44
View File
@@ -52,32 +52,28 @@ async fn docker_init(
docker_rx: Receiver<DockerMessage>,
docker_tx: Sender<DockerMessage>,
gui_state: &Arc<Mutex<GuiState>>,
is_running: &Arc<AtomicBool>,
host: Option<String>,
) {
let host = read_docker_host(&app_data.lock().args);
let connection = host.map_or_else(Docker::connect_with_socket_defaults, |host| {
Docker::connect_with_socket(&host, 120, API_DEFAULT_VERSION)
});
if let Ok(docker) = connection {
if docker.ping().await.is_ok() {
let app_data = Arc::clone(app_data);
let gui_state = Arc::clone(gui_state);
let is_running = Arc::clone(is_running);
tokio::spawn(DockerData::init(
app_data, docker, docker_rx, docker_tx, gui_state, is_running,
tokio::spawn(DockerData::start(
Arc::clone(app_data),
docker,
docker_rx,
docker_tx,
Arc::clone(gui_state),
));
} else {
app_data
.lock()
.set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
return;
}
} else {
app_data
.lock()
.set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
}
app_data
.lock()
.set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
}
/// Create data for, and then spawn a tokio thread, for the input handler
@@ -88,15 +84,12 @@ fn handler_init(
input_rx: Receiver<InputMessages>,
is_running: &Arc<AtomicBool>,
) {
let app_data = Arc::clone(app_data);
let gui_state = Arc::clone(gui_state);
let is_running = Arc::clone(is_running);
tokio::spawn(input_handler::InputHandler::init(
app_data,
tokio::spawn(input_handler::InputHandler::start(
Arc::clone(app_data),
input_rx,
docker_sx.clone(),
gui_state,
is_running,
Arc::clone(gui_state),
Arc::clone(is_running),
));
}
@@ -106,34 +99,20 @@ async fn main() {
let args = CliArgs::new();
// If running via Docker image, need to sleep else program will just quit straight away, no real idea why
// So just sleep for small while
if args.in_container {
std::thread::sleep(std::time::Duration::from_millis(250));
}
let host = read_docker_host(&args);
let app_data = Arc::new(Mutex::new(AppData::default(args.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,
&is_running,
host,
)
.await;
docker_init(&app_data, docker_rx, docker_tx.clone(), &gui_state).await;
if args.gui {
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
Ui::create(app_data, gui_state, input_tx, is_running).await;
Ui::start(app_data, gui_state, input_tx, is_running).await;
} else {
info!("in debug mode\n");
let mut now = std::time::Instant::now();
// Debug mode for testing, less pointless now, will display some basic information
while is_running.load(Ordering::SeqCst) {
let err = app_data.lock().get_error();
@@ -141,10 +120,12 @@ async fn main() {
error!("{}", err);
process::exit(1);
}
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
args.docker_interval,
)))
.await;
if let Some(Ok(to_sleep)) = u128::from(args.docker_interval)
.checked_sub(now.elapsed().as_millis())
.map(u64::try_from)
{
tokio::time::sleep(std::time::Duration::from_millis(to_sleep)).await;
}
let containers = app_data
.lock()
.get_container_items()
@@ -158,6 +139,7 @@ async fn main() {
}
println!();
}
now = std::time::Instant::now();
}
}
}
@@ -182,6 +164,7 @@ mod tests {
docker_interval: 1000,
gui: true,
host: None,
std_err: false,
in_container: false,
save_dir: None,
raw: false,
+10 -9
View File
@@ -37,13 +37,17 @@ pub struct Args {
#[clap(long, short = None)]
pub host: Option<String>,
/// Force use of docker cli when execing into containers
#[clap(long="use-cli", short = None)]
pub use_cli: bool,
/// 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)]
@@ -58,6 +62,7 @@ pub struct CliArgs {
pub raw: bool,
pub show_self: bool,
pub timestamp: bool,
pub std_err: bool,
pub use_cli: bool,
}
@@ -65,12 +70,7 @@ 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 {
if let Ok(value) = std::env::var(ENV_KEY) {
if value == ENV_VALUE {
return true;
}
}
false
std::env::var(ENV_KEY).map_or(false, |i| i == ENV_VALUE)
}
/// Parse cli arguments
@@ -97,6 +97,7 @@ impl CliArgs {
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,
}
+220 -184
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -266,10 +266,9 @@ impl GuiState {
self.delete_container = id;
}
/// Check if the current gui_status contains any of the given status'
/// Don't really like this methodology for gui state, needs a re-think
pub fn status_contains(&self, status: &[Status]) -> bool {
status.iter().any(|i| self.status.contains(i))
/// Return a copy of the Status HashSet
pub fn get_status(&self) -> HashSet<Status> {
self.status.clone()
}
/// Remove a gui_status into the current gui_status HashSet
+71 -50
View File
@@ -4,13 +4,14 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use parking_lot::{Mutex, MutexGuard};
use parking_lot::Mutex;
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Position},
Frame, Terminal,
};
use std::{
collections::HashSet,
io::{self, Stdout, Write},
sync::{atomic::Ordering, Arc},
time::Duration,
@@ -26,18 +27,21 @@ mod gui_state;
pub use self::color_match::*;
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
use crate::{
app_data::{AppData, Columns, ContainerId, Header, SortedOrder},
app_data::{
AppData, Columns, ContainerId, ContainerPorts, CpuTuple, FilterBy, Header, MemTuple,
SortedOrder, State,
},
app_error::AppError,
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 {
app_data: Arc<Mutex<AppData>>,
gui_state: Arc<Mutex<GuiState>>,
input_poll_rate: Duration,
input_tx: Sender<InputMessages>,
is_running: Arc<AtomicBool>,
now: Instant,
@@ -59,7 +63,7 @@ impl Ui {
}
/// Create a new Ui struct, and execute the drawing loop
pub async fn create(
pub async fn start(
app_data: Arc<Mutex<AppData>>,
gui_state: Arc<Mutex<GuiState>>,
input_tx: Sender<InputMessages>,
@@ -71,7 +75,6 @@ impl Ui {
app_data,
cursor_position,
gui_state,
input_poll_rate: std::time::Duration::from_millis(100),
input_tx,
is_running,
now: Instant::now(),
@@ -141,7 +144,7 @@ impl Ui {
Ok(())
}
/// Use exeternal docker cli to exec into a container
/// Use external docker cli to exec into a container
async fn exec(&mut self) {
let exec_mode = self.gui_state.lock().get_exec_mode();
@@ -163,20 +166,21 @@ impl Ui {
/// The loop for drawing the main UI to the terminal
async fn gui_loop(&mut self) -> Result<(), AppError> {
while self.is_running.load(Ordering::SeqCst) {
let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
let fd = FrameData::from(&*self);
let exec = fd.status.contains(&Status::Exec);
if exec {
self.exec().await;
}
if self
.terminal
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state))
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state, &fd))
.is_err()
{
return Err(AppError::Terminal);
}
if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) {
if crossterm::event::poll(POLL_RATE).unwrap_or(false) {
if let Ok(event) = event::read() {
if let Event::Key(key) = event {
if key.kind == event::KeyEventKind::Press {
@@ -206,11 +210,8 @@ impl Ui {
/// Draw either the Error, or main oxker ui, to the terminal
async fn draw_ui(&mut self) -> Result<(), AppError> {
let status_dockerconnect = self
.gui_state
.lock()
.status_contains(&[Status::DockerConnect]);
if status_dockerconnect {
let status = self.gui_state.lock().get_status();
if status.contains(&Status::DockerConnect) {
self.err_loop()?;
} else {
self.gui_loop().await?;
@@ -219,54 +220,73 @@ impl Ui {
}
}
/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here
#[derive(Debug)]
/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here
#[derive(Debug, Clone)]
pub struct FrameData {
chart_data: Option<(CpuTuple, MemTuple)>,
columns: Columns,
container_title: String,
delete_confirm: Option<ContainerId>,
filter_by: FilterBy,
filter_term: Option<String>,
has_containers: bool,
has_error: Option<AppError>,
height: u16,
help_visible: bool,
init: bool,
info_text: Option<(String, Instant)>,
is_loading: bool,
loading_icon: String,
log_title: String,
port_max_lens: (usize, usize, usize),
ports: Option<(Vec<ContainerPorts>, State)>,
selected_panel: SelectablePanel,
sorted_by: Option<(Header, SortedOrder)>,
status: HashSet<Status>,
}
impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
fn from(data: (MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)) -> Self {
impl From<&Ui> for FrameData {
fn from(ui: &Ui) -> Self {
let (app_data, gui_data) = (ui.app_data.lock(), ui.gui_state.lock());
// set max height for container section, needs +5 to deal with docker commands list and borders
let height = data.0.get_container_len();
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 {
columns: data.0.get_width(),
delete_confirm: data.1.get_delete_container(),
has_containers: data.0.get_container_len() > 0,
has_error: data.0.get_error(),
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,
help_visible: data.1.status_contains(&[Status::Help]),
init: data.1.status_contains(&[Status::Init]),
info_text: data.1.info_box_text.clone(),
loading_icon: data.1.get_loading().to_string(),
selected_panel: data.1.get_selected_panel(),
sorted_by: data.0.get_sorted(),
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(),
port_max_lens: app_data.get_longest_port(),
ports: app_data.get_selected_ports(),
selected_panel: gui_data.get_selected_panel(),
sorted_by: app_data.get_sorted(),
status: gui_data.get_status(),
}
}
}
/// 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>>) {
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
let contains_filter = gui_state.lock().status_contains(&[Status::Filter]);
let whole_constraints = if contains_filter {
fn draw_frame(
f: &mut Frame,
app_data: &Arc<Mutex<AppData>>,
gui_state: &Arc<Mutex<GuiState>>,
fd: &FrameData,
) {
let whole_constraints = if fd.status.contains(&Status::Filter) {
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
} else {
vec![Constraint::Max(1), Constraint::Min(1)]
@@ -300,21 +320,21 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
vec![Constraint::Percentage(100)]
};
// Split into 2, logs, and optional charts
// Split into 2, logs and charts
let lower_main = Layout::default()
.direction(Direction::Vertical)
.constraints(lower_split)
.split(upper_main[1]);
draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state);
draw_blocks::containers(app_data, top_panel[0], f, fd, gui_state);
draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state);
draw_blocks::logs(app_data, lower_main[0], f, fd, gui_state);
draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state);
draw_blocks::heading_bar(whole_layout[0], f, fd, gui_state);
// Draw filter bar
if let Some(rect) = whole_layout.get(2) {
draw_blocks::filter_bar(*rect, f, app_data);
draw_blocks::filter_bar(*rect, f, fd);
}
if let Some(id) = fd.delete_confirm.as_ref() {
@@ -325,34 +345,35 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
gui_state.lock().set_delete_container(None);
},
|name| {
draw_blocks::delete_confirm(f, gui_state, &name);
draw_blocks::delete_confirm(f, gui_state, 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(app_data, *rect, 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 max_lens = app_data.lock().get_longest_port();
let ports_len = u16::try_from(max_lens.0 + max_lens.1 + max_lens.2 + 2).unwrap_or(26);
let ports_len =
u16::try_from(fd.port_max_lens.0 + fd.port_max_lens.1 + fd.port_max_lens.2 + 2)
.unwrap_or(26);
let lower = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(1), Constraint::Max(ports_len)])
.split(lower_main[1]);
draw_blocks::chart(f, lower[0], app_data);
draw_blocks::ports(f, lower[1], app_data, max_lens);
draw_blocks::chart(f, lower[0], fd);
draw_blocks::ports(f, lower[1], fd);
}
if let Some((text, instant)) = fd.info_text {
draw_blocks::info(f, &text, instant, gui_state);
if let Some((text, instant)) = fd.info_text.as_ref() {
draw_blocks::info(f, text.to_owned(), instant, gui_state);
}
// Check if error, and show popup if so
if fd.help_visible {
if fd.status.contains(&Status::Help) {
draw_blocks::help_box(f);
}