chore: merge release-v0.0.5 into main

This commit is contained in:
Jack Wills
2022-05-30 02:36:07 +00:00
15 changed files with 179 additions and 143 deletions
+1 -1
View File
@@ -6,4 +6,4 @@ FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT}
RUN printf "alias cls='clear'\nalias ll='ls -l --human-readable --color=auto --group-directories-first --classify --time-style=long-iso -all'" >> /etc/bash.bashrc RUN printf "alias cls='clear'\nalias ll='ls -l --human-readable --color=auto --group-directories-first --classify --time-style=long-iso -all'" >> /etc/bash.bashrc
RUN apt-get update && apt-get -y install upx-ucl # RUN apt-get update && apt-get -y install upx-ucl
+2 -2
View File
@@ -17,8 +17,8 @@
], ],
"mounts": [ "mounts": [
"source=/etc/timezone,target=/etc/timezone,type=bind,readonly", // //"source=/etc/timezone,target=/etc/timezone,type=bind,readonly",
"source=/ramdrive,target=/ramdrive,type=bind", "source=/dev/shm,target=/ramdrive,type=bind",
"source=${localEnv:HOME}/.cargo/bin/cargo-watch,target=/usr/local/cargo/bin/cargo-watch,type=bind,readonly", "source=${localEnv:HOME}/.cargo/bin/cargo-watch,target=/usr/local/cargo/bin/cargo-watch,type=bind,readonly",
"source=${localEnv:HOME}/.cargo/bin/cross,target=/usr/local/cargo/bin/cross,type=bind,readonly", "source=${localEnv:HOME}/.cargo/bin/cross,target=/usr/local/cargo/bin/cross,type=bind,readonly",
], ],
+8 -2
View File
@@ -1,7 +1,13 @@
### 2022-05-08 ### 2022-05-30
### Docs
+ Readme one-liner to download & install latest version, [11d5ba361ee4c11d080f1c3c14d8bb677cbfd1fc]
+ Example docker-compose.yml bump alpine version to 3.16, [98c83f2f68f59e78f0c78270c59886630d98913c]
### Fixes ### Fixes
+ Help menu logo corrected, [2f5452027e86f714729b804d4bf65306e755df7f] + use Some() checks to make sure that container item indexes are still valid, else can create out-of-bounds errors, closes [#8], [4cf02e3f04426ef44ec5a7421687f2104ac5102f]
+ Remove + replace as many unwrap()'s as possible, [d8e22d7444965f1874d7367259310440a889432b]
+ Help panel typo, [e497f3f2d9e1dca99469860c2e728c99e29353ad]
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
+12
View File
@@ -1,3 +1,15 @@
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.5'>v0.0.5</a>
### 2022-05-30
### Docs
+ Readme one-liner to download & install latest version, [11d5ba36](https://github.com/mrjackwills/oxker/commit/11d5ba361ee4c11d080f1c3c14d8bb677cbfd1fc),
+ Example docker-compose.yml bump alpine version to 3.16, [98c83f2f](https://github.com/mrjackwills/oxker/commit/98c83f2f68f59e78f0c78270c59886630d98913c),
### Fixes
+ use Some() checks to make sure that container item indexes are still valid, else can create out-of-bounds errors, closes [#8](https://github.com/mrjackwills/oxker/issues/8), [4cf02e3f](https://github.com/mrjackwills/oxker/commit/4cf02e3f04426ef44ec5a7421687f2104ac5102f),
+ Remove + replace as many unwrap()'s as possible, [d8e22d74](https://github.com/mrjackwills/oxker/commit/d8e22d7444965f1874d7367259310440a889432b),
+ Help panel typo, [e497f3f2](https://github.com/mrjackwills/oxker/commit/e497f3f2d9e1dca99469860c2e728c99e29353ad),
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.4'>v0.0.4</a> # <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.0.4'>v0.0.4</a>
### 2022-05-08 ### 2022-05-08
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "oxker" name = "oxker"
version = "0.0.4" version = "0.0.5"
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"
+6 -4
View File
@@ -25,13 +25,15 @@
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 download & install (x86_64 one liner)
```bash ```bash
tar xzvf oxker_linux_x86_64.tar.gz oxker wget https://www.github.com/mrjackwills/oxker/releases/latest/download/oxker_linux_x86_64.tar.gz &&
install -Dm 755 oxker -t "${HOME}/.local/bin" 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 rm oxker_linux_x86_64.tar.gz oxker
``` ```
## Run ## Run
```oxker``` ```oxker```
@@ -88,7 +90,7 @@ using docker-compose.yml;
or individually or individually
```docker run --name redis -d redis:alpine3.15``` ```docker run --name redis -d redis:alpine3.16```
```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```
+2 -2
View File
@@ -4,7 +4,7 @@ networks:
name: oxker-examaple-net name: oxker-examaple-net
services: services:
postgres: postgres:
image: postgres:alpine image: postgres:alpine3.16
container_name: postgres container_name: postgres
environment: environment:
- POSTGRES_PASSWORD=never_use_this_password_in_production - POSTGRES_PASSWORD=never_use_this_password_in_production
@@ -18,7 +18,7 @@ services:
limits: limits:
memory: 128M memory: 128M
redis: redis:
image: redis:alpine image: redis:alpine3.16
container_name: redis container_name: redis
ipc: private ipc: private
restart: always restart: always
+2 -1
View File
@@ -160,6 +160,7 @@ impl DockerControls {
match state { match state {
State::Dead | State::Exited => vec![Self::Start, Self::Restart], State::Dead | State::Exited => vec![Self::Start, Self::Restart],
State::Paused => vec![Self::Unpause, Self::Stop], State::Paused => vec![Self::Unpause, Self::Stop],
State::Restarting => vec![Self::Stop],
State::Running => vec![Self::Pause, Self::Restart, Self::Stop], State::Running => vec![Self::Pause, Self::Restart, Self::Stop],
_ => vec![], _ => vec![],
} }
@@ -276,7 +277,7 @@ impl Stats for ByteStats {
} }
} }
// convert from bytes to kb, mb, gb etc // convert from bytes to kB, MB, GB etc
impl fmt::Display for ByteStats { impl fmt::Display for ByteStats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let one_kb = 1000.0; let one_kb = 1000.0;
+56 -52
View File
@@ -99,9 +99,9 @@ impl AppData {
self.error = Some(error); self.error = Some(error);
} }
/// Find the if of the currently selected container /// Find the if of the currently selected container.
/// If any containers on system, will always return /// If any containers on system, will always return a string.
/// Only returns None when no containers found /// Only returns None when no containers found.
pub fn get_selected_container_id(&self) -> Option<String> { pub fn get_selected_container_id(&self) -> Option<String> {
let mut output = None; let mut output = None;
if let Some(index) = self.containers.state.selected() { if let Some(index) = self.containers.state.selected() {
@@ -296,7 +296,7 @@ impl AppData {
for (index, id) in all_ids.iter().enumerate() { for (index, id) in all_ids.iter().enumerate() {
if !containers if !containers
.iter() .iter()
.map(|i| i.id.as_ref().unwrap()) .filter_map(|i| i.id.as_ref())
.any(|x| x == id) .any(|x| x == id)
{ {
// If removed container is currently selected, then change selected to previous // If removed container is currently selected, then change selected to previous
@@ -304,60 +304,64 @@ 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 // Check is some, else can cause out of bounds error, if containers get removed before a docker update
self.containers.items.remove(index); if self.containers.items.get(index).is_some() {
self.containers.items.remove(index);
}
} }
} }
for i in containers.iter() { for i in containers.iter() {
let id = i.id.as_ref().unwrap().to_owned(); if let Some(id) = i.id.as_ref() {
let mut name = i let mut name = i
.names .names
.as_ref() .as_ref()
.unwrap_or(&vec!["".to_owned()]) .unwrap_or(&vec!["".to_owned()])
.get(0) .get(0)
.unwrap() .unwrap()
.to_owned(); .to_owned();
if let Some(c) = name.chars().next() { if let Some(c) = name.chars().next() {
if c == '/' { if c == '/' {
name.remove(0); name.remove(0);
}
} }
}
let state = State::from(i.state.as_ref().unwrap_or(&"dead".to_owned()).trim()); let state = State::from(i.state.as_ref().unwrap_or(&"dead".to_owned()).trim());
let status = i let status = i
.status .status
.as_ref() .as_ref()
.unwrap_or(&"".to_owned()) .unwrap_or(&"".to_owned())
.trim() .trim()
.to_owned(); .to_owned();
let image = i.image.as_ref().unwrap_or(&"".to_owned()).trim().to_owned(); let image = i.image.as_ref().unwrap_or(&"".to_owned()).trim().to_owned();
if let Some(current_container) = self.get_container_by_id(&id) { if let Some(current_container) = self.get_container_by_id(id) {
if current_container.name != name { if current_container.name != name {
current_container.name = name current_container.name = name
};
if current_container.status != status {
current_container.status = status
};
if current_container.state != state {
current_container.docker_controls.items = DockerControls::gen_vec(&state);
// Update the list state, needs to be None if the gen_vec returns an empty vec
match state {
State::Removing | State::Restarting | State::Unknown => {
current_container.docker_controls.state.select(None)
}
_ => current_container.docker_controls.start(),
}; };
current_container.state = state; if current_container.status != status {
}; current_container.status = status
if current_container.image != image { };
current_container.image = image if current_container.state != state {
}; current_container.docker_controls.items = DockerControls::gen_vec(&state);
} else {
let mut container = ContainerItem::new(id, status, image, state, name); // Update the list state, needs to be None if the gen_vec returns an empty vec
container.logs.end(); match state {
self.containers.items.push(container); State::Removing | State::Restarting | State::Unknown => {
current_container.docker_controls.state.select(None)
}
_ => current_container.docker_controls.start(),
};
current_container.state = state;
};
if current_container.image != image {
current_container.image = image
};
} else {
let mut container =
ContainerItem::new(id.to_owned(), status, image, state, name);
container.logs.end();
self.containers.items.push(container);
}
} }
} }
} }
@@ -379,7 +383,7 @@ impl AppData {
container.logs.items.push(ListItem::new(lines)); container.logs.items.push(ListItem::new(lines));
}); });
if container.logs.state.selected().is_none() if container.logs.state.selected().is_none()
|| container.logs.state.selected().unwrap() + 1 == current_len || container.logs.state.selected().unwrap_or_default() + 1 == current_len
{ {
container.logs.end(); container.logs.end();
} }
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::app_data::DockerControls; use crate::app_data::DockerControls;
use core::fmt; use std::fmt;
/// app errors to set in global state /// app errors to set in global state
#[allow(unused)] #[allow(unused)]
+18 -14
View File
@@ -34,8 +34,8 @@ impl DockerData {
if stats.cpu_stats.system_cpu_usage.is_some() if stats.cpu_stats.system_cpu_usage.is_some()
&& stats.precpu_stats.system_cpu_usage.is_some() && stats.precpu_stats.system_cpu_usage.is_some()
{ {
let system_delta = (stats.cpu_stats.system_cpu_usage.unwrap() let system_delta = (stats.cpu_stats.system_cpu_usage.unwrap_or(0)
- stats.precpu_stats.system_cpu_usage.unwrap()) - stats.precpu_stats.system_cpu_usage.unwrap_or(0))
as f64; as f64;
let online_cpus = stats.cpu_stats.online_cpus.unwrap_or_else(|| { let online_cpus = stats.cpu_stats.online_cpus.unwrap_or_else(|| {
stats stats
@@ -75,7 +75,7 @@ impl DockerData {
let mem_stat = stats.memory_stats.usage.unwrap_or(0); let mem_stat = stats.memory_stats.usage.unwrap_or(0);
let mem_limit = stats.memory_stats.limit.unwrap_or(0); let mem_limit = stats.memory_stats.limit.unwrap_or(0);
let key = if let Some(networks) = &stats.networks { let some_key = if let Some(networks) = &stats.networks {
networks.keys().next().map(|x| x.to_owned()) networks.keys().next().map(|x| x.to_owned())
} else { } else {
None None
@@ -83,12 +83,14 @@ impl DockerData {
let cpu_stats = Self::calculate_usage(&stats); let cpu_stats = Self::calculate_usage(&stats);
let (rx, tx) = if let Some(k) = key { let no_bytes = (0, 0);
let ii = stats.networks.unwrap(); let (rx, tx) = if let Some(key) = some_key {
let v = ii.get(&k).unwrap(); match stats.networks.unwrap_or_default().get(&key) {
(v.rx_bytes.to_owned(), v.tx_bytes.to_owned()) Some(data) => (data.rx_bytes.to_owned(), data.tx_bytes.to_owned()),
None => no_bytes,
}
} else { } else {
(0, 0) no_bytes
}; };
if is_running { if is_running {
@@ -131,7 +133,7 @@ impl DockerData {
..Default::default() ..Default::default()
})) }))
.await .await
.unwrap(); .unwrap_or_default();
let mut output = vec![]; let mut output = vec![];
// iter over containers, to only send ones which have an id, as use ID for extensivley! // iter over containers, to only send ones which have an id, as use ID for extensivley!
@@ -143,11 +145,13 @@ impl DockerData {
self.app_data.lock().update_containers(&output); self.app_data.lock().update_containers(&output);
output output
.iter() .iter()
.map(|i| { .filter_map(|i| {
( i.id.as_ref().map(|id| {
i.state.as_ref().unwrap() == "running", (
i.id.as_ref().unwrap().to_owned(), i.state.as_ref().unwrap_or(&String::new()) == "running",
) id.to_owned(),
)
})
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
+17 -16
View File
@@ -99,11 +99,14 @@ impl InputHandler {
} }
}; };
let gui_state = Arc::clone(&self.gui_state); // If the info box sleep handle is currently being executed, as in m is pressed twice within a 4000ms window
// then cancel the first handle, as a new handle will be invoked
if self.info_sleep.is_some() { if let Some(info_sleep_timer) = self.info_sleep.as_ref() {
self.info_sleep.as_ref().unwrap().abort() info_sleep_timer.abort();
} }
let gui_state = Arc::clone(&self.gui_state);
// Show the info box - with "mouse capture enabled / disabled", for 4000 ms
self.info_sleep = Some(tokio::spawn(async move { self.info_sleep = Some(tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(4000)).await; tokio::time::sleep(std::time::Duration::from_millis(4000)).await;
gui_state.lock().reset_info_box() gui_state.lock().reset_info_box()
@@ -176,39 +179,37 @@ impl InputHandler {
// Does is matter though? // 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 option_command = self.app_data.lock().get_docker_command();
if command.is_some() { if let Some(command) = option_command {
let id = self.app_data.lock().get_selected_container_id(); let option_id = self.app_data.lock().get_selected_container_id();
if id.is_some() { if let Some(id) = option_id {
let id = id.unwrap(); match command {
match command.unwrap() {
// TODO handle theses errors?
DockerControls::Pause => self DockerControls::Pause => self
.docker_sender .docker_sender
.send(DockerMessage::Pause(id)) .send(DockerMessage::Pause(id))
.await .await
.unwrap(), .unwrap_or(()),
DockerControls::Unpause => self DockerControls::Unpause => self
.docker_sender .docker_sender
.send(DockerMessage::Unpause(id)) .send(DockerMessage::Unpause(id))
.await .await
.unwrap(), .unwrap_or(()),
DockerControls::Start => self DockerControls::Start => self
.docker_sender .docker_sender
.send(DockerMessage::Start(id)) .send(DockerMessage::Start(id))
.await .await
.unwrap(), .unwrap_or(()),
DockerControls::Stop => self DockerControls::Stop => self
.docker_sender .docker_sender
.send(DockerMessage::Stop(id)) .send(DockerMessage::Stop(id))
.await .await
.unwrap(), .unwrap_or(()),
DockerControls::Restart => self DockerControls::Restart => self
.docker_sender .docker_sender
.send(DockerMessage::Restart(id)) .send(DockerMessage::Restart(id))
.await .await
.unwrap(), .unwrap_or(()),
} }
} }
} }
+1 -1
View File
@@ -90,6 +90,6 @@ async fn main() {
update_duration, update_duration,
) )
.await .await
.unwrap(); .unwrap_or(())
} }
} }
+33 -28
View File
@@ -265,36 +265,40 @@ pub fn draw_chart<B: Backend>(
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(area); .split(area);
let (cpu, mem) = app_data.lock().containers.items[index].get_chart_data();
let cpu_dataset = vec![Dataset::default() // Check is some, else can cause out of bounds error, if containers get removed before a docker update
.marker(symbols::Marker::Dot) if let Some(data) = app_data.lock().containers.items.get(index) {
.style(Style::default().fg(Color::Magenta)) let (cpu, mem) = data.get_chart_data();
.graph_type(GraphType::Line)
.data(&cpu.0)];
let mem_dataset = vec![Dataset::default() let cpu_dataset = vec![Dataset::default()
.marker(symbols::Marker::Dot) .marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan)) .style(Style::default().fg(Color::Magenta))
.graph_type(GraphType::Line) .graph_type(GraphType::Line)
.data(&mem.0)]; .data(&cpu.0)];
let cpu_chart = make_chart(
cpu.2,
String::from("cpu"),
cpu_dataset,
CpuStats::new(cpu.0.last().unwrap_or(&(0.00, 0.00)).1),
cpu.1,
);
let mem_chart = make_chart(
mem.2,
String::from("memory"),
mem_dataset,
ByteStats::new(mem.0.last().unwrap_or(&(0.0, 0.0)).1 as u64),
mem.1,
);
f.render_widget(cpu_chart, area[0]); let mem_dataset = vec![Dataset::default()
f.render_widget(mem_chart, area[1]); .marker(symbols::Marker::Dot)
.style(Style::default().fg(Color::Cyan))
.graph_type(GraphType::Line)
.data(&mem.0)];
let cpu_chart = make_chart(
cpu.2,
String::from("cpu"),
cpu_dataset,
CpuStats::new(cpu.0.last().unwrap_or(&(0.00, 0.00)).1),
cpu.1,
);
let mem_chart = make_chart(
mem.2,
String::from("memory"),
mem_dataset,
ByteStats::new(mem.0.last().unwrap_or(&(0.0, 0.0)).1 as u64),
mem.1,
);
f.render_widget(cpu_chart, area[0]);
f.render_widget(mem_chart, area[1]);
}
} }
} }
@@ -460,7 +464,8 @@ pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
let description_text = format!("\n{}", DESCRIPTION); let description_text = format!("\n{}", DESCRIPTION);
let mut help_text = String::from("\n ( tab ) or ( alt+tab ) to change panels"); let mut help_text = String::from("\n ( tab ) or ( alt+tab ) to change panels");
help_text.push_str("\n ( ↑ ↓ ← → ) or ( j k ) to change selected line"); help_text
.push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line");
help_text.push_str("\n ( enter ) to send docker container commands"); help_text.push_str("\n ( enter ) to send docker container commands");
help_text.push_str("\n ( h ) to toggle this help information"); help_text.push_str("\n ( h ) to toggle this help information");
help_text.push_str( help_text.push_str(
+19 -18
View File
@@ -58,13 +58,13 @@ pub async fn create_ui(
) )
.await; .await;
disable_raw_mode().unwrap(); disable_raw_mode().unwrap_or(());
execute!( execute!(
terminal.backend_mut(), terminal.backend_mut(),
LeaveAlternateScreen, LeaveAlternateScreen,
DisableMouseCapture DisableMouseCapture
)?; )?;
terminal.show_cursor().unwrap(); terminal.show_cursor().unwrap_or(());
if let Err(err) = res { if let Err(err) = res {
println!("{}", err); println!("{}", err);
@@ -105,26 +105,27 @@ async fn run_app<B: Backend>(
let mut now = Instant::now(); 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_or_default() {
let event = event::read().unwrap(); if let Ok(event) = event::read() {
if let Event::Key(key) = event { if let Event::Key(key) = event {
sender sender
.send(InputMessages::ButtonPress(key.code)) .send(InputMessages::ButtonPress(key.code))
.await .await
.unwrap_or(()); .unwrap_or(());
} else if let Event::Mouse(m) = event { } else if let Event::Mouse(m) = event {
sender sender
.send(InputMessages::MouseEvent(m)) .send(InputMessages::MouseEvent(m))
.await .await
.unwrap_or(()); .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 { if now.elapsed() >= update_duration {
docker_sx.send(DockerMessage::Update).await.unwrap(); docker_sx.send(DockerMessage::Update).await.unwrap_or(());
now = Instant::now(); now = Instant::now();
} }