diff --git a/.github/release-body.md b/.github/release-body.md
index 4d77784..b689008 100644
--- a/.github/release-body.md
+++ b/.github/release-body.md
@@ -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 CHANGELOG.md for more details
diff --git a/.github/screenshot_01.jpg b/.github/screenshot_01.jpg
index fd08e93..44ece7b 100644
Binary files a/.github/screenshot_01.jpg and b/.github/screenshot_01.jpg differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18a3ffc..9e484f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,16 @@
+# v0.0.2
+### 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
-+ readme.md typo, [589501f9a4a0bfabdb0654e68cc0c752c529d97a]
-+ column heading mem > memory, [5e8e6b590b06f01a542fdd10bae8f14d303ab08a]
-+ cargo fmt added to create_release.sh, [bb29c0ebfafd6a9a036eb317a240954d1405966e]
++ readme.md typo, [589501f9a4a0bfabdb0654e68cc0c752c529d97a](https://github.com/mrjackwills/oxker/commit/589501f9a4a0bfabdb0654e68cc0c752c529d97a),
++ column heading mem > memory, [5e8e6b590b06f01a542fdd10bae8f14d303ab08a](https://github.com/mrjackwills/oxker/commit/5e8e6b590b06f01a542fdd10bae8f14d303ab08a),
++ cargo fmt added to create_release.sh, [bb29c0ebfafd6a9a036eb317a240954d1405966e](https://github.com/mrjackwills/oxker/commit/bb29c0ebfafd6a9a036eb317a240954d1405966e),
# v0.0.1
### 2022-04-25
diff --git a/Cargo.toml b/Cargo.toml
index 59fce02..e69da58 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "oxker"
-version = "0.0.1"
+version = "0.0.2"
edition = "2021"
authors = ["Jack Wills "]
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"]}
tracing = "0.1.32"
tracing-subscriber = "0.3.9"
-tui = "0.17"
+tui = "0.18"
[dev-dependencies]
diff --git a/README.md b/README.md
index ae524e6..8035554 100644
--- a/README.md
+++ b/README.md
@@ -21,14 +21,20 @@
-## Download
+## Download & install
See releases
+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
-```./oxker```
+```oxker```
available command line arguments
| argument|result|
@@ -56,7 +62,7 @@ requires docker & 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),
status: (String::from("status"), 16),
+ // 7 to allow for "100.00%"
+ cpu: (String::from("cpu"), 7),
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_tx: (String::from("↑ tx"), 5),
}
diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs
index e6ede5e..06dc806 100644
--- a/src/app_data/mod.rs
+++ b/src/app_data/mod.rs
@@ -304,6 +304,7 @@ impl AppData {
if self.containers.state.selected().is_some() {
self.containers.previous();
}
+ // docker rm -f $(docker ps -aq) will cause this to crash
self.containers.items.remove(index);
}
}
diff --git a/src/app_error.rs b/src/app_error.rs
index ed28d6f..d6111cd 100644
--- a/src/app_error.rs
+++ b/src/app_error.rs
@@ -1,7 +1,5 @@
-use core::fmt;
-use tracing::error;
-
use crate::app_data::DockerControls;
+use core::fmt;
/// app errors to set in global state
#[allow(unused)]
@@ -11,25 +9,10 @@ pub enum AppError {
DockerInterval,
InputPoll,
DockerCommand(DockerControls),
+ MouseCapture(bool),
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
impl fmt::Display for AppError {
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::Terminal => "Unable to draw to terminal".to_owned(),
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)
}
diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs
new file mode 100644
index 0000000..3d008f2
--- /dev/null
+++ b/src/docker_data/message.rs
@@ -0,0 +1,9 @@
+#[derive(Debug, Clone)]
+pub enum DockerMessage {
+ Update,
+ Start(String),
+ Restart(String),
+ Pause(String),
+ Unpause(String),
+ Stop(String),
+}
diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs
index 0ab2a94..71375ec 100644
--- a/src/docker_data/mod.rs
+++ b/src/docker_data/mod.rs
@@ -1,22 +1,27 @@
use bollard::{
- container::{ListContainersOptions, LogsOptions, Stats, StatsOptions},
+ container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions},
Docker,
};
use futures_util::{future::join_all, StreamExt};
use parking_lot::Mutex;
-use std::{
- sync::Arc,
- time::{Duration, Instant},
-};
+use std::sync::Arc;
+use tokio::{sync::mpsc::Receiver, task::JoinHandle};
-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 {
app_data: Arc>,
docker: Arc,
gui_state: Arc>,
initialised: bool,
- sleep_duration: Duration,
+ receiver: Receiver,
timestamps: bool,
}
@@ -178,8 +183,7 @@ impl DockerData {
}
/// Update all logs, spawn each container into own tokio::spawn thread
- // rename init all logs, as only gets run once
- async fn update_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() {
@@ -207,43 +211,32 @@ impl DockerData {
self.update_all_container_stats(&all_ids).await;
}
- /// Initialise self, and start the updated loop
- pub async fn init(
- args: CliArgs,
- app_data: Arc>,
- docker: Arc,
- gui_state: Arc>,
- ) {
- 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) {
+ /// Animate the loading icon
+ async fn loading_spin(&mut self) -> JoinHandle<()> {
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
- // so instead just abort at end of function
- let loading_spin = tokio::spawn(async move {
+ tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
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;
self.update_all_container_stats(&all_ids).await;
// 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() {
self.initialised = true;
@@ -255,23 +248,94 @@ impl DockerData {
self.initialised = self.app_data.lock().initialised(&all_ids);
}
self.app_data.lock().init = true;
- loading_spin.abort();
- self.gui_state.lock().reset_loading();
+ self.stop_loading_spin(loading_spin);
}
- /// Update all items, wait until all complete
- /// sleep for CliArgs.docker ms before updating next
- async fn update_loop(&mut self) {
- loop {
- let start = Instant::now();
- self.update_everything().await;
-
- let elapsed = start.elapsed();
- if elapsed < self.sleep_duration {
- tokio::time::sleep(self.sleep_duration - elapsed).await;
+ /// Handle incoming messages, container controls & all container information update
+ async fn message_handler(&mut self) {
+ while let Some(message) = self.receiver.recv().await {
+ let docker = Arc::clone(&self.docker);
+ let app_data = Arc::clone(&self.app_data);
+ match message {
+ DockerMessage::Pause(id) => {
+ let loading_spin = self.loading_spin().await;
+ docker.pause_container(&id).await.unwrap_or_else(|_| {
+ 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::>)
+ .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>,
+ docker: Arc,
+ gui_state: Arc>,
+ receiver: Receiver,
+ ) {
+ 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
diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs
index 3e14fb5..0dfead7 100644
--- a/src/input_handler/mod.rs
+++ b/src/input_handler/mod.rs
@@ -3,16 +3,24 @@ use std::sync::{
Arc,
};
-use bollard::{container::StartContainerOptions, Docker};
-use crossterm::event::{KeyCode, MouseButton, MouseEvent, MouseEventKind};
+use crossterm::{
+ event::{
+ DisableMouseCapture, EnableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind,
+ },
+ execute,
+};
use parking_lot::Mutex;
-use tokio::sync::broadcast::Receiver;
+use tokio::{
+ sync::mpsc::{Receiver, Sender},
+ task::JoinHandle,
+};
use tui::layout::Rect;
mod message;
use crate::{
app_data::{AppData, DockerControls},
app_error::AppError,
+ docker_data::DockerMessage,
ui::{GuiState, SelectablePanel},
};
pub use message::InputMessages;
@@ -21,9 +29,11 @@ pub use message::InputMessages;
#[derive(Debug)]
pub struct InputHandler {
app_data: Arc>,
- docker: Arc,
+ docker_sender: Sender,
gui_state: Arc>,
+ info_sleep: Option>,
is_running: Arc,
+ mouse_capture: bool,
rec: Receiver,
}
@@ -32,23 +42,25 @@ impl InputHandler {
pub async fn init(
app_data: Arc>,
rec: Receiver,
- docker: Arc,
+ docker_sender: Sender,
gui_state: Arc>,
is_running: Arc,
) {
let mut inner = Self {
app_data,
- docker,
+ docker_sender,
gui_state,
is_running,
rec,
+ mouse_capture: true,
+ info_sleep: None,
};
inner.start().await;
}
/// check for incoming messages
async fn start(&mut self) {
- while let Ok(message) = self.rec.recv().await {
+ while let Some(message) = self.rec.recv().await {
match message {
InputMessages::ButtonPress(key_code) => self.button_press(key_code).await,
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
async fn button_press(&mut self, key_code: KeyCode) {
let show_error = self.app_data.lock().show_error;
let show_info = self.gui_state.lock().show_help;
+
if show_error {
match key_code {
KeyCode::Char('q') => {
@@ -82,22 +130,16 @@ impl InputHandler {
}
} else if show_info {
match key_code {
- KeyCode::Char('q') => {
- self.is_running.store(false, Ordering::SeqCst);
- }
- KeyCode::Char('h') => {
- self.gui_state.lock().show_help = false;
- }
+ KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst),
+ KeyCode::Char('h') => self.gui_state.lock().show_help = false,
+ KeyCode::Char('m') => self.m_button(),
_ => (),
}
} else {
match key_code {
- KeyCode::Char('q') => {
- self.is_running.store(false, Ordering::SeqCst);
- }
- KeyCode::Char('h') => {
- self.gui_state.lock().show_help = true;
- }
+ KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst),
+ KeyCode::Char('h') => self.gui_state.lock().show_help = true,
+ KeyCode::Char('m') => self.m_button(),
KeyCode::Tab => self.gui_state.lock().next_panel(),
KeyCode::BackTab => self.gui_state.lock().previous_panel(),
KeyCode::Home => {
@@ -129,90 +171,44 @@ impl InputHandler {
}
}
KeyCode::Enter => {
- // Does is matter though?
// 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
+ // Does is matter though?
let panel = self.gui_state.lock().selected_panel;
if panel == SelectablePanel::Commands {
let command = self.app_data.lock().get_docker_command();
if command.is_some() {
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() {
let id = id.unwrap();
match command.unwrap() {
- DockerControls::Pause => {
- tokio::spawn(async move {
- docker.pause_container(&id).await.unwrap_or_else(
- |_| {
- app_data.lock().set_error(
- AppError::DockerCommand(
- DockerControls::Pause,
- ),
- )
- },
- );
- });
- }
- DockerControls::Unpause => {
- tokio::spawn(async move {
- docker.unpause_container(&id).await.unwrap_or_else(
- |_| {
- app_data.lock().set_error(
- AppError::DockerCommand(
- DockerControls::Unpause,
- ),
- )
- },
- );
- });
- }
- DockerControls::Start => {
- tokio::spawn(async move {
- docker
- .start_container(
- &id,
- None::>,
- )
- .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,
- ),
- )
- });
- });
- }
+ // TODO handle theses errors?
+ DockerControls::Pause => self
+ .docker_sender
+ .send(DockerMessage::Pause(id))
+ .await
+ .unwrap(),
+ DockerControls::Unpause => self
+ .docker_sender
+ .send(DockerMessage::Unpause(id))
+ .await
+ .unwrap(),
+ DockerControls::Start => self
+ .docker_sender
+ .send(DockerMessage::Start(id))
+ .await
+ .unwrap(),
+ DockerControls::Stop => self
+ .docker_sender
+ .send(DockerMessage::Stop(id))
+ .await
+ .unwrap(),
+ DockerControls::Restart => self
+ .docker_sender
+ .send(DockerMessage::Restart(id))
+ .await
+ .unwrap(),
}
}
}
diff --git a/src/main.rs b/src/main.rs
index 97a3e50..2b38183 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -31,13 +31,21 @@ async fn main() {
let docker_app_data = Arc::clone(&app_data);
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
let docker = Arc::new(Docker::connect_with_socket_defaults().unwrap());
match docker.ping().await {
Ok(_) => {
let docker = Arc::clone(&docker);
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),
@@ -45,19 +53,20 @@ async fn main() {
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 input_is_running = Arc::clone(&is_running);
let input_gui_state = Arc::clone(&gui_state);
+ let input_docker_sender = docker_sx.clone();
// Spawn input handling into own tokio thread
tokio::spawn(async {
input_handler::InputHandler::init(
input_app_data,
- r,
- input_docker,
+ input_rx,
+ input_docker_sender,
input_gui_state,
input_is_running,
)
@@ -71,6 +80,16 @@ async fn main() {
tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
}
} 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();
}
}
diff --git a/src/parse_args/mod.rs b/src/parse_args/mod.rs
index 351917a..524da85 100644
--- a/src/parse_args/mod.rs
+++ b/src/parse_args/mod.rs
@@ -8,13 +8,16 @@ use tracing::error;
pub struct CliArgs {
/// Docker update interval in ms, minimum 1, reccomended 500+
- #[clap(short = 'd', default_value_t = 1000)]
- pub docker: u32,
+ #[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
+ pub docker_interval: u32,
/// Don't draw gui - for debugging - mostly pointless
#[clap(short = 'g')]
pub gui: bool,
+ // /// Install to ./local/bin
+ // #[clap(short = 'i')]
+ // pub install: bool,
/// Remove timestamps from Docker logs
#[clap(short = 't')]
pub timestamp: bool,
@@ -35,15 +38,16 @@ impl CliArgs {
// Quit the program if the docker update argument is 0
// 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");
process::exit(1)
}
Self {
color: args.color,
- docker: args.docker,
+ docker_interval: args.docker_interval,
gui: !args.gui,
raw: args.raw,
+ // install: args.install,
timestamp: !args.timestamp,
}
}
diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs
index f3ac6c9..182462c 100644
--- a/src/ui/draw_blocks.rs
+++ b/src/ui/draw_blocks.rs
@@ -19,6 +19,7 @@ use crate::{
app_error::AppError,
};
+use super::gui_state::BoxLocation;
use super::{GuiState, SelectablePanel};
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 VERSION: &str = env!("CARGO_PKG_VERSION");
const REPO: &str = env!("CARGO_PKG_REPOSITORY");
+const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
const ORANGE: Color = Color::Rgb(255, 178, 36);
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
fn generate_block<'a>(
selectable_panel: Option,
app_data: &Arc>,
selected_panel: &SelectablePanel,
) -> 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 {
let title = match panel {
@@ -62,11 +66,7 @@ fn generate_block<'a>(
};
block = block.title(title);
if selected_panel == &panel {
- let selected_style = Style::default().fg(Color::LightCyan);
- let selected_border = BorderType::Plain;
- block = block
- .border_style(selected_style)
- .border_type(selected_border);
+ block = block.border_style(Style::default().fg(Color::LightCyan));
}
}
block
@@ -170,6 +170,15 @@ pub fn draw_containers(
format!("{}{:>width$}", MARGIN, mems, width = widths.mem.1),
state_style,
),
+ Span::styled(
+ format!(
+ "{}{:>width$}",
+ MARGIN,
+ i.id.chars().take(8).collect::(),
+ width = widths.id.1
+ ),
+ blue,
+ ),
Span::styled(
format!("{}{:>width$}", MARGIN, i.name, width = widths.name.1),
blue,
@@ -214,6 +223,7 @@ pub fn draw_logs(
f: &mut Frame<'_, B>,
gui_state: &Arc>,
index: Option,
+ loading_icon: String,
selected_panel: &SelectablePanel,
) {
let panel = SelectablePanel::Logs;
@@ -224,8 +234,8 @@ pub fn draw_logs(
let init = app_data.lock().init;
if !init {
- let icon = gui_state.lock().get_loading();
- let parsing_logs = format!("parsing logs {}", icon);
+ // let icon = gui_state.lock().get_loading();
+ let parsing_logs = format!("parsing logs {}", loading_icon);
let paragraph = Paragraph::new(parsing_logs)
.style(Style::default())
.block(block)
@@ -330,7 +340,7 @@ fn make_chart(
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL)
- .border_type(BorderType::Plain),
+ .border_type(BorderType::Rounded),
)
.x_axis(
Axis::default()
@@ -348,27 +358,32 @@ fn make_chart(
.fg(label_color),
),
])
- // add 0.01, for cases when the value is 0
.bounds([0.0, max.get_value() + 0.01]),
)
}
/// Show error popup over whole screen
-pub fn draw_info_bar(
+pub fn draw_heading_bar(
area: Rect,
columns: &Columns,
f: &mut Frame<'_, B>,
has_containers: bool,
+ loading_icon: String,
info_visible: bool,
) {
let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black));
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(
format!(
- "{} {:>width$}",
+ "{} {:>width$}",
MARGIN,
columns.status.0,
width = columns.status.1
@@ -379,6 +394,8 @@ pub fn draw_info_bar(
.push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str());
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$}",
@@ -455,18 +472,23 @@ pub fn draw_info_bar(
pub fn draw_help_box(f: &mut Frame<'_, B>) {
let title = format!(" {} ", VERSION);
- let mut description_text =
- 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.");
- description_text.push_str("\n Mouse input also available.");
- description_text.push_str("\n ( q ) to quit at any time.");
- description_text
- .push_str("\n\n currenty an early work in progress, all and any input appreciated");
- description_text.push_str(format!("\n {}", REPO.trim()).as_str());
+ let description_text = format!("\n{}", DESCRIPTION);
+
+ let mut help_text = String::from("\n ( tab ) or ( alt+tab ) to change panels");
+ help_text.push_str("\n ( ↑ ↓ ← → ) to change selected line");
+ 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 ( 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 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| {
let width = line.chars().count();
@@ -486,7 +508,12 @@ pub fn draw_help_box(f: &mut Frame<'_, B>) {
.block(Block::default())
.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))
.block(Block::default())
.alignment(Alignment::Left);
@@ -497,7 +524,12 @@ pub fn draw_help_box(f: &mut Frame<'_, B>) {
.border_type(BorderType::Rounded)
.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()
.direction(Direction::Vertical)
@@ -505,6 +537,7 @@ pub fn draw_help_box(f: &mut Frame<'_, B>) {
[
Constraint::Max(NAME_TEXT.lines().count() as u16),
Constraint::Max(description_text.lines().count() as u16),
+ Constraint::Max(help_text.lines().count() as u16),
]
.as_ref(),
)
@@ -513,7 +546,8 @@ pub fn draw_help_box(f: &mut Frame<'_, B>) {
// Order is important here
f.render_widget(Clear, area);
f.render_widget(name_paragraph, split_popup[0]);
- f.render_widget(description_paragraph, split_popup[1]);
+ f.render_widget(description_paragrpah, split_popup[1]);
+ f.render_widget(help_paragraph, split_popup[2]);
f.render_widget(block, area);
}
@@ -560,38 +594,79 @@ pub fn draw_error(f: &mut Frame<'_, B>, error: AppError, seconds: Op
.block(block)
.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(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(paragraph, area);
}
/// 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 {
- // This can panic if number_lines or max_line_width is larger than r.height or r.width
- let blank_vertical = (r.height - number_lines) / 2;
- let blank_horizontal = (r.width - max_line_width) / 2;
+fn draw_popup(text_lines: u16, text_width: u16, r: Rect, box_location: BoxLocation) -> Rect {
+ // Make sure blank_space can't be an negative, as will crash
+ let blank_vertical = if r.height > text_lines {
+ (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()
.direction(Direction::Vertical)
- .constraints(
- [
- Constraint::Max(blank_vertical),
- Constraint::Max(number_lines),
- Constraint::Max(blank_vertical),
- ]
- .as_ref(),
- )
+ .constraints(vertical_constraints)
.split(r);
Layout::default()
.direction(Direction::Horizontal)
- .constraints(
- [
- Constraint::Max(blank_horizontal),
- Constraint::Max(max_line_width),
- Constraint::Max(blank_horizontal),
- ]
- .as_ref(),
- )
- .split(popup_layout[1])[1]
+ .constraints(horizontal_constraints)
+ .split(popup_layout[indexes.0])[indexes.1]
}
diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs
index eb53924..97983da 100644
--- a/src/ui/gui_state.rs
+++ b/src/ui/gui_state.rs
@@ -1,5 +1,5 @@
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)]
pub enum SelectablePanel {
@@ -7,7 +7,86 @@ pub enum SelectablePanel {
Commands,
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 {
One,
Two,
@@ -34,20 +113,9 @@ impl Loading {
Self::Eight => Self::Nine,
Self::Nine => Self::Ten,
Self::Ten => Self::One,
- // Self::Five => Self::One
}
}
}
-// "⠋",
-// "⠙",
-// "⠹",
-// "⠸",
-// "⠼",
-// "⠴",
-// "⠦",
-// "⠧",
-// "⠇",
-// "⠏"
impl fmt::Display for Loading {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -92,15 +160,19 @@ impl SelectablePanel {
}
/// Global gui_state, stored in an Arc
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct GuiState {
// 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
// If a BMapTree think it would mean have to implement ordering for SelectablePanel
area_map: HashMap,
- 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 show_help: bool,
+ pub info_box_text: Option,
}
impl GuiState {
@@ -108,9 +180,11 @@ impl GuiState {
pub fn default() -> Self {
Self {
area_map: HashMap::new(),
- loading: Loading::One,
+ loading_icon: Loading::One,
selected_panel: SelectablePanel::Containers,
show_help: false,
+ is_loading: false,
+ info_box_text: None,
}
}
@@ -147,15 +221,33 @@ impl GuiState {
self.selected_panel = self.selected_panel.prev();
}
+ /// Advance loading animation
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 {
- 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) {
- 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;
}
}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 002175a..15d04d2 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -5,12 +5,15 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use parking_lot::Mutex;
-use std::sync::atomic::AtomicBool;
use std::{
io,
sync::{atomic::Ordering, Arc},
};
-use tokio::sync::broadcast::Sender;
+use std::{
+ sync::atomic::AtomicBool,
+ time::{Duration, Instant},
+};
+use tokio::sync::mpsc::Sender;
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
@@ -23,7 +26,10 @@ mod gui_state;
pub use self::color_match::*;
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::*;
/// Take control of the terminal in order to draw gui
@@ -32,6 +38,8 @@ pub async fn create_ui(
sender: Sender,
is_running: Arc,
gui_state: Arc>,
+ docker_sx: Sender,
+ update_duration: Duration,
) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -39,7 +47,16 @@ pub async fn create_ui(
let backend = CrosstermBackend::new(stdout);
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();
execute!(
@@ -50,7 +67,7 @@ pub async fn create_ui(
terminal.show_cursor().unwrap();
if let Err(err) = res {
- err.disp()
+ println!("{}", err);
}
Ok(())
}
@@ -62,6 +79,8 @@ async fn run_app(
sender: Sender,
is_running: Arc,
gui_state: Arc>,
+ docker_sx: Sender,
+ update_duration: Duration,
) -> Result<(), AppError> {
let input_poll_rate = std::time::Duration::from_millis(75);
@@ -83,6 +102,7 @@ async fn run_app(
}
}
} else {
+ let mut now = Instant::now();
loop {
terminal.draw(|f| ui(f, &app_data, &gui_state)).unwrap();
if crossterm::event::poll(input_poll_rate).unwrap() {
@@ -90,15 +110,24 @@ async fn run_app(
if let Event::Key(key) = event {
sender
.send(InputMessages::ButtonPress(key.code))
- .unwrap_or(0);
+ .await
+ .unwrap_or(());
} 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 {
gui_state.lock().clear_area_map();
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) {
break;
}
@@ -126,6 +155,8 @@ fn ui(
let log_index = app_data.lock().get_selected_log_index();
let selected_panel = gui_state.lock().selected_panel;
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()
.direction(Direction::Vertical)
@@ -187,14 +218,16 @@ fn ui(
f,
gui_state,
log_index,
+ loading_icon.to_owned(),
&selected_panel,
);
- draw_info_bar(
+ draw_heading_bar(
whole_layout[0],
&column_widths,
f,
has_containers,
+ loading_icon,
show_help,
);
@@ -203,6 +236,10 @@ fn ui(
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
if show_help {
draw_help_box(f);