chore: merge release-v0.0.2 into main
This commit is contained in:
+12
-2
@@ -1,5 +1,15 @@
|
|||||||
### 2022-04-25
|
### 2022-04-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
+ allow toggling of mouse caputre, to select & copy text with mouse, closes [#2], [aec184ea22b289e91942a4c3e6a415685884bc47]
|
||||||
|
+ show id column, [b10f927481c9e38a48c1d4b94e744ec48e8b6ba6]
|
||||||
|
+ draw_popup, using enum to draw in one of 9 areas, closes [#6], [1017850a6cc91328abc1127bdb117495f5e909d8]
|
||||||
|
+ use a message rx/sx for all docker commands, remove update loop, wait for update message from gui instead, [9b70fdfad7b38361ebee301bdc2545d3f0dfcf9e]
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
+ readme.md typo, [589501f9a4a0bfabdb0654e68cc0c752c529d97a]
|
||||||
|
+ column heading mem > memory, [5e8e6b590b06f01a542fdd10bae8f14d303ab08a]
|
||||||
|
+ cargo fmt added to create_release.sh, [bb29c0ebfafd6a9a036eb317a240954d1405966e]
|
||||||
|
|
||||||
+ init commit
|
|
||||||
|
|
||||||
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: 179 KiB After Width: | Height: | Size: 288 KiB |
+12
-3
@@ -1,7 +1,16 @@
|
|||||||
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.2'>v0.0.2</a>
|
||||||
|
### 2022-04-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
+ allow toggling of mouse caputre, to select & copy text with mouse, closes [#2], [aec184ea22b289e91942a4c3e6a415685884bc47](https://github.com/mrjackwills/oxker/commit/aec184ea22b289e91942a4c3e6a415685884bc47),
|
||||||
|
+ show id column, [b10f927481c9e38a48c1d4b94e744ec48e8b6ba6](https://github.com/mrjackwills/oxker/commit/b10f927481c9e38a48c1d4b94e744ec48e8b6ba6),
|
||||||
|
+ draw_popup, using enum to draw in one of 9 areas, closes [#6], [1017850a6cc91328abc1127bdb117495f5e909d8](https://github.com/mrjackwills/oxker/commit/1017850a6cc91328abc1127bdb117495f5e909d8),
|
||||||
|
+ use a message rx/sx for all docker commands, remove update loop, wait for update message from gui instead, [9b70fdfad7b38361ebee301bdc2545d3f0dfcf9e](https://github.com/mrjackwills/oxker/commit/9b70fdfad7b38361ebee301bdc2545d3f0dfcf9e),
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
+ readme.md typo, [589501f9a4a0bfabdb0654e68cc0c752c529d97a]
|
+ readme.md typo, [589501f9a4a0bfabdb0654e68cc0c752c529d97a](https://github.com/mrjackwills/oxker/commit/589501f9a4a0bfabdb0654e68cc0c752c529d97a),
|
||||||
+ column heading mem > memory, [5e8e6b590b06f01a542fdd10bae8f14d303ab08a]
|
+ column heading mem > memory, [5e8e6b590b06f01a542fdd10bae8f14d303ab08a](https://github.com/mrjackwills/oxker/commit/5e8e6b590b06f01a542fdd10bae8f14d303ab08a),
|
||||||
+ cargo fmt added to create_release.sh, [bb29c0ebfafd6a9a036eb317a240954d1405966e]
|
+ cargo fmt added to create_release.sh, [bb29c0ebfafd6a9a036eb317a240954d1405966e](https://github.com/mrjackwills/oxker/commit/bb29c0ebfafd6a9a036eb317a240954d1405966e),
|
||||||
|
|
||||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.1'>v0.0.1</a>
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.1'>v0.0.1</a>
|
||||||
### 2022-04-25
|
### 2022-04-25
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "oxker"
|
name = "oxker"
|
||||||
version = "0.0.1"
|
version = "0.0.2"
|
||||||
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"
|
||||||
@@ -19,7 +19,7 @@ parking_lot = {version= "0.12.0"}
|
|||||||
tokio = {version = "1.17.0", features=["full"]}
|
tokio = {version = "1.17.0", features=["full"]}
|
||||||
tracing = "0.1.32"
|
tracing = "0.1.32"
|
||||||
tracing-subscriber = "0.3.9"
|
tracing-subscriber = "0.3.9"
|
||||||
tui = "0.17"
|
tui = "0.18"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,20 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## Download
|
## Download & install
|
||||||
|
|
||||||
See <a href="https://github.com/mrjackwills/oxker/releases" target='_blank' rel='noopener noreferrer'>releases</a>
|
See <a href="https://github.com/mrjackwills/oxker/releases" target='_blank' rel='noopener noreferrer'>releases</a>
|
||||||
|
|
||||||
|
install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xzvf oxker_linux_x86_64.tar.gz oxker
|
||||||
|
install -Dm 755 oxker -t "${HOME}/.local/bin"
|
||||||
|
rm oxker_linux_x86_64.tar.gz oxker
|
||||||
|
```
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```./oxker```
|
```oxker```
|
||||||
|
|
||||||
available command line arguments
|
available command line arguments
|
||||||
| argument|result|
|
| argument|result|
|
||||||
@@ -56,7 +62,7 @@ requires docker & <a href='https://github.com/cross-rs/cross' target='_blank' re
|
|||||||
|
|
||||||
#### 32bit pi (pi zero w)
|
#### 32bit pi (pi zero w)
|
||||||
|
|
||||||
Tested, and fully working on pi zero w, running Raspberry Pi OS 32 bit, the initial logs parsing can take an extended period of time if thousands of lines long
|
Tested, and fully working on pi zero w, running Raspberry Pi OS 32 bit, the initial logs parsing can take an extended period of time if thousands of lines long, suggest running with a -d argument of 5000
|
||||||
|
|
||||||
```cross build --target arm-unknown-linux-musleabihf --release```
|
```cross build --target arm-unknown-linux-musleabihf --release```
|
||||||
|
|
||||||
@@ -82,6 +88,13 @@ As of yet untested, needs work
|
|||||||
|
|
||||||
Run some example docker images
|
Run some example docker images
|
||||||
|
|
||||||
|
using docker-compose.yml;
|
||||||
|
|
||||||
|
```docker compose -f docker-compose.yml up -d```
|
||||||
|
|
||||||
|
or individually
|
||||||
|
|
||||||
|
|
||||||
```docker run --name redis -d redis:alpine3.15```
|
```docker run --name redis -d redis:alpine3.15```
|
||||||
|
|
||||||
```docker run --name postgres -e POSTGRES_PASSWORD=never_use_this_password_in_production -d postgres:alpine```
|
```docker run --name postgres -e POSTGRES_PASSWORD=never_use_this_password_in_production -d postgres:alpine```
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
version: "3.8"
|
||||||
|
networks:
|
||||||
|
oxker-example-net:
|
||||||
|
name: oxker-examaple-net
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:alpine
|
||||||
|
container_name: postgres
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=never_use_this_password_in_production
|
||||||
|
ipc: private
|
||||||
|
restart: always
|
||||||
|
shm_size: 256MB
|
||||||
|
networks:
|
||||||
|
- oxker-example-net
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
container_name: redis
|
||||||
|
ipc: private
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- oxker-example-net
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 16M
|
||||||
|
rabbitmq:
|
||||||
|
image: rabbitmq:3
|
||||||
|
container_name: rabbitmq
|
||||||
|
ipc: private
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- oxker-example-net
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
|
||||||
@@ -398,12 +398,13 @@ 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 cpu: (String, usize),
|
|
||||||
pub image: (String, usize),
|
|
||||||
pub name: (String, usize),
|
|
||||||
pub state: (String, usize),
|
pub state: (String, usize),
|
||||||
pub status: (String, usize),
|
pub status: (String, usize),
|
||||||
|
pub cpu: (String, usize),
|
||||||
pub mem: (String, usize),
|
pub mem: (String, usize),
|
||||||
|
pub id: (String, usize),
|
||||||
|
pub name: (String, usize),
|
||||||
|
pub image: (String, usize),
|
||||||
pub net_rx: (String, usize),
|
pub net_rx: (String, usize),
|
||||||
pub net_tx: (String, usize),
|
pub net_tx: (String, usize),
|
||||||
}
|
}
|
||||||
@@ -411,13 +412,14 @@ pub struct Columns {
|
|||||||
impl Columns {
|
impl Columns {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
// 7 to allow for 100.00%
|
|
||||||
cpu: (String::from("cpu"), 7),
|
|
||||||
image: (String::from("image"), 5),
|
|
||||||
name: (String::from("name"), 4),
|
|
||||||
state: (String::from("state"), 11),
|
state: (String::from("state"), 11),
|
||||||
status: (String::from("status"), 16),
|
status: (String::from("status"), 16),
|
||||||
|
// 7 to allow for "100.00%"
|
||||||
|
cpu: (String::from("cpu"), 7),
|
||||||
mem: (String::from("memory/limit"), 12),
|
mem: (String::from("memory/limit"), 12),
|
||||||
|
id: (String::from("id"), 8),
|
||||||
|
name: (String::from("name"), 4),
|
||||||
|
image: (String::from("image"), 5),
|
||||||
net_rx: (String::from("↓ rx"), 5),
|
net_rx: (String::from("↓ rx"), 5),
|
||||||
net_tx: (String::from("↑ tx"), 5),
|
net_tx: (String::from("↑ tx"), 5),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ impl AppData {
|
|||||||
if self.containers.state.selected().is_some() {
|
if self.containers.state.selected().is_some() {
|
||||||
self.containers.previous();
|
self.containers.previous();
|
||||||
}
|
}
|
||||||
|
// docker rm -f $(docker ps -aq) will cause this to crash
|
||||||
self.containers.items.remove(index);
|
self.containers.items.remove(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-19
@@ -1,7 +1,5 @@
|
|||||||
use core::fmt;
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::app_data::DockerControls;
|
use crate::app_data::DockerControls;
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
/// app errors to set in global state
|
/// app errors to set in global state
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
@@ -11,25 +9,10 @@ pub enum AppError {
|
|||||||
DockerInterval,
|
DockerInterval,
|
||||||
InputPoll,
|
InputPoll,
|
||||||
DockerCommand(DockerControls),
|
DockerCommand(DockerControls),
|
||||||
|
MouseCapture(bool),
|
||||||
Terminal,
|
Terminal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppError {
|
|
||||||
/// for handling errors from terminal
|
|
||||||
pub fn disp(&self) {
|
|
||||||
match self {
|
|
||||||
Self::DockerConnect => error!("Unable to access docker daemon"),
|
|
||||||
Self::DockerInterval => error!("Docker update interval needs to be greater than 0"),
|
|
||||||
Self::InputPoll => error!("Unable to poll user input"),
|
|
||||||
Self::Terminal => error!("Unable to draw to terminal"),
|
|
||||||
Self::DockerCommand(s) => {
|
|
||||||
let error = format!("Unable to {} container", s);
|
|
||||||
error!(%error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert errors into strings to display
|
/// Convert errors into strings to display
|
||||||
impl fmt::Display for AppError {
|
impl fmt::Display for AppError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
@@ -39,6 +22,10 @@ impl fmt::Display for AppError {
|
|||||||
Self::InputPoll => "Unable to poll user input".to_owned(),
|
Self::InputPoll => "Unable to poll user input".to_owned(),
|
||||||
Self::Terminal => "Unable to draw to terminal".to_owned(),
|
Self::Terminal => "Unable to draw to terminal".to_owned(),
|
||||||
Self::DockerCommand(s) => format!("Unable to {} container", s),
|
Self::DockerCommand(s) => format!("Unable to {} container", s),
|
||||||
|
Self::MouseCapture(x) => {
|
||||||
|
let reason = if *x { "en" } else { "dis" };
|
||||||
|
format!("Unable to {}able mouse capture", reason)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
write!(f, "{}", disp)
|
write!(f, "{}", disp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DockerMessage {
|
||||||
|
Update,
|
||||||
|
Start(String),
|
||||||
|
Restart(String),
|
||||||
|
Pause(String),
|
||||||
|
Unpause(String),
|
||||||
|
Stop(String),
|
||||||
|
}
|
||||||
+112
-48
@@ -1,22 +1,27 @@
|
|||||||
use bollard::{
|
use bollard::{
|
||||||
container::{ListContainersOptions, LogsOptions, Stats, StatsOptions},
|
container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions},
|
||||||
Docker,
|
Docker,
|
||||||
};
|
};
|
||||||
use futures_util::{future::join_all, StreamExt};
|
use futures_util::{future::join_all, StreamExt};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::sync::Arc;
|
||||||
sync::Arc,
|
use tokio::{sync::mpsc::Receiver, task::JoinHandle};
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{app_data::AppData, parse_args::CliArgs, ui::GuiState};
|
use crate::{
|
||||||
|
app_data::{AppData, DockerControls},
|
||||||
|
app_error::AppError,
|
||||||
|
parse_args::CliArgs,
|
||||||
|
ui::GuiState,
|
||||||
|
};
|
||||||
|
mod message;
|
||||||
|
pub use message::DockerMessage;
|
||||||
|
|
||||||
pub struct DockerData {
|
pub struct DockerData {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
initialised: bool,
|
initialised: bool,
|
||||||
sleep_duration: Duration,
|
receiver: Receiver<DockerMessage>,
|
||||||
timestamps: bool,
|
timestamps: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +183,7 @@ impl DockerData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update all logs, spawn each container into own tokio::spawn thread
|
/// Update all logs, spawn each container into own tokio::spawn thread
|
||||||
// rename init all logs, as only gets run once
|
async fn init_all_logs(&mut self, all_ids: &[(bool, String)]) {
|
||||||
async fn update_all_logs(&mut self, all_ids: &[(bool, String)]) {
|
|
||||||
let mut handles = vec![];
|
let mut handles = vec![];
|
||||||
|
|
||||||
for (_, id) in all_ids.iter() {
|
for (_, id) in all_ids.iter() {
|
||||||
@@ -207,43 +211,32 @@ impl DockerData {
|
|||||||
self.update_all_container_stats(&all_ids).await;
|
self.update_all_container_stats(&all_ids).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialise self, and start the updated loop
|
/// Animate the loading icon
|
||||||
pub async fn init(
|
async fn loading_spin(&mut self) -> JoinHandle<()> {
|
||||||
args: CliArgs,
|
|
||||||
app_data: Arc<Mutex<AppData>>,
|
|
||||||
docker: Arc<Docker>,
|
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
|
||||||
) {
|
|
||||||
if app_data.lock().get_error().is_none() {
|
|
||||||
let mut inner = Self {
|
|
||||||
app_data,
|
|
||||||
docker,
|
|
||||||
gui_state,
|
|
||||||
initialised: false,
|
|
||||||
sleep_duration: Duration::from_millis(args.docker as u64),
|
|
||||||
timestamps: args.timestamp,
|
|
||||||
};
|
|
||||||
inner.initialise_container_data().await;
|
|
||||||
inner.update_loop().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn initialise_container_data(&mut self) {
|
|
||||||
let gui_state = Arc::clone(&self.gui_state);
|
let gui_state = Arc::clone(&self.gui_state);
|
||||||
// could also just loop while init is false, would need to move an arc mutex into here
|
tokio::spawn(async move {
|
||||||
// so instead just abort at end of function
|
|
||||||
let loading_spin = tokio::spawn(async move {
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
gui_state.lock().next_loading();
|
gui_state.lock().next_loading();
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the loading_spin fn, and reset gui loading status
|
||||||
|
fn stop_loading_spin(&mut self, handle: JoinHandle<()>) {
|
||||||
|
handle.abort();
|
||||||
|
self.gui_state.lock().reset_loading();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize docker container data, before any messages are received
|
||||||
|
async fn initialise_container_data(&mut self) {
|
||||||
|
let loading_spin = self.loading_spin().await;
|
||||||
|
|
||||||
let all_ids = self.update_all_containers().await;
|
let all_ids = self.update_all_containers().await;
|
||||||
self.update_all_container_stats(&all_ids).await;
|
self.update_all_container_stats(&all_ids).await;
|
||||||
|
|
||||||
// Maybe only do a single one at first?
|
// Maybe only do a single one at first?
|
||||||
self.update_all_logs(&all_ids).await;
|
self.init_all_logs(&all_ids).await;
|
||||||
|
|
||||||
if all_ids.is_empty() {
|
if all_ids.is_empty() {
|
||||||
self.initialised = true;
|
self.initialised = true;
|
||||||
@@ -255,23 +248,94 @@ impl DockerData {
|
|||||||
self.initialised = self.app_data.lock().initialised(&all_ids);
|
self.initialised = self.app_data.lock().initialised(&all_ids);
|
||||||
}
|
}
|
||||||
self.app_data.lock().init = true;
|
self.app_data.lock().init = true;
|
||||||
loading_spin.abort();
|
self.stop_loading_spin(loading_spin);
|
||||||
self.gui_state.lock().reset_loading();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all items, wait until all complete
|
/// Handle incoming messages, container controls & all container information update
|
||||||
/// sleep for CliArgs.docker ms before updating next
|
async fn message_handler(&mut self) {
|
||||||
async fn update_loop(&mut self) {
|
while let Some(message) = self.receiver.recv().await {
|
||||||
loop {
|
let docker = Arc::clone(&self.docker);
|
||||||
let start = Instant::now();
|
let app_data = Arc::clone(&self.app_data);
|
||||||
self.update_everything().await;
|
match message {
|
||||||
|
DockerMessage::Pause(id) => {
|
||||||
let elapsed = start.elapsed();
|
let loading_spin = self.loading_spin().await;
|
||||||
if elapsed < self.sleep_duration {
|
docker.pause_container(&id).await.unwrap_or_else(|_| {
|
||||||
tokio::time::sleep(self.sleep_duration - elapsed).await;
|
app_data
|
||||||
|
.lock()
|
||||||
|
.set_error(AppError::DockerCommand(DockerControls::Pause))
|
||||||
|
});
|
||||||
|
self.stop_loading_spin(loading_spin);
|
||||||
|
}
|
||||||
|
DockerMessage::Restart(id) => {
|
||||||
|
let loading_spin = self.loading_spin().await;
|
||||||
|
docker
|
||||||
|
.restart_container(&id, None)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
app_data
|
||||||
|
.lock()
|
||||||
|
.set_error(AppError::DockerCommand(DockerControls::Restart))
|
||||||
|
});
|
||||||
|
self.stop_loading_spin(loading_spin);
|
||||||
|
}
|
||||||
|
DockerMessage::Start(id) => {
|
||||||
|
let loading_spin = self.loading_spin().await;
|
||||||
|
docker
|
||||||
|
.start_container(&id, None::<StartContainerOptions<String>>)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
app_data
|
||||||
|
.lock()
|
||||||
|
.set_error(AppError::DockerCommand(DockerControls::Start))
|
||||||
|
});
|
||||||
|
self.stop_loading_spin(loading_spin);
|
||||||
|
}
|
||||||
|
DockerMessage::Stop(id) => {
|
||||||
|
let loading_spin = self.loading_spin().await;
|
||||||
|
docker.stop_container(&id, None).await.unwrap_or_else(|_| {
|
||||||
|
app_data
|
||||||
|
.lock()
|
||||||
|
.set_error(AppError::DockerCommand(DockerControls::Stop))
|
||||||
|
});
|
||||||
|
self.stop_loading_spin(loading_spin);
|
||||||
|
}
|
||||||
|
DockerMessage::Unpause(id) => {
|
||||||
|
let loading_spin = self.loading_spin().await;
|
||||||
|
docker.unpause_container(&id).await.unwrap_or_else(|_| {
|
||||||
|
app_data
|
||||||
|
.lock()
|
||||||
|
.set_error(AppError::DockerCommand(DockerControls::Unpause))
|
||||||
|
});
|
||||||
|
self.stop_loading_spin(loading_spin);
|
||||||
|
self.update_everything().await
|
||||||
|
}
|
||||||
|
DockerMessage::Update => self.update_everything().await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialise self, and start the message receiving loop
|
||||||
|
pub async fn init(
|
||||||
|
args: CliArgs,
|
||||||
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
docker: Arc<Docker>,
|
||||||
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
receiver: Receiver<DockerMessage>,
|
||||||
|
) {
|
||||||
|
if app_data.lock().get_error().is_none() {
|
||||||
|
let mut inner = Self {
|
||||||
|
app_data,
|
||||||
|
docker,
|
||||||
|
gui_state,
|
||||||
|
initialised: false,
|
||||||
|
receiver,
|
||||||
|
timestamps: args.timestamp,
|
||||||
|
};
|
||||||
|
inner.initialise_container_data().await;
|
||||||
|
|
||||||
|
inner.message_handler().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers
|
// 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
|
||||||
|
|||||||
+88
-92
@@ -3,16 +3,24 @@ use std::sync::{
|
|||||||
Arc,
|
Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bollard::{container::StartContainerOptions, Docker};
|
use crossterm::{
|
||||||
use crossterm::event::{KeyCode, MouseButton, MouseEvent, MouseEventKind};
|
event::{
|
||||||
|
DisableMouseCapture, EnableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind,
|
||||||
|
},
|
||||||
|
execute,
|
||||||
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use tokio::sync::broadcast::Receiver;
|
use tokio::{
|
||||||
|
sync::mpsc::{Receiver, Sender},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
use tui::layout::Rect;
|
use tui::layout::Rect;
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, DockerControls},
|
app_data::{AppData, DockerControls},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
|
docker_data::DockerMessage,
|
||||||
ui::{GuiState, SelectablePanel},
|
ui::{GuiState, SelectablePanel},
|
||||||
};
|
};
|
||||||
pub use message::InputMessages;
|
pub use message::InputMessages;
|
||||||
@@ -21,9 +29,11 @@ pub use message::InputMessages;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InputHandler {
|
pub struct InputHandler {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker: Arc<Docker>,
|
docker_sender: Sender<DockerMessage>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
info_sleep: Option<JoinHandle<()>>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
|
mouse_capture: bool,
|
||||||
rec: Receiver<InputMessages>,
|
rec: Receiver<InputMessages>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,23 +42,25 @@ impl InputHandler {
|
|||||||
pub async fn init(
|
pub async fn init(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
rec: Receiver<InputMessages>,
|
rec: Receiver<InputMessages>,
|
||||||
docker: Arc<Docker>,
|
docker_sender: Sender<DockerMessage>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let mut inner = Self {
|
let mut inner = Self {
|
||||||
app_data,
|
app_data,
|
||||||
docker,
|
docker_sender,
|
||||||
gui_state,
|
gui_state,
|
||||||
is_running,
|
is_running,
|
||||||
rec,
|
rec,
|
||||||
|
mouse_capture: true,
|
||||||
|
info_sleep: None,
|
||||||
};
|
};
|
||||||
inner.start().await;
|
inner.start().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// check for incoming messages
|
/// check for incoming messages
|
||||||
async fn start(&mut self) {
|
async fn start(&mut self) {
|
||||||
while let Ok(message) = self.rec.recv().await {
|
while let Some(message) = self.rec.recv().await {
|
||||||
match message {
|
match message {
|
||||||
InputMessages::ButtonPress(key_code) => self.button_press(key_code).await,
|
InputMessages::ButtonPress(key_code) => self.button_press(key_code).await,
|
||||||
InputMessages::MouseEvent(mouse_event) => {
|
InputMessages::MouseEvent(mouse_event) => {
|
||||||
@@ -65,10 +77,46 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn m_button(&mut self) {
|
||||||
|
if self.mouse_capture {
|
||||||
|
match execute!(std::io::stdout(), DisableMouseCapture) {
|
||||||
|
Ok(_) => self
|
||||||
|
.gui_state
|
||||||
|
.lock()
|
||||||
|
.set_info_box("✖ mouse capture disabled".to_owned()),
|
||||||
|
Err(_) => self
|
||||||
|
.app_data
|
||||||
|
.lock()
|
||||||
|
.set_error(AppError::MouseCapture(false)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match execute!(std::io::stdout(), EnableMouseCapture) {
|
||||||
|
Ok(_) => self
|
||||||
|
.gui_state
|
||||||
|
.lock()
|
||||||
|
.set_info_box("✓ mouse capture enabled".to_owned()),
|
||||||
|
Err(_) => self.app_data.lock().set_error(AppError::MouseCapture(true)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let gui_state = Arc::clone(&self.gui_state);
|
||||||
|
|
||||||
|
if self.info_sleep.is_some() {
|
||||||
|
self.info_sleep.as_ref().unwrap().abort()
|
||||||
|
}
|
||||||
|
self.info_sleep = Some(tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(4000)).await;
|
||||||
|
gui_state.lock().reset_info_box()
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.mouse_capture = !self.mouse_capture;
|
||||||
|
}
|
||||||
|
|
||||||
/// 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;
|
||||||
let show_info = self.gui_state.lock().show_help;
|
let show_info = self.gui_state.lock().show_help;
|
||||||
|
|
||||||
if show_error {
|
if show_error {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
@@ -82,22 +130,16 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
} else if show_info {
|
} else if show_info {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst),
|
||||||
self.is_running.store(false, Ordering::SeqCst);
|
KeyCode::Char('h') => self.gui_state.lock().show_help = false,
|
||||||
}
|
KeyCode::Char('m') => self.m_button(),
|
||||||
KeyCode::Char('h') => {
|
|
||||||
self.gui_state.lock().show_help = false;
|
|
||||||
}
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst),
|
||||||
self.is_running.store(false, Ordering::SeqCst);
|
KeyCode::Char('h') => self.gui_state.lock().show_help = true,
|
||||||
}
|
KeyCode::Char('m') => self.m_button(),
|
||||||
KeyCode::Char('h') => {
|
|
||||||
self.gui_state.lock().show_help = true;
|
|
||||||
}
|
|
||||||
KeyCode::Tab => self.gui_state.lock().next_panel(),
|
KeyCode::Tab => self.gui_state.lock().next_panel(),
|
||||||
KeyCode::BackTab => self.gui_state.lock().previous_panel(),
|
KeyCode::BackTab => self.gui_state.lock().previous_panel(),
|
||||||
KeyCode::Home => {
|
KeyCode::Home => {
|
||||||
@@ -129,90 +171,44 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
// Does is matter though?
|
|
||||||
// This isn't great, just means you can't send docker commands before full initialization of the program
|
// This isn't great, just means you can't send docker commands before full initialization of the program
|
||||||
// could change to to if loading = true, although at the moment don't have a loading bool
|
// could change to to if loading = true, although at the moment don't have a loading bool
|
||||||
|
// Does is matter though?
|
||||||
let panel = self.gui_state.lock().selected_panel;
|
let panel = self.gui_state.lock().selected_panel;
|
||||||
if panel == SelectablePanel::Commands {
|
if panel == SelectablePanel::Commands {
|
||||||
let command = self.app_data.lock().get_docker_command();
|
let command = self.app_data.lock().get_docker_command();
|
||||||
|
|
||||||
if command.is_some() {
|
if command.is_some() {
|
||||||
let id = self.app_data.lock().get_selected_container_id();
|
let id = self.app_data.lock().get_selected_container_id();
|
||||||
let app_data = Arc::clone(&self.app_data);
|
|
||||||
let docker = Arc::clone(&self.docker);
|
|
||||||
if id.is_some() {
|
if id.is_some() {
|
||||||
let id = id.unwrap();
|
let id = id.unwrap();
|
||||||
match command.unwrap() {
|
match command.unwrap() {
|
||||||
DockerControls::Pause => {
|
// TODO handle theses errors?
|
||||||
tokio::spawn(async move {
|
DockerControls::Pause => self
|
||||||
docker.pause_container(&id).await.unwrap_or_else(
|
.docker_sender
|
||||||
|_| {
|
.send(DockerMessage::Pause(id))
|
||||||
app_data.lock().set_error(
|
.await
|
||||||
AppError::DockerCommand(
|
.unwrap(),
|
||||||
DockerControls::Pause,
|
DockerControls::Unpause => self
|
||||||
),
|
.docker_sender
|
||||||
)
|
.send(DockerMessage::Unpause(id))
|
||||||
},
|
.await
|
||||||
);
|
.unwrap(),
|
||||||
});
|
DockerControls::Start => self
|
||||||
}
|
.docker_sender
|
||||||
DockerControls::Unpause => {
|
.send(DockerMessage::Start(id))
|
||||||
tokio::spawn(async move {
|
.await
|
||||||
docker.unpause_container(&id).await.unwrap_or_else(
|
.unwrap(),
|
||||||
|_| {
|
DockerControls::Stop => self
|
||||||
app_data.lock().set_error(
|
.docker_sender
|
||||||
AppError::DockerCommand(
|
.send(DockerMessage::Stop(id))
|
||||||
DockerControls::Unpause,
|
.await
|
||||||
),
|
.unwrap(),
|
||||||
)
|
DockerControls::Restart => self
|
||||||
},
|
.docker_sender
|
||||||
);
|
.send(DockerMessage::Restart(id))
|
||||||
});
|
.await
|
||||||
}
|
.unwrap(),
|
||||||
DockerControls::Start => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
docker
|
|
||||||
.start_container(
|
|
||||||
&id,
|
|
||||||
None::<StartContainerOptions<String>>,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
app_data.lock().set_error(
|
|
||||||
AppError::DockerCommand(
|
|
||||||
DockerControls::Start,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
DockerControls::Stop => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
docker.stop_container(&id, None).await.unwrap_or_else(
|
|
||||||
|_| {
|
|
||||||
app_data.lock().set_error(
|
|
||||||
AppError::DockerCommand(
|
|
||||||
DockerControls::Stop,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
DockerControls::Restart => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
docker
|
|
||||||
.restart_container(&id, None)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
app_data.lock().set_error(
|
|
||||||
AppError::DockerCommand(
|
|
||||||
DockerControls::Restart,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-6
@@ -31,13 +31,21 @@ async fn main() {
|
|||||||
let docker_app_data = Arc::clone(&app_data);
|
let docker_app_data = Arc::clone(&app_data);
|
||||||
let docker_gui_state = Arc::clone(&gui_state);
|
let docker_gui_state = Arc::clone(&gui_state);
|
||||||
|
|
||||||
|
let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(16);
|
||||||
// Create docker daemon handler, and only spawn up the docker data handler if ping returns non-error
|
// Create docker daemon handler, and only spawn up the docker data handler if ping returns non-error
|
||||||
let docker = Arc::new(Docker::connect_with_socket_defaults().unwrap());
|
let docker = Arc::new(Docker::connect_with_socket_defaults().unwrap());
|
||||||
match docker.ping().await {
|
match docker.ping().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let docker = Arc::clone(&docker);
|
let docker = Arc::clone(&docker);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
DockerData::init(docker_args, docker_app_data, docker, docker_gui_state).await;
|
DockerData::init(
|
||||||
|
docker_args,
|
||||||
|
docker_app_data,
|
||||||
|
docker,
|
||||||
|
docker_gui_state,
|
||||||
|
docker_rx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(_) => app_data.lock().set_error(AppError::DockerConnect),
|
Err(_) => app_data.lock().set_error(AppError::DockerConnect),
|
||||||
@@ -45,19 +53,20 @@ async fn main() {
|
|||||||
|
|
||||||
let input_app_data = Arc::clone(&app_data);
|
let input_app_data = Arc::clone(&app_data);
|
||||||
|
|
||||||
let (s, r) = tokio::sync::broadcast::channel(16);
|
let (input_sx, input_rx) = tokio::sync::mpsc::channel(16);
|
||||||
|
|
||||||
let input_docker = Arc::clone(&docker);
|
// let input_docker = Arc::clone(&docker);
|
||||||
let is_running = Arc::new(AtomicBool::new(true));
|
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();
|
||||||
|
|
||||||
// Spawn input handling into own tokio thread
|
// Spawn input handling into own tokio thread
|
||||||
tokio::spawn(async {
|
tokio::spawn(async {
|
||||||
input_handler::InputHandler::init(
|
input_handler::InputHandler::init(
|
||||||
input_app_data,
|
input_app_data,
|
||||||
r,
|
input_rx,
|
||||||
input_docker,
|
input_docker_sender,
|
||||||
input_gui_state,
|
input_gui_state,
|
||||||
input_is_running,
|
input_is_running,
|
||||||
)
|
)
|
||||||
@@ -71,6 +80,16 @@ async fn main() {
|
|||||||
tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
create_ui(app_data, s, is_running, gui_state).await.unwrap();
|
let update_duration = std::time::Duration::from_millis(args.docker_interval as u64);
|
||||||
|
create_ui(
|
||||||
|
app_data,
|
||||||
|
input_sx,
|
||||||
|
is_running,
|
||||||
|
gui_state,
|
||||||
|
docker_sx,
|
||||||
|
update_duration,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ use tracing::error;
|
|||||||
|
|
||||||
pub struct CliArgs {
|
pub struct CliArgs {
|
||||||
/// Docker update interval in ms, minimum 1, reccomended 500+
|
/// Docker update interval in ms, minimum 1, reccomended 500+
|
||||||
#[clap(short = 'd', default_value_t = 1000)]
|
#[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
|
||||||
pub docker: u32,
|
pub docker_interval: u32,
|
||||||
|
|
||||||
/// Don't draw gui - for debugging - mostly pointless
|
/// Don't draw gui - for debugging - mostly pointless
|
||||||
#[clap(short = 'g')]
|
#[clap(short = 'g')]
|
||||||
pub gui: bool,
|
pub gui: bool,
|
||||||
|
|
||||||
|
// /// Install to ./local/bin
|
||||||
|
// #[clap(short = 'i')]
|
||||||
|
// pub install: bool,
|
||||||
/// Remove timestamps from Docker logs
|
/// Remove timestamps from Docker logs
|
||||||
#[clap(short = 't')]
|
#[clap(short = 't')]
|
||||||
pub timestamp: bool,
|
pub timestamp: bool,
|
||||||
@@ -35,15 +38,16 @@ impl CliArgs {
|
|||||||
|
|
||||||
// Quit the program if the docker update argument is 0
|
// Quit the program if the docker update argument is 0
|
||||||
// Should maybe change it to check if less than 100
|
// Should maybe change it to check if less than 100
|
||||||
if args.docker == 0 {
|
if args.docker_interval == 0 {
|
||||||
error!("docker args needs to be greater than 0");
|
error!("docker args needs to be greater than 0");
|
||||||
process::exit(1)
|
process::exit(1)
|
||||||
}
|
}
|
||||||
Self {
|
Self {
|
||||||
color: args.color,
|
color: args.color,
|
||||||
docker: args.docker,
|
docker_interval: args.docker_interval,
|
||||||
gui: !args.gui,
|
gui: !args.gui,
|
||||||
raw: args.raw,
|
raw: args.raw,
|
||||||
|
// install: args.install,
|
||||||
timestamp: !args.timestamp,
|
timestamp: !args.timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-48
@@ -19,6 +19,7 @@ use crate::{
|
|||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::gui_state::BoxLocation;
|
||||||
use super::{GuiState, SelectablePanel};
|
use super::{GuiState, SelectablePanel};
|
||||||
|
|
||||||
const NAME_TEXT: &str = r#"
|
const NAME_TEXT: &str = r#"
|
||||||
@@ -34,17 +35,20 @@ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8
|
|||||||
const NAME: &str = env!("CARGO_PKG_NAME");
|
const NAME: &str = env!("CARGO_PKG_NAME");
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||||
|
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||||
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||||
const MARGIN: &str = " ";
|
const MARGIN: &str = " ";
|
||||||
|
|
||||||
/// Generate block, add a bored if is the selected panel,
|
/// Generate block, add a border if is the selected panel,
|
||||||
/// add custom title based on state of each panel
|
/// add custom title based on state of each panel
|
||||||
fn generate_block<'a>(
|
fn generate_block<'a>(
|
||||||
selectable_panel: Option<SelectablePanel>,
|
selectable_panel: Option<SelectablePanel>,
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
selected_panel: &SelectablePanel,
|
selected_panel: &SelectablePanel,
|
||||||
) -> Block<'a> {
|
) -> Block<'a> {
|
||||||
let mut block = Block::default().borders(Borders::ALL);
|
let mut block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded);
|
||||||
|
|
||||||
if let Some(panel) = selectable_panel {
|
if let Some(panel) = selectable_panel {
|
||||||
let title = match panel {
|
let title = match panel {
|
||||||
@@ -62,11 +66,7 @@ fn generate_block<'a>(
|
|||||||
};
|
};
|
||||||
block = block.title(title);
|
block = block.title(title);
|
||||||
if selected_panel == &panel {
|
if selected_panel == &panel {
|
||||||
let selected_style = Style::default().fg(Color::LightCyan);
|
block = block.border_style(Style::default().fg(Color::LightCyan));
|
||||||
let selected_border = BorderType::Plain;
|
|
||||||
block = block
|
|
||||||
.border_style(selected_style)
|
|
||||||
.border_type(selected_border);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
block
|
block
|
||||||
@@ -170,6 +170,15 @@ pub fn draw_containers<B: Backend>(
|
|||||||
format!("{}{:>width$}", MARGIN, mems, width = widths.mem.1),
|
format!("{}{:>width$}", MARGIN, mems, width = widths.mem.1),
|
||||||
state_style,
|
state_style,
|
||||||
),
|
),
|
||||||
|
Span::styled(
|
||||||
|
format!(
|
||||||
|
"{}{:>width$}",
|
||||||
|
MARGIN,
|
||||||
|
i.id.chars().take(8).collect::<String>(),
|
||||||
|
width = widths.id.1
|
||||||
|
),
|
||||||
|
blue,
|
||||||
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}{:>width$}", MARGIN, i.name, width = widths.name.1),
|
format!("{}{:>width$}", MARGIN, i.name, width = widths.name.1),
|
||||||
blue,
|
blue,
|
||||||
@@ -214,6 +223,7 @@ pub fn draw_logs<B: Backend>(
|
|||||||
f: &mut Frame<'_, B>,
|
f: &mut Frame<'_, B>,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
index: Option<usize>,
|
index: Option<usize>,
|
||||||
|
loading_icon: String,
|
||||||
selected_panel: &SelectablePanel,
|
selected_panel: &SelectablePanel,
|
||||||
) {
|
) {
|
||||||
let panel = SelectablePanel::Logs;
|
let panel = SelectablePanel::Logs;
|
||||||
@@ -224,8 +234,8 @@ pub fn draw_logs<B: Backend>(
|
|||||||
|
|
||||||
let init = app_data.lock().init;
|
let init = app_data.lock().init;
|
||||||
if !init {
|
if !init {
|
||||||
let icon = gui_state.lock().get_loading();
|
// let icon = gui_state.lock().get_loading();
|
||||||
let parsing_logs = format!("parsing logs {}", icon);
|
let parsing_logs = format!("parsing logs {}", loading_icon);
|
||||||
let paragraph = Paragraph::new(parsing_logs)
|
let paragraph = Paragraph::new(parsing_logs)
|
||||||
.style(Style::default())
|
.style(Style::default())
|
||||||
.block(block)
|
.block(block)
|
||||||
@@ -330,7 +340,7 @@ fn make_chart<T: Stats + Display>(
|
|||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
))
|
))
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Plain),
|
.border_type(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.x_axis(
|
.x_axis(
|
||||||
Axis::default()
|
Axis::default()
|
||||||
@@ -348,27 +358,32 @@ fn make_chart<T: Stats + Display>(
|
|||||||
.fg(label_color),
|
.fg(label_color),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
// add 0.01, for cases when the value is 0
|
|
||||||
.bounds([0.0, max.get_value() + 0.01]),
|
.bounds([0.0, max.get_value() + 0.01]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show error popup over whole screen
|
/// Show error popup over whole screen
|
||||||
pub fn draw_info_bar<B: Backend>(
|
pub fn draw_heading_bar<B: Backend>(
|
||||||
area: Rect,
|
area: Rect,
|
||||||
columns: &Columns,
|
columns: &Columns,
|
||||||
f: &mut Frame<'_, B>,
|
f: &mut Frame<'_, B>,
|
||||||
has_containers: bool,
|
has_containers: bool,
|
||||||
|
loading_icon: String,
|
||||||
info_visible: bool,
|
info_visible: bool,
|
||||||
) {
|
) {
|
||||||
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));
|
||||||
|
|
||||||
f.render_widget(block(), area);
|
f.render_widget(block(), area);
|
||||||
|
|
||||||
let mut column_headings = format!(" {:>width$}", columns.state.0, width = columns.state.1);
|
let mut column_headings = format!(
|
||||||
|
" {}{:>width$}",
|
||||||
|
loading_icon,
|
||||||
|
columns.state.0,
|
||||||
|
width = columns.state.1
|
||||||
|
);
|
||||||
column_headings.push_str(
|
column_headings.push_str(
|
||||||
format!(
|
format!(
|
||||||
"{} {:>width$}",
|
"{} {:>width$}",
|
||||||
MARGIN,
|
MARGIN,
|
||||||
columns.status.0,
|
columns.status.0,
|
||||||
width = columns.status.1
|
width = columns.status.1
|
||||||
@@ -379,6 +394,8 @@ pub fn draw_info_bar<B: Backend>(
|
|||||||
.push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str());
|
.push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str());
|
||||||
column_headings
|
column_headings
|
||||||
.push_str(format!("{}{:>width$}", MARGIN, columns.mem.0, width = columns.mem.1).as_str());
|
.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(
|
column_headings.push_str(
|
||||||
format!(
|
format!(
|
||||||
"{}{:>width$}",
|
"{}{:>width$}",
|
||||||
@@ -455,18 +472,23 @@ pub fn draw_info_bar<B: Backend>(
|
|||||||
pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
||||||
let title = format!(" {} ", VERSION);
|
let title = format!(" {} ", VERSION);
|
||||||
|
|
||||||
let mut description_text =
|
let description_text = format!("\n{}", DESCRIPTION);
|
||||||
String::from("\n A basic docker container information viewer and controller.");
|
|
||||||
description_text.push_str("\n Tab or Alt+Tab to change panels, arrows to change lines, enter to send docker container commands.");
|
let mut help_text = String::from("\n ( tab ) or ( alt+tab ) to change panels");
|
||||||
description_text.push_str("\n Mouse input also available.");
|
help_text.push_str("\n ( ↑ ↓ ← → ) to change selected line");
|
||||||
description_text.push_str("\n ( q ) to quit at any time.");
|
help_text.push_str("\n ( enter ) to send docker container commands");
|
||||||
description_text
|
help_text.push_str("\n ( h ) to toggle this help information");
|
||||||
.push_str("\n\n currenty an early work in progress, all and any input appreciated");
|
help_text.push_str(
|
||||||
description_text.push_str(format!("\n {}", REPO.trim()).as_str());
|
"\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied",
|
||||||
|
);
|
||||||
|
help_text.push_str("\n ( q ) to quit at any time");
|
||||||
|
help_text.push_str("\n mouse scrolling & clicking also available");
|
||||||
|
help_text.push_str("\n\n currenty an early work in progress, all and any input appreciated");
|
||||||
|
help_text.push_str(format!("\n {}", REPO.trim()).as_str());
|
||||||
|
|
||||||
let mut max_line_width = 0;
|
let mut max_line_width = 0;
|
||||||
|
|
||||||
let all_text = format!("{}{}", NAME_TEXT, description_text);
|
let all_text = format!("{}{}{}", NAME_TEXT, description_text, help_text);
|
||||||
|
|
||||||
all_text.lines().into_iter().for_each(|line| {
|
all_text.lines().into_iter().for_each(|line| {
|
||||||
let width = line.chars().count();
|
let width = line.chars().count();
|
||||||
@@ -486,7 +508,12 @@ pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
|||||||
.block(Block::default())
|
.block(Block::default())
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
|
|
||||||
let description_paragraph = Paragraph::new(description_text.as_str())
|
let description_paragrpah = Paragraph::new(description_text.as_str())
|
||||||
|
.style(Style::default().bg(Color::Magenta).fg(Color::Black))
|
||||||
|
.block(Block::default())
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
|
||||||
|
let help_paragraph = Paragraph::new(help_text.as_str())
|
||||||
.style(Style::default().bg(Color::Magenta).fg(Color::Black))
|
.style(Style::default().bg(Color::Magenta).fg(Color::Black))
|
||||||
.block(Block::default())
|
.block(Block::default())
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
@@ -497,7 +524,12 @@ pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
|||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(Style::default().fg(Color::Black));
|
.border_style(Style::default().fg(Color::Black));
|
||||||
|
|
||||||
let area = centered_info(lines as u16, max_line_width as u16, f.size());
|
let area = draw_popup(
|
||||||
|
lines as u16,
|
||||||
|
max_line_width as u16,
|
||||||
|
f.size(),
|
||||||
|
BoxLocation::MiddleCentre,
|
||||||
|
);
|
||||||
|
|
||||||
let split_popup = Layout::default()
|
let split_popup = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
@@ -505,6 +537,7 @@ pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
|||||||
[
|
[
|
||||||
Constraint::Max(NAME_TEXT.lines().count() as u16),
|
Constraint::Max(NAME_TEXT.lines().count() as u16),
|
||||||
Constraint::Max(description_text.lines().count() as u16),
|
Constraint::Max(description_text.lines().count() as u16),
|
||||||
|
Constraint::Max(help_text.lines().count() as u16),
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
@@ -513,7 +546,8 @@ pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
|||||||
// Order is important here
|
// Order is important here
|
||||||
f.render_widget(Clear, area);
|
f.render_widget(Clear, area);
|
||||||
f.render_widget(name_paragraph, split_popup[0]);
|
f.render_widget(name_paragraph, split_popup[0]);
|
||||||
f.render_widget(description_paragraph, split_popup[1]);
|
f.render_widget(description_paragrpah, split_popup[1]);
|
||||||
|
f.render_widget(help_paragraph, split_popup[2]);
|
||||||
f.render_widget(block, area);
|
f.render_widget(block, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,38 +594,79 @@ pub fn draw_error<B: Backend>(f: &mut Frame<'_, B>, error: AppError, seconds: Op
|
|||||||
.block(block)
|
.block(block)
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
|
|
||||||
let area = centered_info(lines as u16, max_line_width as u16, f.size());
|
let area = draw_popup(
|
||||||
|
lines as u16,
|
||||||
|
max_line_width as u16,
|
||||||
|
f.size(),
|
||||||
|
BoxLocation::MiddleCentre,
|
||||||
|
);
|
||||||
|
f.render_widget(Clear, area);
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show info box in bottom right corner
|
||||||
|
pub fn draw_info<B: Backend>(f: &mut Frame<'_, B>, text: String) {
|
||||||
|
let block = Block::default()
|
||||||
|
.title("")
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::NONE);
|
||||||
|
|
||||||
|
let mut max_line_width = 0;
|
||||||
|
text.lines().into_iter().for_each(|line| {
|
||||||
|
let width = line.chars().count();
|
||||||
|
if width > max_line_width {
|
||||||
|
max_line_width = width;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut lines = text.lines().count();
|
||||||
|
|
||||||
|
// Add some horizontal & vertical margins
|
||||||
|
max_line_width += 8;
|
||||||
|
lines += 2;
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.style(Style::default().bg(Color::Blue).fg(Color::White))
|
||||||
|
.block(block)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
|
||||||
|
let area = draw_popup(
|
||||||
|
lines as u16,
|
||||||
|
max_line_width as u16,
|
||||||
|
f.size(),
|
||||||
|
BoxLocation::BottomRight,
|
||||||
|
);
|
||||||
f.render_widget(Clear, area);
|
f.render_widget(Clear, area);
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// draw a box in the center of the screen, based on max line width + number of lines
|
/// draw a box in the center of the screen, based on max line width + number of lines
|
||||||
fn centered_info(number_lines: u16, max_line_width: u16, r: Rect) -> Rect {
|
fn draw_popup(text_lines: u16, text_width: u16, r: Rect, box_location: BoxLocation) -> Rect {
|
||||||
// This can panic if number_lines or max_line_width is larger than r.height or r.width
|
// Make sure blank_space can't be an negative, as will crash
|
||||||
let blank_vertical = (r.height - number_lines) / 2;
|
let blank_vertical = if r.height > text_lines {
|
||||||
let blank_horizontal = (r.width - max_line_width) / 2;
|
(r.height - text_lines) / 2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
let blank_horizontal = if r.width > text_width {
|
||||||
|
(r.width - text_width) / 2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
|
||||||
|
let vertical_constraints = box_location.get_vertical_constraints(blank_vertical, text_lines);
|
||||||
|
let horizontal_constraints =
|
||||||
|
box_location.get_horizontal_constraints(blank_horizontal, text_width);
|
||||||
|
|
||||||
|
let indexes = box_location.get_indexes();
|
||||||
|
|
||||||
let popup_layout = Layout::default()
|
let popup_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(
|
.constraints(vertical_constraints)
|
||||||
[
|
|
||||||
Constraint::Max(blank_vertical),
|
|
||||||
Constraint::Max(number_lines),
|
|
||||||
Constraint::Max(blank_vertical),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(r);
|
.split(r);
|
||||||
|
|
||||||
Layout::default()
|
Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(
|
.constraints(horizontal_constraints)
|
||||||
[
|
.split(popup_layout[indexes.0])[indexes.1]
|
||||||
Constraint::Max(blank_horizontal),
|
|
||||||
Constraint::Max(max_line_width),
|
|
||||||
Constraint::Max(blank_horizontal),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(popup_layout[1])[1]
|
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-19
@@ -1,5 +1,5 @@
|
|||||||
use std::{collections::HashMap, fmt};
|
use std::{collections::HashMap, fmt};
|
||||||
use tui::layout::Rect;
|
use tui::layout::{Constraint, Rect};
|
||||||
|
|
||||||
#[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 {
|
||||||
@@ -7,7 +7,86 @@ pub enum SelectablePanel {
|
|||||||
Commands,
|
Commands,
|
||||||
Logs,
|
Logs,
|
||||||
}
|
}
|
||||||
#[derive(Debug)]
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum BoxLocation {
|
||||||
|
TopLeft,
|
||||||
|
TopCentre,
|
||||||
|
TopRight,
|
||||||
|
MiddleLeft,
|
||||||
|
MiddleCentre,
|
||||||
|
MiddleRight,
|
||||||
|
BottomLeft,
|
||||||
|
BottomCentre,
|
||||||
|
BottomRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxLocation {
|
||||||
|
pub fn get_indexes(&self) -> (usize, usize) {
|
||||||
|
match self {
|
||||||
|
Self::TopLeft => (0, 0),
|
||||||
|
Self::TopCentre => (0, 1),
|
||||||
|
Self::TopRight => (0, 2),
|
||||||
|
Self::MiddleLeft => (1, 0),
|
||||||
|
Self::MiddleCentre => (1, 1),
|
||||||
|
Self::MiddleRight => (1, 2),
|
||||||
|
Self::BottomLeft => (2, 0),
|
||||||
|
Self::BottomCentre => (2, 1),
|
||||||
|
Self::BottomRight => (2, 2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should combine and just return a tupple?
|
||||||
|
pub fn get_horizontal_constraints(
|
||||||
|
&self,
|
||||||
|
blank_vertical: u16,
|
||||||
|
text_width: u16,
|
||||||
|
) -> [Constraint; 3] {
|
||||||
|
match self {
|
||||||
|
Self::TopLeft | Self::MiddleLeft | Self::BottomLeft => [
|
||||||
|
Constraint::Max(text_width),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
],
|
||||||
|
Self::TopCentre | Self::MiddleCentre | Self::BottomCentre => [
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(text_width),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
],
|
||||||
|
Self::TopRight | Self::MiddleRight | Self::BottomRight => [
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(text_width),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_vertical_constraints(
|
||||||
|
&self,
|
||||||
|
blank_vertical: u16,
|
||||||
|
number_lines: u16,
|
||||||
|
) -> [Constraint; 3] {
|
||||||
|
match self {
|
||||||
|
Self::TopLeft | Self::TopCentre | Self::TopRight => [
|
||||||
|
Constraint::Max(number_lines),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
],
|
||||||
|
Self::MiddleLeft | Self::MiddleCentre | Self::MiddleRight => [
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(number_lines),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
],
|
||||||
|
Self::BottomLeft | Self::BottomCentre | Self::BottomRight => [
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(blank_vertical),
|
||||||
|
Constraint::Max(number_lines),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum Loading {
|
pub enum Loading {
|
||||||
One,
|
One,
|
||||||
Two,
|
Two,
|
||||||
@@ -34,20 +113,9 @@ impl Loading {
|
|||||||
Self::Eight => Self::Nine,
|
Self::Eight => Self::Nine,
|
||||||
Self::Nine => Self::Ten,
|
Self::Nine => Self::Ten,
|
||||||
Self::Ten => Self::One,
|
Self::Ten => Self::One,
|
||||||
// Self::Five => Self::One
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// "⠋",
|
|
||||||
// "⠙",
|
|
||||||
// "⠹",
|
|
||||||
// "⠸",
|
|
||||||
// "⠼",
|
|
||||||
// "⠴",
|
|
||||||
// "⠦",
|
|
||||||
// "⠧",
|
|
||||||
// "⠇",
|
|
||||||
// "⠏"
|
|
||||||
|
|
||||||
impl fmt::Display for Loading {
|
impl fmt::Display for Loading {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
@@ -92,15 +160,19 @@ impl SelectablePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Global gui_state, stored in an Arc<Mutex>
|
/// Global gui_state, stored in an Arc<Mutex>
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GuiState {
|
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>,
|
area_map: HashMap<SelectablePanel, Rect>,
|
||||||
loading: Loading,
|
loading_icon: Loading,
|
||||||
|
// 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
|
||||||
|
is_loading: bool,
|
||||||
pub selected_panel: SelectablePanel,
|
pub selected_panel: SelectablePanel,
|
||||||
pub show_help: bool,
|
pub show_help: bool,
|
||||||
|
pub info_box_text: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GuiState {
|
impl GuiState {
|
||||||
@@ -108,9 +180,11 @@ impl GuiState {
|
|||||||
pub fn default() -> Self {
|
pub fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
area_map: HashMap::new(),
|
area_map: HashMap::new(),
|
||||||
loading: Loading::One,
|
loading_icon: Loading::One,
|
||||||
selected_panel: SelectablePanel::Containers,
|
selected_panel: SelectablePanel::Containers,
|
||||||
show_help: false,
|
show_help: false,
|
||||||
|
is_loading: false,
|
||||||
|
info_box_text: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,15 +221,33 @@ impl GuiState {
|
|||||||
self.selected_panel = self.selected_panel.prev();
|
self.selected_panel = self.selected_panel.prev();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Advance loading animation
|
||||||
pub fn next_loading(&mut self) {
|
pub fn next_loading(&mut self) {
|
||||||
self.loading = self.loading.next()
|
self.loading_icon = self.loading_icon.next();
|
||||||
|
self.is_loading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// if is_loading, return loading animation frame, else single space
|
||||||
pub fn get_loading(&mut self) -> String {
|
pub fn get_loading(&mut self) -> String {
|
||||||
self.loading.to_string()
|
if self.is_loading {
|
||||||
|
self.loading_icon.to_string()
|
||||||
|
} else {
|
||||||
|
String::from(" ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// set is_loading to false, but keep animation frame at same state
|
||||||
pub fn reset_loading(&mut self) {
|
pub fn reset_loading(&mut self) {
|
||||||
self.loading = Loading::One;
|
self.is_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set info box content
|
||||||
|
pub fn set_info_box(&mut self, text: String) {
|
||||||
|
self.info_box_text = Some(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove info box content
|
||||||
|
pub fn reset_info_box(&mut self) {
|
||||||
|
self.info_box_text = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-8
@@ -5,12 +5,15 @@ use crossterm::{
|
|||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
sync::{atomic::Ordering, Arc},
|
sync::{atomic::Ordering, Arc},
|
||||||
};
|
};
|
||||||
use tokio::sync::broadcast::Sender;
|
use std::{
|
||||||
|
sync::atomic::AtomicBool,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
backend::{Backend, CrosstermBackend},
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
@@ -23,7 +26,10 @@ mod gui_state;
|
|||||||
|
|
||||||
pub use self::color_match::*;
|
pub use self::color_match::*;
|
||||||
pub use self::gui_state::{GuiState, SelectablePanel};
|
pub use self::gui_state::{GuiState, SelectablePanel};
|
||||||
use crate::{app_data::AppData, app_error::AppError, input_handler::InputMessages};
|
use crate::{
|
||||||
|
app_data::AppData, app_error::AppError, docker_data::DockerMessage,
|
||||||
|
input_handler::InputMessages,
|
||||||
|
};
|
||||||
use draw_blocks::*;
|
use draw_blocks::*;
|
||||||
|
|
||||||
/// Take control of the terminal in order to draw gui
|
/// Take control of the terminal in order to draw gui
|
||||||
@@ -32,6 +38,8 @@ pub async fn create_ui(
|
|||||||
sender: Sender<InputMessages>,
|
sender: Sender<InputMessages>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
docker_sx: Sender<DockerMessage>,
|
||||||
|
update_duration: Duration,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
@@ -39,7 +47,16 @@ pub async fn create_ui(
|
|||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
let res = run_app(&mut terminal, app_data, sender, is_running, gui_state).await;
|
let res = run_app(
|
||||||
|
&mut terminal,
|
||||||
|
app_data,
|
||||||
|
sender,
|
||||||
|
is_running,
|
||||||
|
gui_state,
|
||||||
|
docker_sx,
|
||||||
|
update_duration,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
disable_raw_mode().unwrap();
|
disable_raw_mode().unwrap();
|
||||||
execute!(
|
execute!(
|
||||||
@@ -50,7 +67,7 @@ pub async fn create_ui(
|
|||||||
terminal.show_cursor().unwrap();
|
terminal.show_cursor().unwrap();
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
err.disp()
|
println!("{}", err);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -62,6 +79,8 @@ async fn run_app<B: Backend>(
|
|||||||
sender: Sender<InputMessages>,
|
sender: Sender<InputMessages>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
docker_sx: Sender<DockerMessage>,
|
||||||
|
update_duration: Duration,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let input_poll_rate = std::time::Duration::from_millis(75);
|
let input_poll_rate = std::time::Duration::from_millis(75);
|
||||||
|
|
||||||
@@ -83,6 +102,7 @@ async fn run_app<B: Backend>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let mut now = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui(f, &app_data, &gui_state)).unwrap();
|
terminal.draw(|f| ui(f, &app_data, &gui_state)).unwrap();
|
||||||
if crossterm::event::poll(input_poll_rate).unwrap() {
|
if crossterm::event::poll(input_poll_rate).unwrap() {
|
||||||
@@ -90,15 +110,24 @@ async fn run_app<B: Backend>(
|
|||||||
if let Event::Key(key) = event {
|
if let Event::Key(key) = event {
|
||||||
sender
|
sender
|
||||||
.send(InputMessages::ButtonPress(key.code))
|
.send(InputMessages::ButtonPress(key.code))
|
||||||
.unwrap_or(0);
|
.await
|
||||||
|
.unwrap_or(());
|
||||||
} else if let Event::Mouse(m) = event {
|
} else if let Event::Mouse(m) = event {
|
||||||
sender.send(InputMessages::MouseEvent(m)).unwrap_or(0);
|
sender
|
||||||
|
.send(InputMessages::MouseEvent(m))
|
||||||
|
.await
|
||||||
|
.unwrap_or(());
|
||||||
} else if let Event::Resize(_, _) = event {
|
} else if let Event::Resize(_, _) = event {
|
||||||
gui_state.lock().clear_area_map();
|
gui_state.lock().clear_area_map();
|
||||||
terminal.autoresize().unwrap_or(());
|
terminal.autoresize().unwrap_or(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if now.elapsed() >= update_duration {
|
||||||
|
docker_sx.send(DockerMessage::Update).await.unwrap();
|
||||||
|
now = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
if !is_running.load(Ordering::SeqCst) {
|
if !is_running.load(Ordering::SeqCst) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -126,6 +155,8 @@ fn ui<B: Backend>(
|
|||||||
let log_index = app_data.lock().get_selected_log_index();
|
let log_index = app_data.lock().get_selected_log_index();
|
||||||
let selected_panel = gui_state.lock().selected_panel;
|
let selected_panel = gui_state.lock().selected_panel;
|
||||||
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 loading_icon = gui_state.lock().get_loading();
|
||||||
|
|
||||||
let whole_layout = Layout::default()
|
let whole_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
@@ -187,14 +218,16 @@ fn ui<B: Backend>(
|
|||||||
f,
|
f,
|
||||||
gui_state,
|
gui_state,
|
||||||
log_index,
|
log_index,
|
||||||
|
loading_icon.to_owned(),
|
||||||
&selected_panel,
|
&selected_panel,
|
||||||
);
|
);
|
||||||
|
|
||||||
draw_info_bar(
|
draw_heading_bar(
|
||||||
whole_layout[0],
|
whole_layout[0],
|
||||||
&column_widths,
|
&column_widths,
|
||||||
f,
|
f,
|
||||||
has_containers,
|
has_containers,
|
||||||
|
loading_icon,
|
||||||
show_help,
|
show_help,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -203,6 +236,10 @@ fn ui<B: Backend>(
|
|||||||
draw_chart(f, lower_main[1], app_data, log_index);
|
draw_chart(f, lower_main[1], app_data, log_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(info) = info_text {
|
||||||
|
draw_info(f, info);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if error, and show popup if so
|
// Check if error, and show popup if so
|
||||||
if show_help {
|
if show_help {
|
||||||
draw_help_box(f);
|
draw_help_box(f);
|
||||||
|
|||||||
Reference in New Issue
Block a user