diff --git a/.github/release-body.md b/.github/release-body.md index e92a330..dae66a3 100644 --- a/.github/release-body.md +++ b/.github/release-body.md @@ -1,20 +1,22 @@ -### 2023-01-21 +### 2023-01-29 ### Chores -+ dependencies updated, [8cd199db49186fad6ce432bb277e3a10f0a08d34], [d880b829c123dbe57deccadef97810e45c083737], [66d57c99558ca14d9593d6dbfd5b0e8e5d59055d], [33f9374908942f4a3b90be227fad94ca353cf351], [007d5d83d7f1b93e1e78777a4417b2740db706bd] -+ create_release.sh typos, [9a27d46a044452080144ee1367dc95886b10abf8] -+ dev container post create install cross, [2d253f034182741d434e4bac12317f24221d0d4a] ++ dependencies updated, [c129f474fe2976454b1868d00e8d7d99b87ec23b], [9788b8afd98e59b1d4412a8adc54b34d2c5671fd], [2ab88eb26e9bbbc4dad4651256d8d9b044ea3272] -### Features -**all potentially considered breaking changes** -+ store Logs in own struct, use a hashset to track timestamps, hopefully closes #11, [657ea2d751a71f05b17547b47c492d5676817336] -+ Spawn docker commands into own thread, can now execute multiple docker commands at the same time, [9ec43e124a62a80f4e78acba85fc3af5980ce260] -+ align memory columns correctly, minimum byte display value now `0.00 kB`, rather than `0 B`, closes #20, [bd7dfcd2c512a527d66a1388f90006988a487186], [51c580010a24de2427373795803936d498dc8cee] +### Docs ++ comment typo, [1025579138f11e4987263c7bfe936c4c8542f8b3] + +### Fixes ++ deadlock on draw logs when no containers found, [68e444bfc393eb46bac2b99eb57697bb9b0451af] ++ github workflow release on main only (with semver tag), [e4ca41dfd8ec3acae202a2d2464b8e18f5c5bdd5], [749ec712f07cff2c941aed6726c56bdbd5cb8d2c] ### Refactors -+ main.rs tidy up, [97b89349dc2de275ca514a1e6420255a63d775e8] -+ derive Default for GuiState, [9dcd0509efeb464f58fb53d813bd78de2447949d] -+ param reduction, AtomicBool to Relaxed, [0350293de3c00c6e5e5d787b7596bb3413d1cda1] ++ major refactor of internal data handling, [b4488e4bdb0252f5c5680cee6a46427f22a282ab] ++ needless (double) referencing removed, [a174dafe1b05908735680a874dc551a86da24777] ++ app_data methods re-ordered & renamed, [c0bb5355d6a5d352260655110ce3d5ab695acda9] + +### Reverts ++ is_running AtomicBool back to SeqCst, [c4d80061dab94afd08d4d793dc147f878c965ad6] see CHANGELOG.md for more details diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml index 636ce0b..253058a 100644 --- a/.github/workflows/create_release_and_build.yml +++ b/.github/workflows/create_release_and_build.yml @@ -1,8 +1,10 @@ name: Release CI on: push: + branch: + - 'main' tags: - - 'v*.*.*' + - 'v[0-9]+.[0-9]+.[0-9]+' jobs: deploy: runs-on: ubuntu-18.04 diff --git a/CHANGELOG.md b/CHANGELOG.md index f7db59b..5c5bd32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# v0.2.1 +### 2023-01-29 + +### Chores ++ dependencies updated, [c129f474](https://github.com/mrjackwills/oxker/commit/c129f474fe2976454b1868d00e8d7d99b87ec23b), [9788b8af](https://github.com/mrjackwills/oxker/commit/9788b8afd98e59b1d4412a8adc54b34d2c5671fd), [2ab88eb2](https://github.com/mrjackwills/oxker/commit/2ab88eb26e9bbbc4dad4651256d8d9b044ea3272) + +### Docs ++ comment typo, [10255791](https://github.com/mrjackwills/oxker/commit/1025579138f11e4987263c7bfe936c4c8542f8b3) + +### Fixes ++ deadlock on draw logs when no containers found, [68e444bf](https://github.com/mrjackwills/oxker/commit/68e444bfc393eb46bac2b99eb57697bb9b0451af) ++ github workflow release on main only (with semver tag), [e4ca41df](https://github.com/mrjackwills/oxker/commit/e4ca41dfd8ec3acae202a2d2464b8e18f5c5bdd5), [749ec712](https://github.com/mrjackwills/oxker/commit/749ec712f07cff2c941aed6726c56bdbd5cb8d2c) + +### Refactors ++ major refactor of internal data handling, [b4488e4b](https://github.com/mrjackwills/oxker/commit/b4488e4bdb0252f5c5680cee6a46427f22a282ab) ++ needless (double) referencing removed, [a174dafe](https://github.com/mrjackwills/oxker/commit/a174dafe1b05908735680a874dc551a86da24777) ++ app_data methods re-ordered & renamed, [c0bb5355](https://github.com/mrjackwills/oxker/commit/c0bb5355d6a5d352260655110ce3d5ab695acda9) + +### Reverts ++ is_running AtomicBool back to SeqCst, [c4d80061](https://github.com/mrjackwills/oxker/commit/c4d80061dab94afd08d4d793dc147f878c965ad6) + # v0.2.0 ### 2023-01-21 diff --git a/Cargo.lock b/Cargo.lock index 94fe363..e6dc54d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,9 +106,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.1" +version = "4.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec7a4128863c188deefe750ac1d1dfe66c236909f845af04beed823638dc1b2" +checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" dependencies = [ "bitflags", "clap_derive", @@ -200,6 +200,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f67c7faacd4db07a939f55d66a983a5355358a1f17d32cc9a8d01d1266b9ce" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.0" @@ -211,9 +227,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b61a7545f753a88bcbe0a70de1fcc0221e10bfc752f576754fa91e663db1622e" +checksum = "322296e2f2e5af4270b54df9e85a02ff037e271af20ba3e7fe1575515dc840b8" dependencies = [ "cc", "cxxbridge-flags", @@ -223,9 +239,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f464457d494b5ed6905c63b0c4704842aba319084a0a3561cdc1359536b53200" +checksum = "017a1385b05d631e7875b1f151c9f012d37b53491e2a87f65bff5c262b2111d8" dependencies = [ "cc", "codespan-reporting", @@ -238,15 +254,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c7119ce3a3701ed81aca8410b9acf6fc399d2629d057b87e2efa4e63a3aaea" +checksum = "c26bbb078acf09bc1ecda02d4223f03bdd28bd4874edcb0379138efc499ce971" [[package]] name = "cxxbridge-macro" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e07508b90551e610910fa648a1878991d367064997a596135b86df30daf07e" +checksum = "357f40d1f06a24b60ae1fe122542c1fb05d28d32acb2aed064e84bc2ad1e252e" dependencies = [ "proc-macro2", "quote", @@ -674,13 +690,13 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "bollard", "cansi", "clap", - "crossterm", + "crossterm 0.26.0", "futures-util", "parking_lot", "tokio", @@ -1223,7 +1239,7 @@ checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" dependencies = [ "bitflags", "cassowary", - "crossterm", + "crossterm 0.25.0", "unicode-segmentation", "unicode-width", ] diff --git a/Cargo.toml b/Cargo.toml index f01d9ef..2b3cc38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxker" -version = "0.2.0" +version = "0.2.1" edition = "2021" authors = ["Jack Wills "] description = "A simple tui to view & control docker containers" @@ -16,7 +16,7 @@ anyhow = "1.0" bollard = "0.14" cansi = "2.2" clap={version="4.1", features = ["derive", "unicode", "color"] } -crossterm = "0.25" +crossterm = "0.26" futures-util = "0.3" parking_lot = {version= "0.12"} tokio = {version = "1.24", features=["full"]} diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index fc106f4..0837c24 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -1,7 +1,7 @@ use bollard::models::ContainerSummary; use core::fmt; use std::time::{SystemTime, UNIX_EPOCH}; -use tui::widgets::ListItem; +use tui::widgets::{ListItem, ListState}; mod container_state; @@ -11,10 +11,10 @@ pub use container_state::*; /// Global app_state, stored in an Arc #[derive(Debug, Clone)] pub struct AppData { + containers: StatefulList, error: Option, sorted_by: Option<(Header, SortedOrder)>, pub args: CliArgs, - pub containers: StatefulList, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -55,39 +55,6 @@ impl fmt::Display for Header { } impl AppData { - /// Generate a default app_state - pub fn default(args: CliArgs) -> Self { - Self { - args, - containers: StatefulList::new(vec![]), - error: None, - sorted_by: None, - } - } - - pub const fn get_sorted(&self) -> Option<(Header, SortedOrder)> { - self.sorted_by - } - - /// Remove the sorted header & order, and sort by default - created datetime - pub fn reset_sorted(&mut self) { - self.set_sorted(None); - } - - /// Sort containers based on a given header, if headings match, and already ascending, remove sorting - pub fn set_sort_by_header(&mut self, selected_header: Header) { - let mut output = Some((selected_header, SortedOrder::Asc)); - if let Some((current_header, order)) = self.get_sorted() { - if current_header == selected_header { - match order { - SortedOrder::Desc => output = None, - SortedOrder::Asc => output = Some((selected_header, SortedOrder::Desc)), - } - } - } - self.set_sorted(output); - } - /// Change the sorted order, also set the selected container state to match new order fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) { self.sorted_by = x; @@ -109,96 +76,39 @@ impl AppData { .as_secs() } - /// Get the current select docker command - /// So know which command to execute - pub fn get_docker_command(&self) -> Option { - let mut output = None; - if let Some(index) = self.containers.state.selected() { - if let Some(control_index) = self.containers.items[index] - .docker_controls - .state - .selected() - { - output = Some(self.containers.items[index].docker_controls.items[control_index]); - } - } - output - } - - /// Change selected choice of docker commands of selected container - pub fn docker_command_next(&mut self) { - if let Some(index) = self.containers.state.selected() { - if let Some(i) = self.containers.items.get_mut(index) { - i.docker_controls.next(); - } + /// Generate a default app_state + pub fn default(args: CliArgs) -> Self { + Self { + args, + containers: StatefulList::new(vec![]), + error: None, + sorted_by: None, } } - /// Change selected choice of docker commands of selected container - pub fn docker_command_previous(&mut self) { - if let Some(index) = self.containers.state.selected() { - if let Some(i) = self.containers.items.get_mut(index) { - i.docker_controls.previous(); + /// Container sort related methods + + /// Remove the sorted header & order, and sort by default - created datetime + pub fn reset_sorted(&mut self) { + self.set_sorted(None); + } + + /// Sort containers based on a given header, if headings match, and already ascending, remove sorting + pub fn set_sort_by_header(&mut self, selected_header: Header) { + let mut output = Some((selected_header, SortedOrder::Asc)); + if let Some((current_header, order)) = self.get_sorted() { + if current_header == selected_header { + match order { + SortedOrder::Desc => output = None, + SortedOrder::Asc => output = Some((selected_header, SortedOrder::Desc)), + } } } + self.set_sorted(output); } - /// Change selected choice of docker commands of selected container - pub fn docker_command_start(&mut self) { - if let Some(index) = self.containers.state.selected() { - if let Some(i) = self.containers.items.get_mut(index) { - i.docker_controls.start(); - } - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_command_end(&mut self) { - if let Some(index) = self.containers.state.selected() { - if let Some(i) = self.containers.items.get_mut(index) { - i.docker_controls.end(); - } - } - } - - /// return single app_state error - pub const fn get_error(&self) -> Option { - self.error - } - - /// remove single app_state error - pub fn remove_error(&mut self) { - self.error = None; - } - - /// insert single app_state error - pub fn set_error(&mut self, error: AppError) { - self.error = Some(error); - } - - /// Find the id of the currently selected container. - /// If any containers on system, will always return a ContainerId - /// Only returns None when no containers found. - pub fn get_selected_container_id(&self) -> Option { - let mut output = None; - if let Some(index) = self.containers.state.selected() { - if let Some(x) = self.containers.items.get(index) { - output = Some(x.id.clone()); - } - } - output - } - - /// Check if the selected container is a dockerised version of oxker - /// So that can disallow commands to be send - /// Is a shabby way of implementing this - pub fn selected_container_is_oxker(&self) -> bool { - if let Some(index) = self.containers.state.selected() { - if let Some(x) = self.containers.items.get(index) { - return x.is_oxker; - } - } - false + pub const fn get_sorted(&self) -> Option<(Header, SortedOrder)> { + self.sorted_by } /// Sort the containers vec, based on a heading, either ascending or descending, @@ -276,71 +186,220 @@ impl AppData { } } - /// Find the index of the currently selected single log line - pub fn get_selected_log_index(&self) -> Option { - let mut output = None; - if let Some(id) = self.get_selected_container_id() { - if let Some(index) = self.containers.items.iter().position(|i| i.id == id) { - output = Some(index); - } - } - output + /// Container state methods + + /// Just get the total number of containers + pub fn get_container_len(&self) -> usize { + self.containers.items.len() } - /// Get the title for log panel for selected container - /// will be either + /// Get title for containers section + pub fn container_title(&self) -> String { + self.containers.get_state_title() + } + + /// Select the first container + pub fn containers_start(&mut self) { + self.containers.start(); + } + + // select the last container + pub fn containers_end(&mut self) { + self.containers.end(); + } + + /// Select the next container + pub fn containers_next(&mut self) { + self.containers.next(); + } + + // select the previous container + pub fn containers_previous(&mut self) { + self.containers.previous(); + } + + /// Get Container items + pub const fn get_container_items(&self) -> &Vec { + &self.containers.items + } + + /// Get Option of the current selected container + pub fn get_selected_container(&self) -> Option<&ContainerItem> { + self.containers + .state + .selected() + .and_then(|i| self.containers.items.get(i)) + } + + /// Get mutable Option of the current selected container + fn get_mut_selected_container(&mut self) -> Option<&mut ContainerItem> { + self.containers + .state + .selected() + .and_then(|i| self.containers.items.get_mut(i)) + } + + /// Get ListState of containers + pub fn get_container_state(&mut self) -> &mut ListState { + &mut self.containers.state + } + + /// Selected DockerCommand methods + + /// Get the current selected docker command + /// So know which command to execute + pub fn selected_docker_command(&self) -> Option { + self.get_selected_container().and_then(|i| { + i.docker_controls.state.selected().and_then(|x| { + i.docker_controls + .items + .get(x) + .map(std::borrow::ToOwned::to_owned) + }) + }) + } + /// Get mutable Option of the currently selected container DockerControls state + pub fn get_control_state(&mut self) -> Option<&mut ListState> { + self.get_mut_selected_container() + .map(|i| &mut i.docker_controls.state) + } + + /// Get mutable Option of the currently selected container DockerControls items + pub fn get_control_items(&mut self) -> Option<&mut Vec> { + self.get_mut_selected_container() + .map(|i| &mut i.docker_controls.items) + } + + /// Change selected choice of docker commands of selected container + pub fn docker_command_next(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.next(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_command_previous(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.previous(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_command_start(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.start(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_command_end(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.end(); + } + } + + /// Logs related methods + + /// Get the title for log panel for selected container, will be either /// 1) "logs x/x - container_name" where container_name is 32 chars max /// 2) "logs - container_name" when no logs found, again 32 chars max + /// 3) "" no container currently selected - aka no containers on system pub fn get_log_title(&self) -> String { - self.get_selected_log_index() - .map_or(String::new(), |index| { - let logs_len = self.containers.items[index].logs.get_state_title(); - let mut name = self.containers.items[index].name.clone(); - name.truncate(32); - if logs_len.is_empty() { - format!("- {name} ") - } else { - format!("{logs_len} - {name}") - } - }) + self.get_selected_container().map_or_else(String::new, |y| { + let logs_len = y.logs.get_state_title(); + let mut name = y.name.clone(); + name.truncate(32); + if logs_len.is_empty() { + format!("- {name} ") + } else { + format!("{logs_len} - {name}") + } + }) } /// select next selected log line pub fn log_next(&mut self) { - if let Some(index) = self.get_selected_log_index() { - if let Some(i) = self.containers.items.get_mut(index) { - i.logs.next(); - } + if let Some(i) = self.get_mut_selected_container() { + i.logs.next(); } } /// select previous selected log line pub fn log_previous(&mut self) { - if let Some(index) = self.get_selected_log_index() { - if let Some(i) = self.containers.items.get_mut(index) { - i.logs.previous(); - } + if let Some(i) = self.get_mut_selected_container() { + i.logs.previous(); } } /// select last selected log line pub fn log_end(&mut self) { - if let Some(index) = self.get_selected_log_index() { - if let Some(i) = self.containers.items.get_mut(index) { - i.logs.end(); - } + if let Some(i) = self.get_mut_selected_container() { + i.logs.end(); } } /// select first selected log line pub fn log_start(&mut self) { - if let Some(index) = self.get_selected_log_index() { - if let Some(i) = self.containers.items.get_mut(index) { - i.logs.start(); - } + if let Some(i) = self.get_mut_selected_container() { + i.logs.start(); } } + /// Chart data related methods + + /// Get mutable Option of the currently selected container chart data + pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> { + self.containers + .state + .selected() + .and_then(|i| self.containers.items.get_mut(i)) + .map(|i| i.get_chart_data()) + } + + /// Logs related methods + + /// Get mutable Vec of current containers logs + pub fn get_logs(&mut self) -> Vec> { + self.containers + .state + .selected() + .and_then(|i| self.containers.items.get_mut(i)) + .map_or(vec![], |i| i.logs.to_vec()) + } + + /// Get mutable Option of the currently selected container Logs state + pub fn get_log_state(&mut self) -> Option<&mut ListState> { + self.containers + .state + .selected() + .and_then(|i| self.containers.items.get_mut(i)) + .map(|i| i.logs.state()) + } + + /// Error realted methods + + /// return single app_state error + pub const fn get_error(&self) -> Option { + self.error + } + + /// remove single app_state error + pub fn remove_error(&mut self) { + self.error = None; + } + + /// insert single app_state error + pub fn set_error(&mut self, error: AppError) { + self.error = Some(error); + } + + /// Check if the selected container is a dockerised version of oxker + /// So that can disallow commands to be send + /// Is a shabby way of implementing this + pub fn is_oxker(&self) -> bool { + self.get_selected_container().map_or(false, |i| i.is_oxker) + } + /// Check if the initial parsing has been completed, by making sure that all ids given (which are running) have a non empty cpu_stats vecdec pub fn initialised(&mut self, all_ids: &[(bool, ContainerId)]) -> bool { let count_is_running = all_ids.iter().filter(|i| i.0).count(); @@ -353,15 +412,10 @@ impl AppData { count_is_running == number_with_cpu_status } - /// Just get the total number of containers - pub fn get_container_len(&self) -> usize { - self.containers.items.len() - } - /// Find the widths for the strings in the containers panel. /// So can display nicely and evenly pub fn get_width(&self) -> Columns { - let mut output = Columns::new(); + let mut columns = Columns::new(); let count = |x: &String| u8::try_from(x.chars().count()).unwrap_or(12); // Should probably find a refactor here somewhere @@ -389,51 +443,51 @@ impl AppData { ); let mem_limit_count = count(&container.mem_limit.to_string()); - if cpu_count > output.cpu.1 { - output.cpu.1 = cpu_count; + if cpu_count > columns.cpu.1 { + columns.cpu.1 = cpu_count; }; - if image_count > output.image.1 { - output.image.1 = image_count; + if image_count > columns.image.1 { + columns.image.1 = image_count; }; - if mem_current_count > output.mem.1 { - output.mem.1 = mem_current_count; + if mem_current_count > columns.mem.1 { + columns.mem.1 = mem_current_count; }; - if mem_limit_count > output.mem.2 { - output.mem.2 = mem_limit_count; + if mem_limit_count > columns.mem.2 { + columns.mem.2 = mem_limit_count; }; - if name_count > output.name.1 { - output.name.1 = name_count; + if name_count > columns.name.1 { + columns.name.1 = name_count; }; - if state_count > output.state.1 { - output.state.1 = state_count; + if state_count > columns.state.1 { + columns.state.1 = state_count; }; - if status_count > output.status.1 { - output.status.1 = status_count; + if status_count > columns.status.1 { + columns.status.1 = status_count; }; - if rx_count > output.net_rx.1 { - output.net_rx.1 = rx_count; + if rx_count > columns.net_rx.1 { + columns.net_rx.1 = rx_count; }; - if tx_count > output.net_tx.1 { - output.net_tx.1 = tx_count; + if tx_count > columns.net_tx.1 { + columns.net_tx.1 = tx_count; }; } - output + columns } - /// Get all containers ids - pub fn get_all_ids(&self) -> Vec { - self.containers - .items - .iter() - .map(|i| i.id.clone()) - .collect::>() - } + /// Update related methods /// return a mutable container by given id fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { self.containers.items.iter_mut().find(|i| &i.id == id) } + /// Find the id of the currently selected container. + /// If any containers on system, will always return a ContainerId + /// Only returns None when no containers found. + pub fn get_selected_container_id(&self) -> Option { + self.get_selected_container().map(|i| i.id.clone()) + } + /// Update container mem, cpu, & network stats, in single function so only need to call .lock() once /// Will also, if a sort is set, sort the containers pub fn update_stats( @@ -465,14 +519,19 @@ impl AppData { container.mem_limit.update(mem_limit); } // need to benchmark this? - if self.get_sorted().is_some() { - self.sort_containers(); - } + // if self.get_sorted().is_some() { + self.sort_containers(); + // } } /// Update, or insert, containers pub fn update_containers(&mut self, all_containers: &mut [ContainerSummary]) { - let all_ids = self.get_all_ids(); + let all_ids = self + .containers + .items + .iter() + .map(|i| i.id.clone()) + .collect::>(); // Only sort it no containers currently set, as afterwards the order is fixed if self.containers.items.is_empty() { @@ -563,7 +622,7 @@ impl AppData { } /// update logs of a given container, based on id - pub fn update_log_by_id(&mut self, output: Vec, id: &ContainerId) { + pub fn update_log_by_id(&mut self, logs: Vec, id: &ContainerId) { let color = self.args.color; let raw = self.args.raw; @@ -573,7 +632,7 @@ impl AppData { container.last_updated = Self::get_systemtime(); let current_len = container.logs.len(); - for mut i in output { + for mut i in logs { let tz = LogsTz::from(&i); // Strip the timestamp if `-t` flag set if !timestamp { diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index c700654..43a2124 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -267,25 +267,18 @@ impl DockerData { /// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed) async fn update_everything(&mut self) { let all_ids = self.update_all_containers().await; - let optional_index = self.app_data.lock().get_selected_log_index(); - if let Some(index) = optional_index { - if let Some(container) = self.app_data.lock().containers.items.get(index) { - self.spawns - .lock() - .entry(SpawnId::Log(container.id.clone())) - .or_insert_with(|| { - let docker = Arc::clone(&self.docker); - let app_data = Arc::clone(&self.app_data); - let spawns = Arc::clone(&self.spawns); - tokio::spawn(Self::update_log( - app_data, - docker, - container.id.clone(), - container.last_updated, - spawns, - )) - }); - } + if let Some(container) = self.app_data.lock().get_selected_container() { + let id = container.id.clone(); + let last_updated = container.last_updated; + self.spawns + .lock() + .entry(SpawnId::Log(id.clone())) + .or_insert_with(|| { + let docker = Arc::clone(&self.docker); + let app_data = Arc::clone(&self.app_data); + let spawns = Arc::clone(&self.spawns); + tokio::spawn(Self::update_log(app_data, docker, id, last_updated, spawns)) + }); }; self.update_all_container_stats(&all_ids); self.app_data.lock().sort_containers(); @@ -293,7 +286,7 @@ impl DockerData { /// Animate the loading icon async fn loading_spin(loading_uuid: Uuid, gui_state: &Arc>) -> JoinHandle<()> { - let gui_state = Arc::clone(&gui_state); + let gui_state = Arc::clone(gui_state); tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_millis(100)).await; @@ -412,7 +405,7 @@ impl DockerData { .values() .into_iter() .for_each(tokio::task::JoinHandle::abort); - self.is_running.store(false, Ordering::Relaxed); + self.is_running.store(false, Ordering::SeqCst); } } } diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 9d88df9..7e1f649 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -73,7 +73,7 @@ impl InputHandler { } } } - if !self.is_running.load(Ordering::Relaxed) { + if !self.is_running.load(Ordering::SeqCst) { break; } } @@ -134,7 +134,7 @@ impl InputHandler { .lock() .status_contains(&[Status::Error, Status::Init]); if error_init || self.docker_sender.send(DockerMessage::Quit).await.is_err() { - self.is_running.store(false, Ordering::Relaxed); + self.is_running.store(false, Ordering::SeqCst); } } @@ -207,7 +207,7 @@ impl InputHandler { KeyCode::Home => { let mut locked_data = self.app_data.lock(); match self.gui_state.lock().selected_panel { - SelectablePanel::Containers => locked_data.containers.start(), + SelectablePanel::Containers => locked_data.containers_start(), SelectablePanel::Logs => locked_data.log_start(), SelectablePanel::Commands => locked_data.docker_command_start(), } @@ -215,7 +215,7 @@ impl InputHandler { KeyCode::End => { let mut locked_data = self.app_data.lock(); match self.gui_state.lock().selected_panel { - SelectablePanel::Containers => locked_data.containers.end(), + SelectablePanel::Containers => locked_data.containers_end(), SelectablePanel::Logs => locked_data.log_end(), SelectablePanel::Commands => locked_data.docker_command_end(), } @@ -236,12 +236,12 @@ impl InputHandler { // This isn't great, just means you can't send docker commands before full initialization of the program let panel = self.gui_state.lock().selected_panel; if panel == SelectablePanel::Commands { - let option_command = self.app_data.lock().get_docker_command(); + let option_command = self.app_data.lock().selected_docker_command(); if let Some(command) = option_command { let option_id = self.app_data.lock().get_selected_container_id(); // Poor way of disallowing commands to be sent to a containerised okxer - if self.app_data.lock().selected_container_is_oxker() { + if self.app_data.lock().is_oxker() { return; }; if let Some(id) = option_id { @@ -287,14 +287,12 @@ impl InputHandler { MouseEventKind::ScrollUp => self.previous(), MouseEventKind::ScrollDown => self.next(), MouseEventKind::Down(MouseButton::Left) => { - let header_intersects = self.gui_state.lock().header_intersect(Rect::new( + if let Some(header) = self.gui_state.lock().header_intersect(Rect::new( mouse_event.column, mouse_event.row, 1, 1, - )); - - if let Some(header) = header_intersects { + )) { self.sort(header); } @@ -313,7 +311,7 @@ impl InputHandler { fn next(&mut self) { let mut locked_data = self.app_data.lock(); match self.gui_state.lock().selected_panel { - SelectablePanel::Containers => locked_data.containers.next(), + SelectablePanel::Containers => locked_data.containers_next(), SelectablePanel::Logs => locked_data.log_next(), SelectablePanel::Commands => locked_data.docker_command_next(), }; @@ -323,7 +321,7 @@ impl InputHandler { fn previous(&mut self) { let mut locked_data = self.app_data.lock(); match self.gui_state.lock().selected_panel { - SelectablePanel::Containers => locked_data.containers.previous(), + SelectablePanel::Containers => locked_data.containers_previous(), SelectablePanel::Logs => locked_data.log_previous(), SelectablePanel::Commands => locked_data.docker_command_previous(), } diff --git a/src/main.rs b/src/main.rs index 863ad56..9e3fdfe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] #![warn(clippy::unused_async, clippy::unwrap_used, clippy::expect_used)] -// Wanring - These are indeed pedantic +// Warning - These are indeed pedantic #![warn(clippy::pedantic)] #![warn(clippy::nursery)] #![allow( @@ -49,9 +49,9 @@ async fn docker_init( ) { if let Ok(docker) = Docker::connect_with_socket_defaults() { if docker.ping().await.is_ok() { - let app_data = Arc::clone(&app_data); - let gui_state = Arc::clone(&gui_state); - let is_running = Arc::clone(&is_running); + let app_data = Arc::clone(app_data); + let gui_state = Arc::clone(gui_state); + let is_running = Arc::clone(is_running); tokio::spawn(DockerData::init( app_data, docker, docker_rx, gui_state, is_running, )); @@ -66,16 +66,16 @@ async fn docker_init( } /// Create data for, and then spawn a tokio thread, for the input handler -async fn handler_init( +fn handler_init( app_data: &Arc>, docker_sx: &Sender, gui_state: &Arc>, input_rx: Receiver, is_running: &Arc, ) { - let input_app_data = Arc::clone(&app_data); - let input_gui_state = Arc::clone(&gui_state); - let input_is_running = Arc::clone(&is_running); + let input_app_data = Arc::clone(app_data); + let input_gui_state = Arc::clone(gui_state); + let input_is_running = Arc::clone(is_running); tokio::spawn(input_handler::InputHandler::init( input_app_data, input_rx, @@ -97,7 +97,7 @@ async fn main() { docker_init(&app_data, docker_rx, &gui_state, &is_running).await; - handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running).await; + handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running); if args.gui { create_ui(app_data, docker_sx, gui_state, is_running, input_sx) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 6f48b2e..9e1a0fe 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -57,11 +57,7 @@ fn generate_block<'a>( let current_selected_panel = gui_state.lock().selected_panel; let mut title = match panel { SelectablePanel::Containers => { - format!( - "{} {}", - panel.title(), - app_data.lock().containers.get_state_title() - ) + format!("{} {}", panel.title(), app_data.lock().container_title()) } SelectablePanel::Logs => { format!("{} {}", panel.title(), app_data.lock().get_log_title()) @@ -87,35 +83,31 @@ pub fn commands( area: Rect, f: &mut Frame<'_, B>, gui_state: &Arc>, - index: Option, ) { - let block = generate_block(app_data, area, gui_state, SelectablePanel::Commands); - if let Some(i) = index { - let items = app_data.lock().containers.items[i] - .docker_controls - .items - .iter() - .map(|i| { + let block = || generate_block(app_data, area, gui_state, SelectablePanel::Commands); + let items = app_data.lock().get_control_items().map_or(vec![], |i| { + i.iter() + .map(|c| { let lines = Spans::from(vec![Span::styled( - i.to_string(), - Style::default().fg(i.get_color()), + c.to_string(), + Style::default().fg(c.get_color()), )]); ListItem::new(lines) }) - .collect::>(); + .collect::>() + }); - let items = List::new(items) - .block(block) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)) - .highlight_symbol(ARROW); + let items = List::new(items) + .block(block()) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol(ARROW); - f.render_stateful_widget( - items, - area, - &mut app_data.lock().containers.items[i].docker_controls.state, - ); + if let Some(i) = app_data.lock().get_control_state() { + f.render_stateful_widget(items, area, i); } else { - let paragraph = Paragraph::new("").block(block).alignment(Alignment::Center); + let paragraph = Paragraph::new("") + .block(block()) + .alignment(Alignment::Center); f.render_widget(paragraph, area); } } @@ -132,8 +124,7 @@ pub fn containers( let items = app_data .lock() - .containers - .items + .get_container_items() .iter() .map(|i| { let state_style = Style::default().fg(i.state.get_color()); @@ -204,6 +195,7 @@ pub fn containers( ListItem::new(lines) }) .collect::>(); + if items.is_empty() { let paragraph = Paragraph::new("no containers running") .block(block) @@ -215,7 +207,7 @@ pub fn containers( .highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_symbol(CIRCLE); - f.render_stateful_widget(items, area, &mut app_data.lock().containers.state); + f.render_stateful_widget(items, area, app_data.lock().get_container_state()); } } @@ -225,69 +217,63 @@ pub fn logs( area: Rect, f: &mut Frame<'_, B>, gui_state: &Arc>, - index: Option, loading_icon: &str, ) { - let block = generate_block(app_data, area, gui_state, SelectablePanel::Logs); - let contains_init = gui_state.lock().status_contains(&[Status::Init]); - if contains_init { + let block = || generate_block(app_data, area, gui_state, SelectablePanel::Logs); + if gui_state.lock().status_contains(&[Status::Init]) { let paragraph = Paragraph::new(format!("parsing logs {loading_icon}")) .style(Style::default()) - .block(block) + .block(block()) .alignment(Alignment::Center); f.render_widget(paragraph, area); - } else if let Some(index) = index { - let items = List::new(app_data.lock().containers.items[index].logs.to_vec()) - .block(block) - .highlight_symbol(ARROW) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)); - f.render_stateful_widget( - items, - area, - app_data.lock().containers.items[index].logs.state(), - ); } else { - let paragraph = Paragraph::new("no logs found") - .block(block) - .alignment(Alignment::Center); - f.render_widget(paragraph, area); + let logs = app_data.lock().get_logs(); + + if logs.is_empty() { + let paragraph = Paragraph::new("no logs found") + .block(block()) + .alignment(Alignment::Center); + f.render_widget(paragraph, area); + } else { + let items = List::new(logs) + .block(block()) + .highlight_symbol(ARROW) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)); + + // This should always return Some, as logs is not empty + if let Some(i) = app_data.lock().get_log_state() { + f.render_stateful_widget(items, area, i); + } + } } } /// Draw the cpu + mem charts -pub fn chart( - f: &mut Frame<'_, B>, - area: Rect, - app_data: &Arc>, - index: Option, -) { - if let Some(index) = index { +pub fn chart(f: &mut Frame<'_, B>, area: Rect, app_data: &Arc>) { + if let Some((cpu, mem)) = app_data.lock().get_chart_data() { let area = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(area); - // Check is some, else can cause out of bounds error, if containers get removed before a docker update - if let Some(data) = app_data.lock().containers.items.get(index) { - let (cpu, mem) = data.get_chart_data(); - let cpu_dataset = vec![Dataset::default() - .marker(symbols::Marker::Dot) - .style(Style::default().fg(Color::Magenta)) - .graph_type(GraphType::Line) - .data(&cpu.0)]; - let mem_dataset = vec![Dataset::default() - .marker(symbols::Marker::Dot) - .style(Style::default().fg(Color::Cyan)) - .graph_type(GraphType::Line) - .data(&mem.0)]; - let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1)); - let mem_stats = ByteStats::new(mem.0.last().map_or(0, |f| f.1 as u64)); - let cpu_chart = make_chart(cpu.2, "cpu", cpu_dataset, &cpu_stats, &cpu.1); - let mem_chart = make_chart(mem.2, "memory", mem_dataset, &mem_stats, &mem.1); + let cpu_dataset = vec![Dataset::default() + .marker(symbols::Marker::Dot) + .style(Style::default().fg(Color::Magenta)) + .graph_type(GraphType::Line) + .data(&cpu.0)]; + let mem_dataset = vec![Dataset::default() + .marker(symbols::Marker::Dot) + .style(Style::default().fg(Color::Cyan)) + .graph_type(GraphType::Line) + .data(&mem.0)]; - f.render_widget(cpu_chart, area[0]); - f.render_widget(mem_chart, area[1]); - } + let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1)); + let mem_stats = ByteStats::new(mem.0.last().map_or(0, |f| f.1 as u64)); + let cpu_chart = make_chart(cpu.2, "cpu", cpu_dataset, &cpu_stats, &cpu.1); + let mem_chart = make_chart(mem.2, "memory", mem_dataset, &mem_stats, &mem.1); + + f.render_widget(cpu_chart, area[0]); + f.render_widget(mem_chart, area[1]); } } @@ -490,8 +476,6 @@ pub fn heading_bar( // If no containers, don't display the headers, could maybe do this first? let help_index = if has_containers { 2 } else { 0 }; - // render help info - f.render_widget(help_paragraph, split_bar[help_index]); } diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 1c197ef..923c15a 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -242,6 +242,7 @@ impl GuiState { } /// Check if the current gui_status contains any of the given status' + /// Don't really like this methodology for gui state, needs a re-think pub fn status_contains(&self, status: &[Status]) -> bool { status.iter().any(|i| self.status.contains(i)) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 43ff005..a7006a1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -60,7 +60,7 @@ pub async fn create_ui( terminal.show_cursor()?; if let Err(err) = res { - println!("{err}"); + println!("error: {err}"); } std::io::stdout().flush().unwrap_or(()); Ok(()) @@ -98,7 +98,7 @@ async fn run_app( } } } else { - while is_running.load(Ordering::Relaxed) { + while is_running.load(Ordering::SeqCst) { if crossterm::event::poll(input_poll_rate).unwrap_or(false) { if let Ok(event) = event::read() { if let Event::Key(key) = event { @@ -141,9 +141,8 @@ fn ui( let height = if height < 12 { height + 4 } else { 12 }; let column_widths = app_data.lock().get_width(); - let has_containers = !app_data.lock().containers.items.is_empty(); + let has_containers = app_data.lock().get_container_len() > 0; let has_error = app_data.lock().get_error(); - let log_index = app_data.lock().get_selected_log_index(); let sorted_by = app_data.lock().get_sorted(); let show_help = gui_state.lock().status_contains(&[Status::Help]); @@ -193,17 +192,10 @@ fn ui( draw_blocks::containers(app_data, top_panel[0], f, gui_state, &column_widths); if has_containers { - draw_blocks::commands(app_data, top_panel[1], f, gui_state, log_index); + draw_blocks::commands(app_data, top_panel[1], f, gui_state); } - draw_blocks::logs( - app_data, - lower_main[0], - f, - gui_state, - log_index, - &loading_icon, - ); + draw_blocks::logs(app_data, lower_main[0], f, gui_state, &loading_icon); draw_blocks::heading_bar( whole_layout[0], @@ -217,7 +209,7 @@ fn ui( // only draw charts if there are containers if has_containers { - draw_blocks::chart(f, lower_main[1], app_data, log_index); + draw_blocks::chart(f, lower_main[1], app_data); } if let Some(info) = info_text {