chore: merge release-v0.1.0 into main
This commit is contained in:
+6
-14
@@ -1,19 +1,11 @@
|
|||||||
### 2022-07-06
|
### 2022-07-23
|
||||||
|
|
||||||
### Docs
|
### Chores
|
||||||
+ readme update, [f29e29ad151ddf424ba630e6d33edf19acfd7636]
|
+ dependencies updated, [cf7e02dde94f69832a2e485b99785afc66a5bc15]
|
||||||
+ comments improved, [1674db8a20aafa447732deb2e44ac8b97cf0471b]
|
|
||||||
+ readme logo size, [a733efa65865e04d9ec86c7ca8785dfbae635695]
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
+ Remove unwraps(), [61db81ecfe5684ddb8a360715f43357a042162c0]
|
|
||||||
+ Help menu alt+tab > shift+tab typo, thanks [siph](https://github.com/siph), [04466803481b75feb7d7f275248279fdb8729862]
|
|
||||||
|
|
||||||
### Refactors
|
|
||||||
+ tokio spawns, [1fd230f2f3cf4e376058359515e76f4fa6e425c2]
|
|
||||||
+ max_line_width(), [a5d7dabbd68dc15a081df33352ce3b55d9a9891c]
|
|
||||||
+ create_release dead code removed, [297979c197c2defd409053d8da724f922b0bba1b]
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
+ Enable sorting of containers by each, and every, heading. Either via keyboard or mouse, closes [#3], [a6c296f2cde56cf241bcd696cab8bd477270e5f4]
|
||||||
|
+ Spawn & track docker information update requests, multiple identical requests cannot be executed, [740c059b276f35acd1cb03f1030134646bf8a07d]
|
||||||
|
|
||||||
|
|
||||||
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
|
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 426 KiB |
@@ -1,3 +1,13 @@
|
|||||||
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.0'>v0.1.0</a>
|
||||||
|
### 2022-07-23
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
+ dependencies updated, [cf7e02dd](https://github.com/mrjackwills/oxker/commit/cf7e02dde94f69832a2e485b99785afc66a5bc15),
|
||||||
|
|
||||||
|
### Features
|
||||||
|
+ Enable sorting of containers by each, and every, heading. Either via keyboard or mouse, closes [#3](https://github.com/mrjackwills/oxker/issues/3), [a6c296f2](https://github.com/mrjackwills/oxker/commit/a6c296f2cde56cf241bcd696cab8bd477270e5f4),
|
||||||
|
+ Spawn & track docker information update requests, multiple identical requests cannot be executed, [740c059b](https://github.com/mrjackwills/oxker/commit/740c059b276f35acd1cb03f1030134646bf8a07d),
|
||||||
|
|
||||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.6'>v0.0.6</a>
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.6'>v0.0.6</a>
|
||||||
### 2022-07-06
|
### 2022-07-06
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "oxker"
|
name = "oxker"
|
||||||
version = "0.0.6"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Jack Wills <email@mrjackwills.com>"]
|
authors = ["Jack Wills <email@mrjackwills.com>"]
|
||||||
description = "a simple tui to view & control docker containers"
|
description = "a simple tui to view & control docker containers"
|
||||||
@@ -10,13 +10,13 @@ readme = "README.md"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bollard = "0.12.0"
|
bollard = "0.13"
|
||||||
cansi = "2.1"
|
cansi = "2.1"
|
||||||
clap={version="3.1", features = ["derive", "unicode"] }
|
clap={version="3.2", features = ["derive", "unicode"] }
|
||||||
crossterm = "0.23"
|
crossterm = "0.24"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
parking_lot = {version= "0.12"}
|
parking_lot = {version= "0.12"}
|
||||||
tokio = {version = "1.19", features=["full"]}
|
tokio = {version = "1.20", features=["full"]}
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
tui = "0.18"
|
tui = "0.18"
|
||||||
|
|||||||
@@ -38,10 +38,23 @@ rm oxker_linux_x86_64.tar.gz oxker
|
|||||||
|
|
||||||
```oxker```
|
```oxker```
|
||||||
|
|
||||||
|
In application controls
|
||||||
|
| button| result|
|
||||||
|
|--|--|
|
||||||
|
| ```( tab )``` or ```( shift+tab )``` | Change panel, clicking on a panel also changes the selected panel|
|
||||||
|
| ```( ↑ ↓ )``` or ```( j k )``` or ```(PgUp PgDown)``` or ```(Home End)```| Change selected line in selected panel, mouse scroll also changes selected line |
|
||||||
|
| ```( enter )```| execute selected docker command|
|
||||||
|
| ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column |
|
||||||
|
| ```( 0 )``` | stop sorting |
|
||||||
|
| ```( h )``` | Show help menu |
|
||||||
|
| ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected|
|
||||||
|
| ```( q )``` | to quit at any time |
|
||||||
|
|
||||||
|
|
||||||
available command line arguments
|
available command line arguments
|
||||||
| argument|result|
|
| argument|result|
|
||||||
|--|--|
|
|--|--|
|
||||||
|```-d [number > 0]```| set the update interval for docker information, in ms, defaults to 1000 (1 second) |
|
|```-d [number > 0]```| set the minimum update interval for docker information, in ms, defaults to 1000 (1 second) |
|
||||||
|```-r```| Show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) |
|
|```-r```| Show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) |
|
||||||
|```-c```| Attempt to color the logs (conflicts with -r) |
|
|```-c```| Attempt to color the logs (conflicts with -r) |
|
||||||
|```-t```| Remove timestamps from each log entry |
|
|```-t```| Remove timestamps from each log entry |
|
||||||
|
|||||||
+1
-1
@@ -183,7 +183,6 @@ cargo_test () {
|
|||||||
release_flow() {
|
release_flow() {
|
||||||
check_git
|
check_git
|
||||||
get_git_remote_url
|
get_git_remote_url
|
||||||
cargo fmt
|
|
||||||
cargo_test
|
cargo_test
|
||||||
cd "${CWD}" || error_close "Can't find ${CWD}"
|
cd "${CWD}" || error_close "Can't find ${CWD}"
|
||||||
check_tag
|
check_tag
|
||||||
@@ -195,6 +194,7 @@ release_flow() {
|
|||||||
ask_changelog_update
|
ask_changelog_update
|
||||||
git checkout -b "$RELEASE_BRANCH"
|
git checkout -b "$RELEASE_BRANCH"
|
||||||
update_version_number_in_files
|
update_version_number_in_files
|
||||||
|
cargo fmt
|
||||||
git add .
|
git add .
|
||||||
git commit -m "chore: release $NEW_TAG_WITH_V"
|
git commit -m "chore: release $NEW_TAG_WITH_V"
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use tui::{
|
|||||||
widgets::{ListItem, ListState},
|
widgets::{ListItem, ListState},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::Header;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StatefulList<T> {
|
pub struct StatefulList<T> {
|
||||||
pub state: ListState,
|
pub state: ListState,
|
||||||
@@ -102,6 +104,18 @@ impl State {
|
|||||||
_ => Color::Red,
|
_ => Color::Red,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Dirty way to create order for the state, rather than impl Ord
|
||||||
|
pub fn order(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Running => "a",
|
||||||
|
Self::Paused => "b",
|
||||||
|
Self::Restarting => "c",
|
||||||
|
Self::Removing => "d",
|
||||||
|
Self::Exited => "e",
|
||||||
|
Self::Dead => "f",
|
||||||
|
Self::Unknown => "g",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for State {
|
impl From<&str> for State {
|
||||||
@@ -304,8 +318,8 @@ pub struct ContainerItem {
|
|||||||
pub mem_limit: ByteStats,
|
pub mem_limit: ByteStats,
|
||||||
pub mem_stats: VecDeque<ByteStats>,
|
pub mem_stats: VecDeque<ByteStats>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub net_rx: ByteStats,
|
pub rx: ByteStats,
|
||||||
pub net_tx: ByteStats,
|
pub tx: ByteStats,
|
||||||
pub state: State,
|
pub state: State,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
}
|
}
|
||||||
@@ -328,8 +342,8 @@ impl ContainerItem {
|
|||||||
mem_limit: ByteStats::new(0),
|
mem_limit: ByteStats::new(0),
|
||||||
mem_stats: VecDeque::with_capacity(60),
|
mem_stats: VecDeque::with_capacity(60),
|
||||||
name,
|
name,
|
||||||
net_rx: ByteStats::new(0),
|
rx: ByteStats::new(0),
|
||||||
net_tx: ByteStats::new(0),
|
tx: ByteStats::new(0),
|
||||||
state,
|
state,
|
||||||
status,
|
status,
|
||||||
}
|
}
|
||||||
@@ -397,31 +411,31 @@ impl ContainerItem {
|
|||||||
/// Container information panel headings + widths, for nice pretty formatting
|
/// Container information panel headings + widths, for nice pretty formatting
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Columns {
|
pub struct Columns {
|
||||||
pub state: (String, usize),
|
pub state: (Header, usize),
|
||||||
pub status: (String, usize),
|
pub status: (Header, usize),
|
||||||
pub cpu: (String, usize),
|
pub cpu: (Header, usize),
|
||||||
pub mem: (String, usize),
|
pub mem: (Header, usize),
|
||||||
pub id: (String, usize),
|
pub id: (Header, usize),
|
||||||
pub name: (String, usize),
|
pub name: (Header, usize),
|
||||||
pub image: (String, usize),
|
pub image: (Header, usize),
|
||||||
pub net_rx: (String, usize),
|
pub net_rx: (Header, usize),
|
||||||
pub net_tx: (String, usize),
|
pub net_tx: (Header, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Columns {
|
impl Columns {
|
||||||
//. (Column titles, minimum header string length)
|
// (Column titles, minimum header string length)
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: (String::from("state"), 11),
|
state: (Header::State, 11),
|
||||||
status: (String::from("status"), 16),
|
status: (Header::Status, 16),
|
||||||
// 7 to allow for "100.00%"
|
// 7 to allow for "100.00%"
|
||||||
cpu: (String::from("cpu"), 7),
|
cpu: (Header::Cpu, 7),
|
||||||
mem: (String::from("memory/limit"), 12),
|
mem: (Header::Memory, 12),
|
||||||
id: (String::from("id"), 8),
|
id: (Header::Id, 8),
|
||||||
name: (String::from("name"), 4),
|
name: (Header::Name, 4),
|
||||||
image: (String::from("image"), 5),
|
image: (Header::Image, 5),
|
||||||
net_rx: (String::from("↓ rx"), 5),
|
net_rx: (Header::Rx, 5),
|
||||||
net_tx: (String::from("↑ tx"), 5),
|
net_tx: (Header::Tx, 5),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+140
-16
@@ -1,4 +1,5 @@
|
|||||||
use bollard::models::ContainerSummary;
|
use bollard::models::ContainerSummary;
|
||||||
|
use core::fmt;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tui::widgets::ListItem;
|
use tui::widgets::ListItem;
|
||||||
|
|
||||||
@@ -16,9 +17,63 @@ pub struct AppData {
|
|||||||
pub containers: StatefulList<ContainerItem>,
|
pub containers: StatefulList<ContainerItem>,
|
||||||
pub init: bool,
|
pub init: bool,
|
||||||
pub show_error: bool,
|
pub show_error: bool,
|
||||||
|
sorted_by: Option<(Header, SortedOrder)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum SortedOrder {
|
||||||
|
Asc,
|
||||||
|
Desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
|
||||||
|
pub enum Header {
|
||||||
|
State,
|
||||||
|
Status,
|
||||||
|
Cpu,
|
||||||
|
Memory,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
Image,
|
||||||
|
Rx,
|
||||||
|
Tx,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert errors into strings to display
|
||||||
|
impl fmt::Display for Header {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let disp = match self {
|
||||||
|
Self::State => "state",
|
||||||
|
Self::Status => "status",
|
||||||
|
Self::Cpu => "cpu",
|
||||||
|
Self::Memory => "memory/limit",
|
||||||
|
Self::Id => "id",
|
||||||
|
Self::Name => "name",
|
||||||
|
Self::Image => "image",
|
||||||
|
Self::Rx => "↓ rx",
|
||||||
|
Self::Tx => "↑ tx",
|
||||||
|
};
|
||||||
|
write!(f, "{:>x$}", disp, x = f.width().unwrap_or(1))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
|
pub fn get_sorted(&self) -> Option<(Header, SortedOrder)> {
|
||||||
|
self.sorted_by.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the sorted order, also set the selected container state to match new order
|
||||||
|
pub fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
|
||||||
|
self.sorted_by = x;
|
||||||
|
let id = self.get_selected_container_id();
|
||||||
|
self.sort_containers();
|
||||||
|
self.containers.state.select(
|
||||||
|
self.containers
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.position(|i| Some(i.id.to_owned()) == id),
|
||||||
|
);
|
||||||
|
}
|
||||||
/// Generate a default app_state
|
/// Generate a default app_state
|
||||||
pub fn default(args: CliArgs) -> Self {
|
pub fn default(args: CliArgs) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -28,6 +83,7 @@ impl AppData {
|
|||||||
init: false,
|
init: false,
|
||||||
logs_parsed: false,
|
logs_parsed: false,
|
||||||
show_error: false,
|
show_error: false,
|
||||||
|
sorted_by: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +174,76 @@ impl AppData {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sort the containers vec, based on a heading, either ascending or descending
|
||||||
|
pub fn sort_containers(&mut self) {
|
||||||
|
if let Some((head, so)) = self.sorted_by.as_ref() {
|
||||||
|
match head {
|
||||||
|
Header::State => match so {
|
||||||
|
SortedOrder::Desc => self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.sort_by(|a, b| a.state.order().cmp(b.state.order())),
|
||||||
|
SortedOrder::Asc => self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.sort_by(|a, b| b.state.order().cmp(a.state.order())),
|
||||||
|
},
|
||||||
|
Header::Status => match so {
|
||||||
|
SortedOrder::Asc => self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.sort_by(|a, b| a.status.cmp(&b.status)),
|
||||||
|
SortedOrder::Desc => self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.sort_by(|a, b| b.status.cmp(&a.status)),
|
||||||
|
},
|
||||||
|
Header::Cpu => match so {
|
||||||
|
SortedOrder::Asc => self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())),
|
||||||
|
SortedOrder::Desc => self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())),
|
||||||
|
},
|
||||||
|
Header::Memory => match so {
|
||||||
|
SortedOrder::Asc => self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())),
|
||||||
|
SortedOrder::Desc => self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())),
|
||||||
|
},
|
||||||
|
Header::Id => match so {
|
||||||
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)),
|
||||||
|
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)),
|
||||||
|
},
|
||||||
|
Header::Image => match so {
|
||||||
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)),
|
||||||
|
SortedOrder::Desc => {
|
||||||
|
self.containers.items.sort_by(|a, b| b.image.cmp(&a.image))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Header::Name => match so {
|
||||||
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)),
|
||||||
|
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)),
|
||||||
|
},
|
||||||
|
Header::Rx => match so {
|
||||||
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.rx.cmp(&b.rx)),
|
||||||
|
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.rx.cmp(&a.rx)),
|
||||||
|
},
|
||||||
|
Header::Tx => match so {
|
||||||
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.tx.cmp(&b.tx)),
|
||||||
|
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.tx.cmp(&a.tx)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the index of the currently selected single log line
|
/// Find the index of the currently selected single log line
|
||||||
pub fn get_selected_log_index(&self) -> Option<usize> {
|
pub fn get_selected_log_index(&self) -> Option<usize> {
|
||||||
let mut output = None;
|
let mut output = None;
|
||||||
@@ -203,8 +329,8 @@ impl AppData {
|
|||||||
container.mem_limit
|
container.mem_limit
|
||||||
));
|
));
|
||||||
|
|
||||||
let net_rx_count = count(&container.net_rx.to_string());
|
let net_rx_count = count(&container.rx.to_string());
|
||||||
let net_tx_count = count(&container.net_tx.to_string());
|
let net_tx_count = count(&container.tx.to_string());
|
||||||
let image_count = count(&container.image);
|
let image_count = count(&container.image);
|
||||||
let name_count = count(&container.name);
|
let name_count = count(&container.name);
|
||||||
let state_count = count(&container.state.to_string());
|
let state_count = count(&container.state.to_string());
|
||||||
@@ -277,8 +403,8 @@ impl AppData {
|
|||||||
container.mem_stats.push_back(ByteStats::new(mem));
|
container.mem_stats.push_back(ByteStats::new(mem));
|
||||||
}
|
}
|
||||||
|
|
||||||
container.net_rx.update(rx);
|
container.rx.update(rx);
|
||||||
container.net_tx.update(tx);
|
container.tx.update(tx);
|
||||||
container.mem_limit.update(mem_limit);
|
container.mem_limit.update(mem_limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,22 +490,27 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// update logs of a given container, based on index not id
|
/// update logs of a given container, based on id
|
||||||
pub fn update_log_by_index(&mut self, output: Vec<String>, index: usize) {
|
pub fn update_log_by_id(&mut self, output: Vec<String>, id: String) {
|
||||||
let tz = self.get_systemtime();
|
let tz = self.get_systemtime();
|
||||||
if let Some(container) = self.containers.items.get_mut(index) {
|
let color = self.args.color;
|
||||||
|
let raw = self.args.raw;
|
||||||
|
|
||||||
|
if let Some(container) = self.get_container_by_id(&id) {
|
||||||
container.last_updated = tz;
|
container.last_updated = tz;
|
||||||
let current_len = container.logs.items.len();
|
let current_len = container.logs.items.len();
|
||||||
|
|
||||||
output.iter().for_each(|i| {
|
output.iter().for_each(|i| {
|
||||||
let lines = if self.args.color {
|
let lines = if color {
|
||||||
log_sanitizer::colorize_logs(i.to_owned())
|
log_sanitizer::colorize_logs(i.to_owned())
|
||||||
} else if self.args.raw {
|
} else if raw {
|
||||||
log_sanitizer::raw(i.to_owned())
|
log_sanitizer::raw(i.to_owned())
|
||||||
} else {
|
} else {
|
||||||
log_sanitizer::remove_ansi(i.to_owned())
|
log_sanitizer::remove_ansi(i.to_owned())
|
||||||
};
|
};
|
||||||
container.logs.items.push(ListItem::new(lines));
|
container.logs.items.push(ListItem::new(lines));
|
||||||
});
|
});
|
||||||
|
|
||||||
if container.logs.state.selected().is_none()
|
if container.logs.state.selected().is_none()
|
||||||
|| container.logs.state.selected().unwrap_or_default() + 1 == current_len
|
|| container.logs.state.selected().unwrap_or_default() + 1 == current_len
|
||||||
{
|
{
|
||||||
@@ -388,11 +519,4 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
self.logs_parsed = true;
|
self.logs_parsed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all containers logs, should only be used on first initialisation
|
|
||||||
pub fn update_all_logs(&mut self, all_logs: Vec<Vec<String>>) {
|
|
||||||
for (index, output) in all_logs.into_iter().enumerate() {
|
|
||||||
self.update_log_by_index(output, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ pub enum DockerMessage {
|
|||||||
Pause(String),
|
Pause(String),
|
||||||
Unpause(String),
|
Unpause(String),
|
||||||
Stop(String),
|
Stop(String),
|
||||||
|
Quit,
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-14
@@ -2,9 +2,15 @@ use bollard::{
|
|||||||
container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions},
|
container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions},
|
||||||
Docker,
|
Docker,
|
||||||
};
|
};
|
||||||
use futures_util::{future::join_all, StreamExt};
|
use futures_util::StreamExt;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
};
|
||||||
use tokio::{sync::mpsc::Receiver, task::JoinHandle};
|
use tokio::{sync::mpsc::Receiver, task::JoinHandle};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -21,7 +27,9 @@ pub struct DockerData {
|
|||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
initialised: bool,
|
initialised: bool,
|
||||||
|
is_running: Arc<AtomicBool>,
|
||||||
receiver: Receiver<DockerMessage>,
|
receiver: Receiver<DockerMessage>,
|
||||||
|
spawns: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
|
||||||
timestamps: bool,
|
timestamps: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +63,13 @@ impl DockerData {
|
|||||||
|
|
||||||
/// Get a single docker stat in order to update mem and cpu usage
|
/// Get a single docker stat in order to update mem and cpu usage
|
||||||
/// don't take &self, so that can tokio::spawn into it's own thread
|
/// don't take &self, so that can tokio::spawn into it's own thread
|
||||||
|
/// remove if from spawns hashmap when complete
|
||||||
async fn update_container_stat(
|
async fn update_container_stat(
|
||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
id: String,
|
id: String,
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
is_running: bool,
|
is_running: bool,
|
||||||
|
spawns: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
|
||||||
) {
|
) {
|
||||||
let mut stream = docker
|
let mut stream = docker
|
||||||
.stats(
|
.stats(
|
||||||
@@ -107,6 +117,7 @@ impl DockerData {
|
|||||||
.lock()
|
.lock()
|
||||||
.update_stats(id.clone(), None, None, mem_limit, rx, tx);
|
.update_stats(id.clone(), None, None, mem_limit, rx, tx);
|
||||||
}
|
}
|
||||||
|
spawns.lock().remove(&id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,17 +126,26 @@ impl DockerData {
|
|||||||
for (is_running, id) in all_ids.iter() {
|
for (is_running, id) in all_ids.iter() {
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let app_data = Arc::clone(&self.app_data);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
|
let spawns = Arc::clone(&self.spawns);
|
||||||
let is_running = *is_running;
|
let is_running = *is_running;
|
||||||
let id = id.to_owned();
|
let id = id.to_owned();
|
||||||
tokio::spawn(Self::update_container_stat(
|
|
||||||
docker, id, app_data, is_running,
|
let spawn_contains_id = spawns.lock().contains_key(&id);
|
||||||
|
let s = tokio::spawn(Self::update_container_stat(
|
||||||
|
docker,
|
||||||
|
id.to_owned(),
|
||||||
|
app_data,
|
||||||
|
is_running,
|
||||||
|
spawns,
|
||||||
));
|
));
|
||||||
|
if !spawn_contains_id {
|
||||||
|
self.spawns.lock().insert(id, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here
|
/// 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
|
/// Just make sure that items sent are guaranteed to have an id
|
||||||
/// return Vec<(is_running, id)>
|
|
||||||
pub async fn update_all_containers(&mut self) -> Vec<(bool, String)> {
|
pub async fn update_all_containers(&mut self) -> Vec<(bool, String)> {
|
||||||
let containers = self
|
let containers = self
|
||||||
.docker
|
.docker
|
||||||
@@ -144,6 +164,10 @@ impl DockerData {
|
|||||||
.for_each(|c| output.push(c.to_owned()));
|
.for_each(|c| output.push(c.to_owned()));
|
||||||
|
|
||||||
self.app_data.lock().update_containers(&output);
|
self.app_data.lock().update_containers(&output);
|
||||||
|
|
||||||
|
let current_sort = self.app_data.lock().get_sorted();
|
||||||
|
self.app_data.lock().set_sorted(current_sort);
|
||||||
|
|
||||||
output
|
output
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|i| {
|
.filter_map(|i| {
|
||||||
@@ -159,12 +183,15 @@ impl DockerData {
|
|||||||
|
|
||||||
/// Update single container logs
|
/// Update single container logs
|
||||||
/// don't take &self, so that can tokio::spawn into it's own thread
|
/// don't take &self, so that can tokio::spawn into it's own thread
|
||||||
|
/// remove if from spawns hashmap when complete
|
||||||
async fn update_log(
|
async fn update_log(
|
||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
id: String,
|
id: String,
|
||||||
timestamps: bool,
|
timestamps: bool,
|
||||||
since: i64,
|
since: i64,
|
||||||
) -> Vec<String> {
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
spawns: Arc<Mutex<HashMap<String, JoinHandle<()>>>>,
|
||||||
|
) {
|
||||||
let options = Some(LogsOptions::<String> {
|
let options = Some(LogsOptions::<String> {
|
||||||
stdout: true,
|
stdout: true,
|
||||||
timestamps,
|
timestamps,
|
||||||
@@ -184,21 +211,25 @@ impl DockerData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output
|
spawns.lock().remove(&id);
|
||||||
|
app_data.lock().update_log_by_id(output, id.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all logs, spawn each container into own tokio::spawn thread
|
/// Update all logs, spawn each container into own tokio::spawn thread
|
||||||
async fn init_all_logs(&mut self, all_ids: &[(bool, String)]) {
|
async fn init_all_logs(&mut self, all_ids: &[(bool, String)]) {
|
||||||
let mut handles = vec![];
|
|
||||||
|
|
||||||
for (_, id) in all_ids.iter() {
|
for (_, id) in all_ids.iter() {
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let timestamps = self.timestamps;
|
let timestamps = self.timestamps;
|
||||||
let id = id.to_owned();
|
let id = id.to_owned();
|
||||||
handles.push(Self::update_log(docker, id, timestamps, 0));
|
let app_data = Arc::clone(&self.app_data);
|
||||||
|
let spawns = Arc::clone(&self.spawns);
|
||||||
|
self.spawns.lock().insert(
|
||||||
|
id.to_owned(),
|
||||||
|
tokio::spawn(Self::update_log(
|
||||||
|
docker, id, timestamps, 0, app_data, spawns,
|
||||||
|
)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let all_logs = join_all(handles).await;
|
|
||||||
self.app_data.lock().update_all_logs(all_logs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_everything(&mut self) {
|
async fn update_everything(&mut self) {
|
||||||
@@ -206,11 +237,26 @@ impl DockerData {
|
|||||||
let optional_index = self.app_data.lock().get_selected_log_index();
|
let optional_index = self.app_data.lock().get_selected_log_index();
|
||||||
if let Some(index) = optional_index {
|
if let Some(index) = optional_index {
|
||||||
let id = self.app_data.lock().containers.items[index].id.to_owned();
|
let id = self.app_data.lock().containers.items[index].id.to_owned();
|
||||||
|
|
||||||
|
let running = self.spawns.lock().contains_key(&id);
|
||||||
|
|
||||||
|
if !running {
|
||||||
let since = self.app_data.lock().containers.items[index].last_updated as i64;
|
let since = self.app_data.lock().containers.items[index].last_updated as i64;
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let timestamps = self.timestamps;
|
let timestamps = self.timestamps;
|
||||||
let logs = Self::update_log(docker, id, timestamps, since).await;
|
|
||||||
self.app_data.lock().update_log_by_index(logs, index);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
|
let spawns = Arc::clone(&self.spawns);
|
||||||
|
let s = tokio::spawn(Self::update_log(
|
||||||
|
docker,
|
||||||
|
id.to_owned(),
|
||||||
|
timestamps,
|
||||||
|
since,
|
||||||
|
app_data,
|
||||||
|
spawns,
|
||||||
|
));
|
||||||
|
self.spawns.lock().insert(id, s);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.update_all_container_stats(&all_ids).await;
|
self.update_all_container_stats(&all_ids).await;
|
||||||
@@ -315,6 +361,14 @@ impl DockerData {
|
|||||||
self.update_everything().await
|
self.update_everything().await
|
||||||
}
|
}
|
||||||
DockerMessage::Update => self.update_everything().await,
|
DockerMessage::Update => self.update_everything().await,
|
||||||
|
DockerMessage::Quit => {
|
||||||
|
self.spawns
|
||||||
|
.lock()
|
||||||
|
.values()
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|i| i.abort());
|
||||||
|
self.is_running.store(false, Ordering::SeqCst);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,6 +380,7 @@ impl DockerData {
|
|||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
receiver: Receiver<DockerMessage>,
|
receiver: Receiver<DockerMessage>,
|
||||||
|
is_running: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
if app_data.lock().get_error().is_none() {
|
if app_data.lock().get_error().is_none() {
|
||||||
let mut inner = Self {
|
let mut inner = Self {
|
||||||
@@ -334,7 +389,9 @@ impl DockerData {
|
|||||||
gui_state,
|
gui_state,
|
||||||
initialised: false,
|
initialised: false,
|
||||||
receiver,
|
receiver,
|
||||||
|
spawns: Arc::new(Mutex::new(HashMap::new())),
|
||||||
timestamps: args.timestamp,
|
timestamps: args.timestamp,
|
||||||
|
is_running,
|
||||||
};
|
};
|
||||||
inner.initialise_container_data().await;
|
inner.initialise_container_data().await;
|
||||||
|
|
||||||
|
|||||||
+77
-11
@@ -18,7 +18,7 @@ use tui::layout::Rect;
|
|||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, DockerControls},
|
app_data::{AppData, DockerControls, Header, SortedOrder},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
docker_data::DockerMessage,
|
docker_data::DockerMessage,
|
||||||
ui::{GuiState, SelectablePanel},
|
ui::{GuiState, SelectablePanel},
|
||||||
@@ -77,6 +77,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mouse button
|
||||||
fn m_button(&mut self) {
|
fn m_button(&mut self) {
|
||||||
if self.mouse_capture {
|
if self.mouse_capture {
|
||||||
match execute!(std::io::stdout(), DisableMouseCapture) {
|
match execute!(std::io::stdout(), DisableMouseCapture) {
|
||||||
@@ -115,6 +116,26 @@ impl InputHandler {
|
|||||||
self.mouse_capture = !self.mouse_capture;
|
self.mouse_capture = !self.mouse_capture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sort containers based on a given header, switch asc to desc if already sorted, else always desc
|
||||||
|
fn sort(&self, header: Header) {
|
||||||
|
let mut output = Some((header.to_owned(), SortedOrder::Desc));
|
||||||
|
let mut locked_data = self.app_data.lock();
|
||||||
|
if let Some((h, order)) = locked_data.get_sorted().as_ref() {
|
||||||
|
if &SortedOrder::Desc == order && h == &header {
|
||||||
|
output = Some((header, SortedOrder::Asc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
locked_data.set_sorted(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a quit message to docker, to abort all spawns, if error, quit here instead
|
||||||
|
async fn quit(&self) {
|
||||||
|
match self.docker_sender.send(DockerMessage::Quit).await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(_) => self.is_running.store(false, Ordering::SeqCst),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle any keyboard button events
|
/// Handle any keyboard button events
|
||||||
async fn button_press(&mut self, key_code: KeyCode) {
|
async fn button_press(&mut self, key_code: KeyCode) {
|
||||||
let show_error = self.app_data.lock().show_error;
|
let show_error = self.app_data.lock().show_error;
|
||||||
@@ -122,9 +143,7 @@ impl InputHandler {
|
|||||||
|
|
||||||
if show_error {
|
if show_error {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => self.quit().await,
|
||||||
self.is_running.store(false, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
KeyCode::Char('c') => {
|
KeyCode::Char('c') => {
|
||||||
self.app_data.lock().show_error = false;
|
self.app_data.lock().show_error = false;
|
||||||
self.app_data.lock().remove_error();
|
self.app_data.lock().remove_error();
|
||||||
@@ -133,18 +152,54 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
} else if show_info {
|
} else if show_info {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst),
|
KeyCode::Char('q') => self.quit().await,
|
||||||
KeyCode::Char('h') => self.gui_state.lock().show_help = false,
|
KeyCode::Char('h') => self.gui_state.lock().show_help = false,
|
||||||
KeyCode::Char('m') => self.m_button(),
|
KeyCode::Char('m') => self.m_button(),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst),
|
KeyCode::Char('0') => self.app_data.lock().set_sorted(None),
|
||||||
|
KeyCode::Char('1') => self.sort(Header::State),
|
||||||
|
KeyCode::Char('2') => self.sort(Header::Status),
|
||||||
|
KeyCode::Char('3') => self.sort(Header::Cpu),
|
||||||
|
KeyCode::Char('4') => self.sort(Header::Memory),
|
||||||
|
KeyCode::Char('5') => self.sort(Header::Id),
|
||||||
|
KeyCode::Char('6') => self.sort(Header::Name),
|
||||||
|
KeyCode::Char('7') => self.sort(Header::Image),
|
||||||
|
KeyCode::Char('8') => self.sort(Header::Rx),
|
||||||
|
KeyCode::Char('9') => self.sort(Header::Tx),
|
||||||
|
KeyCode::Char('q') => self.quit().await,
|
||||||
KeyCode::Char('h') => self.gui_state.lock().show_help = true,
|
KeyCode::Char('h') => self.gui_state.lock().show_help = true,
|
||||||
KeyCode::Char('m') => self.m_button(),
|
KeyCode::Char('m') => self.m_button(),
|
||||||
KeyCode::Tab => self.gui_state.lock().next_panel(),
|
KeyCode::Tab => {
|
||||||
KeyCode::BackTab => self.gui_state.lock().previous_panel(),
|
// Skip control panel if no containers, could be refactored
|
||||||
|
let has_containers = self.app_data.lock().get_container_len() == 0;
|
||||||
|
let is_containers =
|
||||||
|
self.gui_state.lock().selected_panel == SelectablePanel::Containers;
|
||||||
|
let count = if has_containers && is_containers {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
for _ in 0..count {
|
||||||
|
self.gui_state.lock().next_panel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::BackTab => {
|
||||||
|
// Skip control panel if no containers, could be refactored
|
||||||
|
let has_containers = self.app_data.lock().get_container_len() == 0;
|
||||||
|
let is_containers =
|
||||||
|
self.gui_state.lock().selected_panel == SelectablePanel::Logs;
|
||||||
|
let count = if has_containers && is_containers {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
for _ in 0..count {
|
||||||
|
self.gui_state.lock().previous_panel();
|
||||||
|
}
|
||||||
|
}
|
||||||
KeyCode::Home => {
|
KeyCode::Home => {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
match self.gui_state.lock().selected_panel {
|
match self.gui_state.lock().selected_panel {
|
||||||
@@ -224,7 +279,18 @@ impl InputHandler {
|
|||||||
MouseEventKind::ScrollUp => self.previous(),
|
MouseEventKind::ScrollUp => self.previous(),
|
||||||
MouseEventKind::ScrollDown => self.next(),
|
MouseEventKind::ScrollDown => self.next(),
|
||||||
MouseEventKind::Down(MouseButton::Left) => {
|
MouseEventKind::Down(MouseButton::Left) => {
|
||||||
self.gui_state.lock().rect_insersects(Rect::new(
|
let header_intersects = self.gui_state.lock().header_intersect(Rect::new(
|
||||||
|
mouse_event.column,
|
||||||
|
mouse_event.row,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(header) = header_intersects {
|
||||||
|
self.sort(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.gui_state.lock().panel_intersect(Rect::new(
|
||||||
mouse_event.column,
|
mouse_event.column,
|
||||||
mouse_event.row,
|
mouse_event.row,
|
||||||
1,
|
1,
|
||||||
@@ -235,7 +301,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change state of selected container
|
/// Change state to next, depending which panel is currently in focus
|
||||||
fn next(&mut self) {
|
fn next(&mut self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
match self.gui_state.lock().selected_panel {
|
match self.gui_state.lock().selected_panel {
|
||||||
@@ -245,7 +311,7 @@ impl InputHandler {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change state of selected container
|
/// Change state to previous, depending which panel is currently in focus
|
||||||
fn previous(&mut self) {
|
fn previous(&mut self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
match self.gui_state.lock().selected_panel {
|
match self.gui_state.lock().selected_panel {
|
||||||
|
|||||||
+3
-1
@@ -26,6 +26,7 @@ async fn main() {
|
|||||||
let args = CliArgs::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(args.clone())));
|
||||||
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
||||||
|
let is_running = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
let docker_args = args.clone();
|
let docker_args = args.clone();
|
||||||
let docker_app_data = Arc::clone(&app_data);
|
let docker_app_data = Arc::clone(&app_data);
|
||||||
@@ -38,12 +39,14 @@ async fn main() {
|
|||||||
match docker.ping().await {
|
match docker.ping().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let docker = Arc::clone(&docker);
|
let docker = Arc::clone(&docker);
|
||||||
|
let is_running = Arc::clone(&is_running);
|
||||||
tokio::spawn(DockerData::init(
|
tokio::spawn(DockerData::init(
|
||||||
docker_args,
|
docker_args,
|
||||||
docker_app_data,
|
docker_app_data,
|
||||||
docker,
|
docker,
|
||||||
docker_gui_state,
|
docker_gui_state,
|
||||||
docker_rx,
|
docker_rx,
|
||||||
|
is_running,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Err(_) => app_data.lock().set_error(AppError::DockerConnect),
|
Err(_) => app_data.lock().set_error(AppError::DockerConnect),
|
||||||
@@ -53,7 +56,6 @@ async fn main() {
|
|||||||
|
|
||||||
let (input_sx, input_rx) = tokio::sync::mpsc::channel(16);
|
let (input_sx, input_rx) = tokio::sync::mpsc::channel(16);
|
||||||
|
|
||||||
let is_running = Arc::new(AtomicBool::new(true));
|
|
||||||
let input_is_running = Arc::clone(&is_running);
|
let input_is_running = Arc::clone(&is_running);
|
||||||
let input_gui_state = Arc::clone(&gui_state);
|
let input_gui_state = Arc::clone(&gui_state);
|
||||||
let input_docker_sender = docker_sx.clone();
|
let input_docker_sender = docker_sx.clone();
|
||||||
|
|||||||
+109
-76
@@ -14,12 +14,13 @@ use tui::{
|
|||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::app_data::{Header, SortedOrder};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::gui_state::BoxLocation;
|
use super::gui_state::{BoxLocation, Region};
|
||||||
use super::{GuiState, SelectablePanel};
|
use super::{GuiState, SelectablePanel};
|
||||||
|
|
||||||
const NAME_TEXT: &str = r#"
|
const NAME_TEXT: &str = r#"
|
||||||
@@ -47,7 +48,7 @@ fn generate_block<'a>(
|
|||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
panel: SelectablePanel,
|
panel: SelectablePanel,
|
||||||
) -> Block<'a> {
|
) -> Block<'a> {
|
||||||
gui_state.lock().insert_into_area_map(panel, area);
|
gui_state.lock().update_map(Region::Panel(panel), area);
|
||||||
let mut block = Block::default()
|
let mut block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded);
|
.border_type(BorderType::Rounded);
|
||||||
@@ -145,7 +146,7 @@ pub fn draw_containers<B: Backend>(
|
|||||||
state_style,
|
state_style,
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}{:>width$}", MARGIN, i.status, width = widths.status.1),
|
format!("{}{:>width$}", MARGIN, i.status, width = &widths.status.1),
|
||||||
state_style,
|
state_style,
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
@@ -153,12 +154,12 @@ pub fn draw_containers<B: Backend>(
|
|||||||
"{}{:>width$}",
|
"{}{:>width$}",
|
||||||
MARGIN,
|
MARGIN,
|
||||||
i.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)),
|
i.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)),
|
||||||
width = widths.cpu.1
|
width = &widths.cpu.1
|
||||||
),
|
),
|
||||||
state_style,
|
state_style,
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}{:>width$}", MARGIN, mems, width = widths.mem.1),
|
format!("{}{:>width$}", MARGIN, mems, width = &widths.mem.1),
|
||||||
state_style,
|
state_style,
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
@@ -166,7 +167,7 @@ pub fn draw_containers<B: Backend>(
|
|||||||
"{}{:>width$}",
|
"{}{:>width$}",
|
||||||
MARGIN,
|
MARGIN,
|
||||||
i.id.chars().take(8).collect::<String>(),
|
i.id.chars().take(8).collect::<String>(),
|
||||||
width = widths.id.1
|
width = &widths.id.1
|
||||||
),
|
),
|
||||||
blue,
|
blue,
|
||||||
),
|
),
|
||||||
@@ -179,18 +180,17 @@ pub fn draw_containers<B: Backend>(
|
|||||||
blue,
|
blue,
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}{:>width$}", MARGIN, i.net_rx, width = widths.net_rx.1),
|
format!("{}{:>width$}", MARGIN, i.rx, width = widths.net_rx.1),
|
||||||
Style::default().fg(Color::Rgb(255, 233, 193)),
|
Style::default().fg(Color::Rgb(255, 233, 193)),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}{:>width$}", MARGIN, i.net_tx, width = widths.net_tx.1),
|
format!("{}{:>width$}", MARGIN, i.tx, width = widths.net_tx.1),
|
||||||
Style::default().fg(Color::Rgb(205, 140, 140)),
|
Style::default().fg(Color::Rgb(205, 140, 140)),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
ListItem::new(lines)
|
ListItem::new(lines)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
let debug_text = String::from("no containers running");
|
let debug_text = String::from("no containers running");
|
||||||
let paragraph = Paragraph::new(debug_text)
|
let paragraph = Paragraph::new(debug_text)
|
||||||
@@ -357,81 +357,104 @@ pub fn draw_heading_bar<B: Backend>(
|
|||||||
f: &mut Frame<'_, B>,
|
f: &mut Frame<'_, B>,
|
||||||
has_containers: bool,
|
has_containers: bool,
|
||||||
loading_icon: String,
|
loading_icon: String,
|
||||||
info_visible: bool,
|
sorted_by: Option<(Header, SortedOrder)>,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
) {
|
) {
|
||||||
let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black));
|
let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black));
|
||||||
|
let info_visible = gui_state.lock().show_help;
|
||||||
|
|
||||||
f.render_widget(block(), area);
|
f.render_widget(block(), area);
|
||||||
|
|
||||||
let mut column_headings = format!(
|
// Generate a bloack for the header, if the header is currently being used to sort a column, then highlight it white
|
||||||
" {}{:>width$}",
|
let header_block = |x: &Header| {
|
||||||
|
let mut color = Color::Black;
|
||||||
|
let mut suffix = "";
|
||||||
|
let mut suffix_margin = 0;
|
||||||
|
if let Some((a, b)) = sorted_by.as_ref() {
|
||||||
|
if x == a {
|
||||||
|
match b {
|
||||||
|
SortedOrder::Asc => suffix = " ⌃",
|
||||||
|
SortedOrder::Desc => suffix = " ⌄",
|
||||||
|
}
|
||||||
|
suffix_margin = 2;
|
||||||
|
color = Color::White
|
||||||
|
};
|
||||||
|
};
|
||||||
|
(
|
||||||
|
Block::default().style(Style::default().bg(Color::Magenta).fg(color)),
|
||||||
|
suffix,
|
||||||
|
suffix_margin,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
let gen_header = |header: &Header, width: usize| {
|
||||||
|
let block = header_block(header);
|
||||||
|
let text = match header {
|
||||||
|
Header::State => format!(
|
||||||
|
" {}{:>width$}{ic}",
|
||||||
loading_icon,
|
loading_icon,
|
||||||
columns.state.0,
|
header,
|
||||||
width = columns.state.1
|
ic = block.1,
|
||||||
);
|
width = width - block.2,
|
||||||
column_headings.push_str(
|
),
|
||||||
format!(
|
Header::Status => format!(
|
||||||
"{} {:>width$}",
|
"{} {:>width$}{ic}",
|
||||||
MARGIN,
|
MARGIN,
|
||||||
columns.status.0,
|
header,
|
||||||
width = columns.status.1
|
ic = block.1,
|
||||||
)
|
width = width - block.2
|
||||||
.as_str(),
|
),
|
||||||
);
|
|
||||||
column_headings
|
_ => format!(
|
||||||
.push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str());
|
"{}{:>width$}{ic}",
|
||||||
column_headings
|
|
||||||
.push_str(format!("{}{:>width$}", MARGIN, columns.mem.0, width = columns.mem.1).as_str());
|
|
||||||
column_headings
|
|
||||||
.push_str(format!("{}{:>width$}", MARGIN, columns.id.0, width = columns.id.1).as_str());
|
|
||||||
column_headings.push_str(
|
|
||||||
format!(
|
|
||||||
"{}{:>width$}",
|
|
||||||
MARGIN,
|
MARGIN,
|
||||||
columns.name.0,
|
header,
|
||||||
width = columns.name.1
|
ic = block.1,
|
||||||
|
width = width - block.2
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let count = text.chars().count() as u16;
|
||||||
|
let status = Paragraph::new(text)
|
||||||
|
.block(block.0)
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
(status, count)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Meta data for iterate over to create blocks and correct widths
|
||||||
|
let header_meta = [
|
||||||
|
(Header::State, columns.state.1),
|
||||||
|
(Header::Status, columns.status.1),
|
||||||
|
(Header::Cpu, columns.cpu.1),
|
||||||
|
(Header::Memory, columns.mem.1),
|
||||||
|
(Header::Id, columns.id.1),
|
||||||
|
(Header::Name, columns.name.1),
|
||||||
|
(Header::Image, columns.image.1),
|
||||||
|
(Header::Rx, columns.net_rx.1),
|
||||||
|
(Header::Tx, columns.net_tx.1),
|
||||||
|
];
|
||||||
|
|
||||||
|
let header_data = header_meta
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
let header_block = gen_header(&i.0, i.1);
|
||||||
|
(
|
||||||
|
header_block.0,
|
||||||
|
i.0.to_owned(),
|
||||||
|
Constraint::Max(header_block.1),
|
||||||
)
|
)
|
||||||
.as_str(),
|
})
|
||||||
);
|
.collect::<Vec<_>>();
|
||||||
column_headings.push_str(
|
|
||||||
format!(
|
|
||||||
"{}{:>width$}",
|
|
||||||
MARGIN,
|
|
||||||
columns.image.0,
|
|
||||||
width = columns.image.1
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
column_headings.push_str(
|
|
||||||
format!(
|
|
||||||
"{}{:>width$}",
|
|
||||||
MARGIN,
|
|
||||||
columns.net_rx.0,
|
|
||||||
width = columns.net_rx.1
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
column_headings.push_str(
|
|
||||||
format!(
|
|
||||||
"{}{:>width$}",
|
|
||||||
MARGIN,
|
|
||||||
columns.net_tx.0,
|
|
||||||
width = columns.net_tx.1
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let suffix = if info_visible { "exit" } else { "show" };
|
let suffix = if info_visible { "exit" } else { "show" };
|
||||||
let info_text = format!("( h ) to {} help {}", suffix, MARGIN);
|
let info_text = format!("( h ) {} help {}", suffix, MARGIN);
|
||||||
let info_width = info_text.chars().count();
|
let info_width = info_text.chars().count() as u16;
|
||||||
|
|
||||||
let column_width = column_headings.chars().count();
|
|
||||||
|
|
||||||
|
let column_width = area.width - info_width;
|
||||||
|
let column_width = if column_width > 0 { column_width } else { 1 };
|
||||||
let splits = if has_containers {
|
let splits = if has_containers {
|
||||||
vec![
|
vec![Constraint::Min(column_width), Constraint::Min(info_width)]
|
||||||
Constraint::Min(column_width as u16),
|
|
||||||
Constraint::Min(info_width as u16),
|
|
||||||
]
|
|
||||||
} else {
|
} else {
|
||||||
vec![Constraint::Percentage(100)]
|
vec![Constraint::Percentage(100)]
|
||||||
};
|
};
|
||||||
@@ -440,12 +463,20 @@ pub fn draw_heading_bar<B: Backend>(
|
|||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(splits.as_ref())
|
.constraints(splits.as_ref())
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
if has_containers {
|
if has_containers {
|
||||||
let paragraph = Paragraph::new(column_headings)
|
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
||||||
.block(block())
|
|
||||||
.alignment(Alignment::Left);
|
let headers_section = Layout::default()
|
||||||
f.render_widget(paragraph, split_bar[0]);
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(container_splits.as_ref())
|
||||||
|
.split(split_bar[0]);
|
||||||
|
|
||||||
|
// draw the actual header blocks
|
||||||
|
for (index, (paragraph, header, _)) in header_data.into_iter().enumerate() {
|
||||||
|
let rect = headers_section[index];
|
||||||
|
gui_state.lock().update_map(Region::Header(header), rect);
|
||||||
|
f.render_widget(paragraph, rect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let paragraph = Paragraph::new(info_text)
|
let paragraph = Paragraph::new(info_text)
|
||||||
@@ -479,6 +510,8 @@ pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
|||||||
.push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line");
|
.push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line");
|
||||||
help_text.push_str("\n ( enter ) to send docker container commands");
|
help_text.push_str("\n ( enter ) to send docker container commands");
|
||||||
help_text.push_str("\n ( h ) to toggle this help information");
|
help_text.push_str("\n ( h ) to toggle this help information");
|
||||||
|
help_text.push_str("\n ( 0 ) stop sort");
|
||||||
|
help_text.push_str("\n ( 1 - 9 ) sort by header - or click header");
|
||||||
help_text.push_str(
|
help_text.push_str(
|
||||||
"\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied",
|
"\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied",
|
||||||
);
|
);
|
||||||
|
|||||||
+39
-10
@@ -1,6 +1,8 @@
|
|||||||
use std::{collections::HashMap, fmt};
|
use std::{collections::HashMap, fmt};
|
||||||
use tui::layout::{Constraint, Rect};
|
use tui::layout::{Constraint, Rect};
|
||||||
|
|
||||||
|
use crate::app_data::Header;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, std::hash::Hash, std::cmp::Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, std::hash::Hash, std::cmp::Eq, Clone, Copy)]
|
||||||
pub enum SelectablePanel {
|
pub enum SelectablePanel {
|
||||||
Containers,
|
Containers,
|
||||||
@@ -8,6 +10,11 @@ pub enum SelectablePanel {
|
|||||||
Logs,
|
Logs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Region {
|
||||||
|
Panel(SelectablePanel),
|
||||||
|
Header(Header),
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum BoxLocation {
|
pub enum BoxLocation {
|
||||||
@@ -37,7 +44,7 @@ impl BoxLocation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should combine and just return a tupple?
|
// Should combine and just return a tuple?
|
||||||
pub fn get_horizontal_constraints(
|
pub fn get_horizontal_constraints(
|
||||||
&self,
|
&self,
|
||||||
blank_vertical: u16,
|
blank_vertical: u16,
|
||||||
@@ -165,7 +172,8 @@ pub struct GuiState {
|
|||||||
// Think this should be a BMapTree, so can define order when iterating over potential intersects
|
// Think this should be a BMapTree, so can define order when iterating over potential intersects
|
||||||
// Is an issue if two panels are in the same space, sush as a smaller panel embedded, yet infront of, a larger panel
|
// Is an issue if two panels are in the same space, sush as a smaller panel embedded, yet infront of, a larger panel
|
||||||
// If a BMapTree think it would mean have to implement ordering for SelectablePanel
|
// If a BMapTree think it would mean have to implement ordering for SelectablePanel
|
||||||
area_map: HashMap<SelectablePanel, Rect>,
|
panel_map: HashMap<SelectablePanel, Rect>,
|
||||||
|
heading_map: HashMap<Header, Rect>,
|
||||||
loading_icon: Loading,
|
loading_icon: Loading,
|
||||||
// Should be a vec, each time loading add a new to the vec, and reset remove from vec
|
// Should be a vec, each time loading add a new to the vec, and reset remove from vec
|
||||||
// for for if is_loading just check if vec is empty or not
|
// for for if is_loading just check if vec is empty or not
|
||||||
@@ -174,12 +182,12 @@ pub struct GuiState {
|
|||||||
pub show_help: bool,
|
pub show_help: bool,
|
||||||
pub info_box_text: Option<String>,
|
pub info_box_text: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GuiState {
|
impl GuiState {
|
||||||
/// Generate a default gui_state
|
/// Generate a default gui_state
|
||||||
pub fn default() -> Self {
|
pub fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
area_map: HashMap::new(),
|
panel_map: HashMap::new(),
|
||||||
|
heading_map: HashMap::new(),
|
||||||
loading_icon: Loading::One,
|
loading_icon: Loading::One,
|
||||||
selected_panel: SelectablePanel::Containers,
|
selected_panel: SelectablePanel::Containers,
|
||||||
show_help: false,
|
show_help: false,
|
||||||
@@ -190,13 +198,13 @@ impl GuiState {
|
|||||||
|
|
||||||
/// clear panels hash map, so on resize can fix the sizes for mouse clicks
|
/// clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||||
pub fn clear_area_map(&mut self) {
|
pub fn clear_area_map(&mut self) {
|
||||||
self.area_map.clear();
|
self.panel_map.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||||
pub fn rect_insersects(&mut self, rect: Rect) {
|
pub fn panel_intersect(&mut self, rect: Rect) {
|
||||||
if let Some(data) = self
|
if let Some(data) = self
|
||||||
.area_map
|
.panel_map
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| i.1.intersects(rect))
|
.filter(|i| i.1.intersects(rect))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@@ -206,9 +214,30 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert selectable gui panel into area map
|
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||||
pub fn insert_into_area_map(&mut self, panel: SelectablePanel, area: Rect) {
|
pub fn header_intersect(&mut self, rect: Rect) -> Option<Header> {
|
||||||
self.area_map.entry(panel).or_insert(area);
|
self.heading_map
|
||||||
|
.iter()
|
||||||
|
.filter(|i| i.1.intersects(rect))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.get(0)
|
||||||
|
.map(|data| data.0.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert, or updatem header area panel into heading_map
|
||||||
|
pub fn update_map(&mut self, region: Region, area: Rect) {
|
||||||
|
match region {
|
||||||
|
Region::Header(header) => self
|
||||||
|
.heading_map
|
||||||
|
.entry(header)
|
||||||
|
.and_modify(|w| *w = area)
|
||||||
|
.or_insert(area),
|
||||||
|
Region::Panel(panel) => self
|
||||||
|
.panel_map
|
||||||
|
.entry(panel)
|
||||||
|
.and_modify(|w| *w = area)
|
||||||
|
.or_insert(area),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change to next selectable panel
|
/// Change to next selectable panel
|
||||||
|
|||||||
+4
-1
@@ -154,6 +154,8 @@ fn ui<B: Backend>(
|
|||||||
let has_containers = !app_data.lock().containers.items.is_empty();
|
let has_containers = !app_data.lock().containers.items.is_empty();
|
||||||
let has_error = app_data.lock().get_error();
|
let has_error = app_data.lock().get_error();
|
||||||
let log_index = app_data.lock().get_selected_log_index();
|
let log_index = app_data.lock().get_selected_log_index();
|
||||||
|
let sorted_by = app_data.lock().get_sorted();
|
||||||
|
|
||||||
let show_help = gui_state.lock().show_help;
|
let show_help = gui_state.lock().show_help;
|
||||||
let info_text = gui_state.lock().info_box_text.clone();
|
let info_text = gui_state.lock().info_box_text.clone();
|
||||||
let loading_icon = gui_state.lock().get_loading();
|
let loading_icon = gui_state.lock().get_loading();
|
||||||
@@ -213,7 +215,8 @@ fn ui<B: Backend>(
|
|||||||
f,
|
f,
|
||||||
has_containers,
|
has_containers,
|
||||||
loading_icon,
|
loading_icon,
|
||||||
show_help,
|
sorted_by,
|
||||||
|
gui_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// only draw charts if there are containers
|
// only draw charts if there are containers
|
||||||
|
|||||||
Reference in New Issue
Block a user