From b8079ad96264acaf82120a4f2b3780df9e851b8b Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 22 Oct 2023 15:37:36 +0000 Subject: [PATCH 01/40] fix: devcontainer remove duplicate plugin --- .devcontainer/devcontainer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d86c30d..502db66 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -35,7 +35,6 @@ "mutantdino.resourcemonitor", "rangav.vscode-thunder-client", "redhat.vscode-yaml", - "redhat.vscode-yaml", "rust-lang.rust-analyzer", "serayuzgur.crates", "tamasfe.even-better-toml", From 4e9fb65fe29809a741546cf1017408afc8e21eb6 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:55:32 +0000 Subject: [PATCH 02/40] chore: Dependencies updated ratatui v0.24.0 fixes --- Cargo.lock | 90 ++++++++++++++++++++++++++++++++++--------- Cargo.toml | 2 +- src/ui/draw_blocks.rs | 31 ++++++++------- src/ui/mod.rs | 6 +-- 4 files changed, 90 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4731fc..5ba50de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -109,9 +127,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "bitflags" @@ -219,9 +237,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", "clap_derive", @@ -229,9 +247,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ "anstream", "anstyle", @@ -243,9 +261,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", @@ -255,9 +273,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "colorchoice" @@ -432,6 +450,10 @@ name = "hashbrown" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "heck" @@ -635,6 +657,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lru" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60" +dependencies = [ + "hashbrown 0.14.2", +] + [[package]] name = "memchr" version = "2.6.4" @@ -652,9 +683,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "log", @@ -859,15 +890,16 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e2e4cd95294a85c3b4446e63ef054eea43e0205b1fd60120c16b74ff7ff96ad" +checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425" dependencies = [ "bitflags 2.4.1", "cassowary", "crossterm", "indoc", "itertools", + "lru", "paste", "strum", "unicode-segmentation", @@ -1196,9 +1228,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -1248,12 +1280,12 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] @@ -1523,3 +1555,23 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zerocopy" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index e0055f6..9adde00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ parking_lot = { version= "0.12" } tokio = { version = "1.33", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" -ratatui = "0.23" +ratatui = "0.24" uuid = { version = "1.5", features = ["v4", "fast-rng"] } [dev-dependencies] diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 4080966..da3c3c2 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1,6 +1,5 @@ use parking_lot::Mutex; use ratatui::{ - backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, symbols, @@ -85,10 +84,10 @@ fn generate_block<'a>( } /// Draw the command panel -pub fn commands( +pub fn commands( app_data: &Arc>, area: Rect, - f: &mut Frame<'_, B>, + f: &mut Frame, gui_state: &Arc>, ) { let block = || generate_block(app_data, area, gui_state, SelectablePanel::Commands); @@ -120,10 +119,10 @@ pub fn commands( } /// Draw the containers panel -pub fn containers( +pub fn containers( app_data: &Arc>, area: Rect, - f: &mut Frame<'_, B>, + f: &mut Frame, gui_state: &Arc>, widths: &Columns, ) { @@ -219,10 +218,10 @@ pub fn containers( } /// Draw the logs panel -pub fn logs( +pub fn logs( app_data: &Arc>, area: Rect, - f: &mut Frame<'_, B>, + f: &mut Frame, gui_state: &Arc>, loading_icon: &str, ) { @@ -256,7 +255,7 @@ pub fn logs( } /// Draw the cpu + mem charts -pub fn chart(f: &mut Frame<'_, B>, area: Rect, app_data: &Arc>) { +pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc>) { if let Some((cpu, mem)) = app_data.lock().get_chart_data() { let area = Layout::default() .direction(Direction::Horizontal) @@ -337,10 +336,10 @@ fn make_chart<'a, T: Stats + Display>( /// Draw heading bar at top of program, always visible /// TODO Should separate into loading icon/headers/help functions #[allow(clippy::too_many_lines)] -pub fn heading_bar( +pub fn heading_bar( area: Rect, columns: &Columns, - f: &mut Frame<'_, B>, + f: &mut Frame, has_containers: bool, loading_icon: &str, sorted_by: Option<(Header, SortedOrder)>, @@ -646,7 +645,7 @@ impl HelpInfo { } /// Draw the help box in the centre of the screen -pub fn help_box(f: &mut Frame<'_, B>) { +pub fn help_box(f: &mut Frame) { let title = format!(" {VERSION} "); let name_info = HelpInfo::gen_name(); @@ -725,8 +724,8 @@ pub fn help_box(f: &mut Frame<'_, B>) { /// Draw the delete confirm box in the centre of the screen /// take in container id and container name here? -pub fn delete_confirm( - f: &mut Frame<'_, B>, +pub fn delete_confirm( + f: &mut Frame, gui_state: &Arc>, name: &str, ) { @@ -834,7 +833,7 @@ pub fn delete_confirm( } /// Draw an error popup over whole screen -pub fn error(f: &mut Frame<'_, B>, error: AppError, seconds: Option) { +pub fn error(f: &mut Frame, error: AppError, seconds: Option) { let block = Block::default() .title(" Error ") .border_type(BorderType::Rounded) @@ -876,7 +875,7 @@ pub fn error(f: &mut Frame<'_, B>, error: AppError, seconds: Option< } /// Draw info box in one of the 9 BoxLocations -pub fn info(f: &mut Frame<'_, B>, text: String) { +pub fn info(f: &mut Frame, text: String) { let block = Block::default() .title("") .title_alignment(Alignment::Center) @@ -928,7 +927,7 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio } // Draw nothing, as in a blank screen -// pub fn nothing(f: &mut Frame<'_, B>) { +// pub fn nothing(f: &mut Frame) { // let whole_layout = Layout::default() // .direction(Direction::Vertical) // .constraints([Constraint::Min(100)].as_ref()) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c8b8da6..b7c3a1f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use parking_lot::Mutex; use ratatui::{ - backend::{Backend, CrosstermBackend}, + backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, Frame, Terminal, }; @@ -208,8 +208,8 @@ macro_rules! value_capture { /// Draw the main ui to a frame of the terminal /// TODO add a single line area for debug message - if not in release mode? -fn draw_frame( - f: &mut Frame<'_, B>, +fn draw_frame( + f: &mut Frame, app_data: &Arc>, gui_state: &Arc>, ) { From e1998c9fca28230a3f1c97d64ee68b6382d2259c Mon Sep 17 00:00:00 2001 From: Daniel Boll Date: Mon, 30 Oct 2023 10:04:16 -0300 Subject: [PATCH 03/40] refactor(draw_blocks.rs): remove unnecessary .as_ref() calls on constraints method to improve code readability refactor(mod.rs): remove unnecessary .as_ref() calls on constraints method to improve code readability chore(draw_blocks.rs): format code using rustfmt to improve code readability chore(draw_blocks.rs): remove unnecessary indentation in NAME_TEXT constant to improve code readability chore(draw_blocks.rs): remove unnecessary fmt skip directive Signed-off-by: Daniel Boll --- src/ui/draw_blocks.rs | 4 ++-- src/ui/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index da3c3c2..26a9800 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -444,7 +444,7 @@ pub fn heading_bar( let split_bar = Layout::default() .direction(Direction::Horizontal) - .constraints(splits.as_ref()) + .constraints(splits) .split(area); if has_containers { // Draw loading icon, or not, and a prefix with a single space @@ -457,7 +457,7 @@ pub fn heading_bar( let container_splits = header_data.iter().map(|i| i.2).collect::>(); let headers_section = Layout::default() .direction(Direction::Horizontal) - .constraints(container_splits.as_ref()) + .constraints(container_splits) .split(split_bar[1]); // draw the actual header blocks diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b7c3a1f..005b1b3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -250,7 +250,7 @@ fn draw_frame( // Containers + docker commands let top_panel = Layout::default() .direction(Direction::Horizontal) - .constraints(top_split.as_ref()) + .constraints(top_split) .split(upper_main[0]); let lower_split = if has_containers { @@ -262,7 +262,7 @@ fn draw_frame( // Split into 2, logs, and optional charts let lower_main = Layout::default() .direction(Direction::Vertical) - .constraints(lower_split.as_ref()) + .constraints(lower_split) .split(upper_main[1]); draw_blocks::containers(app_data, top_panel[0], f, gui_state, &column_widths); From c8077bca0b673478cfbb417e677a885136ba9eff Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:38:15 +0000 Subject: [PATCH 04/40] feat: Docker exec mode, closes #28 --- README.md | 1 + containerised/Dockerfile | 22 ++- containerised/Dockerfile_dev | 22 ++- docker-compose.yml | 6 +- src/app_data/mod.rs | 50 +++--- src/app_error.rs | 2 + src/docker_data/mod.rs | 58 +++---- src/input_handler/mod.rs | 323 ++++++++++++++++++++--------------- src/main.rs | 32 +--- src/parse_args.rs | 35 +++- src/ui/draw_blocks.rs | 11 +- src/ui/gui_state.rs | 37 +++- src/ui/mod.rs | 70 ++++++-- 13 files changed, 397 insertions(+), 272 deletions(-) diff --git a/README.md b/README.md index 6c0140e..aaffc2e 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ In application controls | ```( enter )```| execute selected docker command| | ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column | | ```( 0 )``` | stop sorting | +| ```( e )``` | (attempt) to exec into the selected container | | ```( h )``` | toggle help menu | | ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected| | ```( q )``` | to quit at any time | diff --git a/containerised/Dockerfile b/containerised/Dockerfile index d3f255d..99242b6 100644 --- a/containerised/Dockerfile +++ b/containerised/Dockerfile @@ -45,18 +45,32 @@ RUN cargo build --release --target $(cat /.platform) RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker / +################ +## MUSL SETUP ## +################ + +FROM alpine:3.18 as MUSL_SETUP + +RUN apk add --update --no-cache docker-cli upx + +# Compress the docker executable, to reduce final image size +RUN upx -9 /usr/bin/docker + ############# ## Runtime ## ############# -FROM scratch AS RUNTIME +FROM alpine:3.18 as RUNTIME -# Set an ENV that we're running in a container, so that the application can sleep for 250ms at start +# Set an ENV to indicate that we're running in a container ENV OXKER_RUNTIME=container -# Copy application binary from builder image COPY --from=BUILDER /oxker /app/ +COPY --from=MUSL_SETUP /usr/bin/docker /usr/bin/ + +# remove sh and busybox, probably pointless +RUN rm /bin/sh /bin/busybox # Run the application -# this is used in the application itself, to stop itself from listing itself, so DO NOT EDIT +# this is used in the application itself so DO NOT EDIT ENTRYPOINT [ "/app/oxker"] diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev index 6ff45ba..0a3118c 100644 --- a/containerised/Dockerfile_dev +++ b/containerised/Dockerfile_dev @@ -1,12 +1,28 @@ +################ +## MUSL SETUP ## +################ + +FROM alpine:3.18 as MUSL_SETUP + +RUN apk add --update --no-cache docker-cli upx + +# Copy application binary from builder image +RUN upx -9 /usr/bin/docker + ############# ## Runtime ## ############# -FROM scratch -# Set env that we're running in a container, so that the application can sleep for 250ms at start +FROM alpine:3.18 as RUNTIME + +# Set an ENV that we're running in a container, so that the application can sleep for 250ms at start ENV OXKER_RUNTIME=container -# Copy application binary from builder image +COPY --from=MUSL_SETUP /usr/bin/docker /usr/bin/ + + +RUN rm /bin/sh /bin/busybox + COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/ # Run the application diff --git a/docker-compose.yml b/docker-compose.yml index cbf627b..0521dea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: deploy: resources: limits: - memory: 128M + memory: 1024M redis: image: redis:alpine3.18 container_name: redis @@ -27,7 +27,7 @@ services: deploy: resources: limits: - memory: 16M + memory: 384M rabbitmq: image: rabbitmq:3 container_name: rabbitmq @@ -38,6 +38,6 @@ services: deploy: resources: limits: - memory: 256M + memory: 512M diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 6a851a9..30a44e7 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -563,6 +563,8 @@ impl AppData { }) }); + let id = ContainerId::from(id.as_str()); + let is_oxker = i .command .as_ref() @@ -579,8 +581,6 @@ impl AppData { .as_ref() .map_or(String::new(), std::clone::Clone::clone); - let id = ContainerId::from(id.as_str()); - let created = i .created .map_or(0, |i| u64::try_from(i).unwrap_or_default()); @@ -624,31 +624,33 @@ impl AppData { let timestamp = self.args.timestamp; if let Some(container) = self.get_container_by_id(id) { - container.last_updated = Self::get_systemtime(); - let current_len = container.logs.len(); + if !container.is_oxker { + container.last_updated = Self::get_systemtime(); + let current_len = container.logs.len(); - for mut i in logs { - let tz = LogsTz::from(i.as_str()); - // Strip the timestamp if `-t` flag set - if !timestamp { - i = i.replace(&tz.to_string(), ""); + for mut i in logs { + let tz = LogsTz::from(i.as_str()); + // Strip the timestamp if `-t` flag set + if !timestamp { + i = i.replace(&tz.to_string(), ""); + } + let lines = if color { + log_sanitizer::colorize_logs(&i) + } else if raw { + log_sanitizer::raw(&i) + } else { + log_sanitizer::remove_ansi(&i) + }; + container.logs.insert(ListItem::new(lines), tz); } - let lines = if color { - log_sanitizer::colorize_logs(&i) - } else if raw { - log_sanitizer::raw(&i) - } else { - log_sanitizer::remove_ansi(&i) - }; - container.logs.insert(ListItem::new(lines), tz); - } - // Set the logs selected row for each container - // Either when no long currently selected, or currently selected (before updated) is already at end - if container.logs.state().selected().is_none() - || container.logs.state().selected().map_or(1, |f| f + 1) == current_len - { - container.logs.end(); + // Set the logs selected row for each container + // Either when no long currently selected, or currently selected (before updated) is already at end + if container.logs.state().selected().is_none() + || container.logs.state().selected().map_or(1, |f| f + 1) == current_len + { + container.logs.end(); + } } } } diff --git a/src/app_error.rs b/src/app_error.rs index f580b06..5a9a9aa 100644 --- a/src/app_error.rs +++ b/src/app_error.rs @@ -6,6 +6,7 @@ use std::fmt; #[derive(Debug, Clone, Copy)] pub enum AppError { DockerCommand(DockerControls), + DockerExec, DockerConnect, DockerInterval, InputPoll, @@ -18,6 +19,7 @@ impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::DockerCommand(s) => write!(f, "Unable to {s} container"), + Self::DockerExec => write!(f, "Unable to exec into container"), Self::DockerConnect => write!(f, "Unable to access docker daemon"), Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"), Self::InputPoll => write!(f, "Unable to poll user input"), diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 1540597..0f69d97 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -54,7 +54,6 @@ pub struct DockerData { app_data: Arc>, args: CliArgs, binate: Binate, - containerised: bool, docker: Arc, gui_state: Arc>, is_running: Arc, @@ -101,6 +100,7 @@ impl DockerData { spawn_id: SpawnId, spawns: Arc>>>, ) { + let mut stream = docker .stats( id.get(), @@ -191,7 +191,7 @@ impl DockerData { .into_iter() .filter_map(|f| match f.id { Some(_) => { - if self.containerised + if self.args.in_container && f.command .as_ref() .map_or(false, |c| c.starts_with(ENTRY_POINT)) @@ -286,32 +286,12 @@ impl DockerData { self.app_data.lock().sort_containers(); } - /// Animate the loading icon - fn loading_spin(loading_uuid: Uuid, gui_state: &Arc>) -> JoinHandle<()> { - let gui_state = Arc::clone(gui_state); - tokio::spawn(async move { - loop { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - gui_state.lock().next_loading(loading_uuid); - } - }) - } - - /// Stop the loading_spin function, and reset gui loading status - fn stop_loading_spin( - gui_state: &Arc>, - handle: &JoinHandle<()>, - loading_uuid: Uuid, - ) { - handle.abort(); - gui_state.lock().remove_loading(loading_uuid); - } - /// Initialize docker container data, before any messages are received async fn initialise_container_data(&mut self) { self.gui_state.lock().status_push(Status::Init); let loading_uuid = Uuid::new_v4(); - let loading_spin = Self::loading_spin(loading_uuid, &Arc::clone(&self.gui_state)); + let loading_handle = GuiState::start_loading_animation(&self.gui_state, loading_uuid); + // let handle = self.gui_state.lock().st let all_ids = self.update_all_containers().await; @@ -323,7 +303,9 @@ impl DockerData { while !self.app_data.lock().initialised(&all_ids) { tokio::time::sleep(std::time::Duration::from_millis(100)).await; } - Self::stop_loading_spin(&self.gui_state, &loading_spin, loading_uuid); + self.gui_state + .lock() + .stop_loading_animation(&loading_handle, loading_uuid); self.gui_state.lock().status_del(Status::Init); } @@ -350,27 +332,27 @@ impl DockerData { match message { DockerMessage::Pause(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.pause_container(id.get()).await.is_err() { Self::set_error(&app_data, DockerControls::Pause, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Restart(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.restart_container(id.get(), None).await.is_err() { Self::set_error(&app_data, DockerControls::Restart, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Start(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker .start_container(id.get(), None::>) .await @@ -378,33 +360,33 @@ impl DockerData { { Self::set_error(&app_data, DockerControls::Start, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Stop(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.stop_container(id.get(), None).await.is_err() { Self::set_error(&app_data, DockerControls::Stop, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Unpause(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.unpause_container(id.get()).await.is_err() { Self::set_error(&app_data, DockerControls::Unpause, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Delete(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker .remove_container( id.get(), @@ -419,7 +401,7 @@ impl DockerData { { Self::set_error(&app_data, DockerControls::Stop, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; self.gui_state.lock().set_delete_container(None); @@ -443,7 +425,6 @@ impl DockerData { /// Initialise self, and start the message receiving loop pub async fn init( app_data: Arc>, - containerised: bool, docker: Docker, docker_rx: Receiver, gui_state: Arc>, @@ -453,7 +434,6 @@ impl DockerData { if app_data.lock().get_error().is_none() { let mut inner = Self { app_data, - containerised, args, binate: Binate::One, docker: Arc::new(docker), diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index c05164c..8e9ba2b 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -13,17 +13,20 @@ use tokio::{ sync::mpsc::{Receiver, Sender}, task::JoinHandle, }; +use uuid::Uuid; mod message; use crate::{ app_data::{AppData, DockerControls, Header}, app_error::AppError, docker_data::DockerMessage, - ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui}, + ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui, DOCKER_COMMAND}, value_capture, }; pub use message::InputMessages; +const OCI_ERROR: &str = "OCI runtime exec failed"; + /// Handle all input events #[derive(Debug)] pub struct InputHandler { @@ -161,6 +164,42 @@ impl InputHandler { self.gui_state.lock().set_delete_container(None); } + /// Validate that one can exec into a Docker container + fn e_key(&self) { + let is_oxker = self.app_data.lock().is_oxker(); + if !is_oxker { + let uuid = Uuid::new_v4(); + let handle = GuiState::start_loading_animation(&self.gui_state, uuid); + let mut exec_err = Some(()); + + let id = self.app_data.lock().get_selected_container_id(); + + if let Some(id) = id { + if let Ok(output) = std::process::Command::new(DOCKER_COMMAND) + .args(["exec", id.get(), "pwd"]) + .output() + { + if let Ok(output) = String::from_utf8(output.stdout) { + if !output.starts_with(OCI_ERROR) { + exec_err = None; + } + } + } + + if exec_err.is_some() { + self.app_data.lock().set_error( + AppError::DockerExec, + &self.gui_state, + Status::Error, + ); + } else { + self.gui_state.lock().status_push(Status::Exec); + } + } + self.gui_state.lock().stop_loading_animation(&handle, uuid); + } + } + /// Handle any keyboard button events #[allow(clippy::too_many_lines)] async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) { @@ -171,153 +210,161 @@ impl InputHandler { .status_contains(&[Status::DeleteConfirm]) ); - value_capture!( - contains_error, - self.gui_state.lock().status_contains(&[Status::Error]) - ); - value_capture!( - contains_help, - self.gui_state.lock().status_contains(&[Status::Help]) - ); + let contains = |s: Status| self.gui_state.lock().status_contains(&[s]); - // Always just quit on Ctrl + c/C or q/Q - let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C'); - let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q'); - if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() { - self.quit().await; - } + let contains_error = contains(Status::Error); + let contains_help = contains(Status::Help); + let contains_exec = contains(Status::Exec); - if contains_error { - if let KeyCode::Char('c' | 'C') = key_code { - self.app_data.lock().remove_error(); - self.gui_state.lock().status_del(Status::Error); + if !contains_exec { + // Always just quit on Ctrl + c/C or q/Q + let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C'); + let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q'); + if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() { + self.quit().await; } - } else if contains_help { - match key_code { - KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help), - KeyCode::Char('m' | 'M') => self.m_key(), - _ => (), - } - } else if contains_delete { - match key_code { - KeyCode::Char('y' | 'Y') => self.confirm_delete().await, - KeyCode::Char('n' | 'N') => self.clear_delete(), - _ => (), - } - } else { - match key_code { - KeyCode::Char('0') => self.app_data.lock().reset_sorted(), - KeyCode::Char('1') => self.sort(Header::State), - KeyCode::Char('2') => self.sort(Header::Status), - KeyCode::Char('3') => self.sort(Header::Cpu), - KeyCode::Char('4') => self.sort(Header::Memory), - KeyCode::Char('5') => self.sort(Header::Id), - KeyCode::Char('6') => self.sort(Header::Name), - KeyCode::Char('7') => self.sort(Header::Image), - KeyCode::Char('8') => self.sort(Header::Rx), - KeyCode::Char('9') => self.sort(Header::Tx), - KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), - KeyCode::Char('m' | 'M') => self.m_key(), - KeyCode::Tab => { - // Skip control panel if no containers, could be refactored - let is_containers = - self.gui_state.lock().selected_panel == SelectablePanel::Containers; - let count = if self.app_data.lock().get_container_len() == 0 && is_containers { - 2 - } else { - 1 - }; - for _ in 0..count { - self.gui_state.lock().next_panel(); - } - } - KeyCode::BackTab => { - // Skip control panel if no containers, could be refactored - let is_containers = - self.gui_state.lock().selected_panel == SelectablePanel::Logs; - let count = if self.app_data.lock().get_container_len() == 0 && is_containers { - 2 - } else { - 1 - }; - for _ in 0..count { - self.gui_state.lock().previous_panel(); - } - } - KeyCode::Home => { - let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; - match selected_panel { - SelectablePanel::Containers => locked_data.containers_start(), - SelectablePanel::Logs => locked_data.log_start(), - SelectablePanel::Commands => locked_data.docker_command_start(), - } - } - KeyCode::End => { - let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; - match selected_panel { - SelectablePanel::Containers => locked_data.containers_end(), - SelectablePanel::Logs => locked_data.log_end(), - SelectablePanel::Commands => locked_data.docker_command_end(), - } - } - KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(), - KeyCode::PageUp => { - for _ in 0..=6 { - self.previous(); - } - } - KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(), - KeyCode::PageDown => { - for _ in 0..=6 { - self.next(); - } - } - KeyCode::Enter => { - // 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().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().is_oxker() { - return; + if contains_error { + if let KeyCode::Char('c' | 'C') = key_code { + self.app_data.lock().remove_error(); + self.gui_state.lock().status_del(Status::Error); + } + } else if contains_help { + match key_code { + KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help), + KeyCode::Char('m' | 'M') => self.m_key(), + _ => (), + } + } else if contains_delete { + match key_code { + KeyCode::Char('y' | 'Y') => self.confirm_delete().await, + KeyCode::Char('n' | 'N') => self.clear_delete(), + _ => (), + } + } else { + match key_code { + KeyCode::Char('0') => self.app_data.lock().reset_sorted(), + KeyCode::Char('1') => self.sort(Header::State), + KeyCode::Char('2') => self.sort(Header::Status), + KeyCode::Char('3') => self.sort(Header::Cpu), + KeyCode::Char('4') => self.sort(Header::Memory), + KeyCode::Char('5') => self.sort(Header::Id), + KeyCode::Char('6') => self.sort(Header::Name), + KeyCode::Char('7') => self.sort(Header::Image), + KeyCode::Char('8') => self.sort(Header::Rx), + KeyCode::Char('9') => self.sort(Header::Tx), + KeyCode::Char('e' | 'E') => self.e_key(), + KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), + KeyCode::Char('m' | 'M') => self.m_key(), + KeyCode::Tab => { + // Skip control panel if no containers, could be refactored + let is_containers = + self.gui_state.lock().selected_panel == SelectablePanel::Containers; + let count = + if self.app_data.lock().get_container_len() == 0 && is_containers { + 2 + } else { + 1 }; - if let Some(id) = option_id { - match command { - DockerControls::Delete => self - .docker_sender - .send(DockerMessage::ConfirmDelete(id)) - .await - .ok(), - DockerControls::Pause => { - self.docker_sender.send(DockerMessage::Pause(id)).await.ok() - } - DockerControls::Unpause => self - .docker_sender - .send(DockerMessage::Unpause(id)) - .await - .ok(), - DockerControls::Start => { - self.docker_sender.send(DockerMessage::Start(id)).await.ok() - } - DockerControls::Stop => { - self.docker_sender.send(DockerMessage::Stop(id)).await.ok() - } - DockerControls::Restart => self - .docker_sender - .send(DockerMessage::Restart(id)) - .await - .ok(), + for _ in 0..count { + self.gui_state.lock().next_panel(); + } + } + KeyCode::BackTab => { + // Skip control panel if no containers, could be refactored + let is_containers = + self.gui_state.lock().selected_panel == SelectablePanel::Logs; + let count = + if self.app_data.lock().get_container_len() == 0 && is_containers { + 2 + } else { + 1 + }; + for _ in 0..count { + self.gui_state.lock().previous_panel(); + } + } + KeyCode::Home => { + let mut locked_data = self.app_data.lock(); + let selected_panel = self.gui_state.lock().selected_panel; + match selected_panel { + SelectablePanel::Containers => locked_data.containers_start(), + SelectablePanel::Logs => locked_data.log_start(), + SelectablePanel::Commands => locked_data.docker_command_start(), + } + } + KeyCode::End => { + let mut locked_data = self.app_data.lock(); + let selected_panel = self.gui_state.lock().selected_panel; + match selected_panel { + SelectablePanel::Containers => locked_data.containers_end(), + SelectablePanel::Logs => locked_data.log_end(), + SelectablePanel::Commands => locked_data.docker_command_end(), + } + } + KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(), + KeyCode::PageUp => { + for _ in 0..=6 { + self.previous(); + } + } + KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(), + KeyCode::PageDown => { + for _ in 0..=6 { + self.next(); + } + } + KeyCode::Enter => { + // 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().selected_docker_command(); + + if let Some(command) = option_command { + // Poor way of disallowing commands to be sent to a containerised okxer + if self.app_data.lock().is_oxker() { + return; }; + let option_id = self.app_data.lock().get_selected_container_id(); + if let Some(id) = option_id { + match command { + DockerControls::Delete => self + .docker_sender + .send(DockerMessage::ConfirmDelete(id)) + .await + .ok(), + DockerControls::Pause => self + .docker_sender + .send(DockerMessage::Pause(id)) + .await + .ok(), + DockerControls::Unpause => self + .docker_sender + .send(DockerMessage::Unpause(id)) + .await + .ok(), + DockerControls::Start => self + .docker_sender + .send(DockerMessage::Start(id)) + .await + .ok(), + DockerControls::Stop => self + .docker_sender + .send(DockerMessage::Stop(id)) + .await + .ok(), + DockerControls::Restart => self + .docker_sender + .send(DockerMessage::Restart(id)) + .await + .ok(), + }; + } } } } + _ => (), } - _ => (), } } } diff --git a/src/main.rs b/src/main.rs index ef3e279..bad3e04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,18 +55,6 @@ fn setup_tracing() { tracing_subscriber::fmt().with_max_level(Level::INFO).init(); } -/// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately -/// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container -fn check_if_containerised() -> bool { - if let Ok(value) = std::env::var(ENV_KEY) { - if value == ENV_VALUE { - std::thread::sleep(std::time::Duration::from_millis(250)); - return true; - } - } - false -} - /// Read the optional docker_host path, the cli args take priority over the DOCKER_HOST env fn read_docker_host(args: &CliArgs) -> Option { args.host @@ -77,7 +65,6 @@ fn read_docker_host(args: &CliArgs) -> Option { /// Create docker daemon handler, and only spawn up the docker data handler if a ping returns non-error async fn docker_init( app_data: &Arc>, - containerised: bool, docker_rx: Receiver, gui_state: &Arc>, is_running: &Arc, @@ -93,12 +80,7 @@ async fn docker_init( let gui_state = Arc::clone(gui_state); let is_running = Arc::clone(is_running); tokio::spawn(DockerData::init( - app_data, - containerised, - docker, - docker_rx, - gui_state, - is_running, + app_data, docker, docker_rx, gui_state, is_running, )); } else { app_data @@ -134,8 +116,6 @@ fn handler_init( #[tokio::main] async fn main() { - let containerised = check_if_containerised(); - setup_tracing(); let args = CliArgs::new(); @@ -146,15 +126,7 @@ async fn main() { let is_running = Arc::new(AtomicBool::new(true)); let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(32); - docker_init( - &app_data, - containerised, - docker_rx, - &gui_state, - &is_running, - host, - ) - .await; + docker_init(&app_data, docker_rx, &gui_state, &is_running, host).await; if args.gui { let (input_sx, input_rx) = tokio::sync::mpsc::channel(32); diff --git a/src/parse_args.rs b/src/parse_args.rs index 7a69053..dc4f8a8 100644 --- a/src/parse_args.rs +++ b/src/parse_args.rs @@ -3,10 +3,12 @@ use std::process; use clap::Parser; use tracing::error; +use crate::{ENV_KEY, ENV_VALUE}; + #[derive(Parser, Debug, Clone)] #[allow(clippy::struct_excessive_bools)] #[command(version, about)] -pub struct CliArgs { +pub struct Args { /// Docker update interval in ms, minimum effectively 1000 #[clap(short = 'd', value_name = "ms", default_value_t = 1000)] pub docker_interval: u32, @@ -36,10 +38,34 @@ pub struct CliArgs { pub gui: bool, } +#[derive(Debug, Clone)] +#[allow(clippy::struct_excessive_bools)] +pub struct CliArgs { + pub in_container: bool, + pub color: bool, + pub docker_interval: u32, + pub gui: bool, + pub host: Option, + pub raw: bool, + pub show_self: bool, + pub timestamp: bool, +} + impl CliArgs { + /// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately + /// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container + fn check_if_in_container() -> bool { + if let Ok(value) = std::env::var(ENV_KEY) { + if value == ENV_VALUE { + return true; + } + } + false + } + /// Parse cli arguments pub fn new() -> Self { - let args = Self::parse(); + let args = Args::parse(); // Quit the program if the docker update argument is 0 // Should maybe change it to check if less than 100 @@ -50,10 +76,11 @@ impl CliArgs { Self { color: args.color, docker_interval: args.docker_interval, - host: args.host, gui: !args.gui, - show_self: !args.show_self, + host: args.host, + in_container: Self::check_if_in_container(), raw: args.raw, + show_self: !args.show_self, timestamp: !args.timestamp, } } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 26a9800..8d227de 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -585,6 +585,11 @@ impl HelpInfo { space(), button_item("enter"), button_desc("to send docker container command"), + ]), + Line::from(vec![ + space(), + button_item("e"), + button_desc("exec into a container"), ]), Line::from(vec![ space(), @@ -724,11 +729,7 @@ pub fn help_box(f: &mut Frame) { /// Draw the delete confirm box in the centre of the screen /// take in container id and container name here? -pub fn delete_confirm( - f: &mut Frame, - gui_state: &Arc>, - name: &str, -) { +pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &str) { let block = Block::default() .title(" Confirm Delete ") .border_type(BorderType::Rounded) diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 18e92f5..967aff9 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -1,5 +1,10 @@ +use parking_lot::Mutex; use ratatui::layout::{Constraint, Rect}; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; +use tokio::task::JoinHandle; use uuid::Uuid; use crate::app_data::{ContainerId, Header}; @@ -150,11 +155,12 @@ const FRAMES_LEN: u8 = 9; /// Various functions (e.g input handler), operate differently depending upon current Status #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Status { - Init, - Help, - DockerConnect, + Exec, DeleteConfirm, + DockerConnect, Error, + Help, + Init, } /// Global gui_state, stored in an Arc @@ -296,13 +302,34 @@ impl GuiState { } /// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0 - pub fn remove_loading(&mut self, uuid: Uuid) { + fn remove_loading(&mut self, uuid: Uuid) { self.is_loading.remove(&uuid); if self.is_loading.is_empty() { self.loading_index = 0; } } + /// Animate the loading icon in its own Tokio thread + pub fn start_loading_animation( + gui_state: &Arc>, + loading_uuid: Uuid, + ) -> JoinHandle<()> { + gui_state.lock().next_loading(loading_uuid); + let gui_state = Arc::clone(gui_state); + tokio::spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + gui_state.lock().next_loading(loading_uuid); + } + }) + } + + /// Stop the loading_spin function, and reset gui loading status + pub fn stop_loading_animation(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) { + handle.abort(); + self.remove_loading(loading_uuid); + } + /// Set info box content pub fn set_info_box(&mut self, text: &str) { self.info_box_text = Some(text.to_owned()); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 005b1b3..7d81228 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -27,10 +27,13 @@ pub use self::color_match::*; pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ app_data::AppData, app_error::AppError, docker_data::DockerMessage, - input_handler::InputMessages, + input_handler::InputMessages, parse_args::CliArgs, }; +pub const DOCKER_COMMAND: &str = "docker"; + pub struct Ui { + args: CliArgs, app_data: Arc>, docker_sx: Sender, gui_state: Arc>, @@ -63,7 +66,9 @@ impl Ui { sender: Sender, ) { if let Ok(terminal) = Self::setup_terminal() { + let args = app_data.lock().args.clone(); let mut ui = Self { + args, app_data, docker_sx, gui_state, @@ -86,19 +91,17 @@ impl Ui { /// Setup the terminal for full-screen drawing mode, with mouse capture fn setup_terminal() -> Result>> { - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen)?; - Self::enable_mouse_capture()?; + let stdout = Self::init_terminal()?; let backend = CrosstermBackend::new(stdout); Ok(Terminal::new(backend)?) } - /// This is a fix for mouse-events being printed to screen, read an event and do nothing with it - fn nullify_event_read(&self) { - if crossterm::event::poll(self.input_poll_rate).unwrap_or(true) { - event::read().ok(); - } + fn init_terminal() -> Result { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + Self::enable_mouse_capture()?; + Ok(stdout) } /// reset the terminal back to default settings @@ -137,12 +140,48 @@ impl Ui { Ok(()) } + /// Use exeternal docker cli to exec into a container + fn exec(&mut self) { + let id = self.app_data.lock().get_selected_container_id(); + + if let Some(id) = id { + // if Self::can_exec(&id).is_some() { + if let Ok(mut child) = std::process::Command::new(DOCKER_COMMAND) + .args(["exec", "-it", id.get(), "sh"]) + .stdin(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .spawn() + { + self.reset_terminal().ok(); + child.wait().ok(); + if child.kill().is_err() { + std::process::exit(1) + } + // } + } + } + + self.terminal.clear().ok(); + self.reset_terminal().ok(); + Self::init_terminal().ok(); + self.gui_state.lock().status_del(Status::Exec); + } + /// The loop for drawing the main UI to the terminal async fn gui_loop(&mut self) -> Result<(), AppError> { let update_duration = - std::time::Duration::from_millis(u64::from(self.app_data.lock().args.docker_interval)); + std::time::Duration::from_millis(u64::from(self.args.docker_interval)); while self.is_running.load(Ordering::SeqCst) { + let exec = self.gui_state.lock().status_contains(&[Status::Exec]); + + if exec { + self.exec(); + self.docker_sx.send(DockerMessage::Update).await.ok(); + continue; + } + if self .terminal .draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state)) @@ -150,6 +189,7 @@ impl Ui { { return Err(AppError::Terminal); } + if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) { if let Ok(event) = event::read() { if let Event::Key(key) = event { @@ -173,6 +213,7 @@ impl Ui { } } + // Should this be done in the docker thread instead? if self.now.elapsed() >= update_duration { self.docker_sx.send(DockerMessage::Update).await.ok(); self.now = Instant::now(); @@ -192,7 +233,6 @@ impl Ui { } else { self.gui_loop().await?; } - self.nullify_event_read(); Ok(()) } } @@ -208,11 +248,7 @@ macro_rules! value_capture { /// Draw the main ui to a frame of the terminal /// TODO add a single line area for debug message - if not in release mode? -fn draw_frame( - f: &mut Frame, - app_data: &Arc>, - gui_state: &Arc>, -) { +fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) { value_capture!(height, app_data.lock().get_container_len()); value_capture!(column_widths, app_data.lock().get_width()); value_capture!(has_containers, app_data.lock().get_container_len() > 0); From e301b51891e03ea40b2f904583119da3bc4daf53 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:42:46 +0000 Subject: [PATCH 05/40] chore: dependencies updated --- Cargo.lock | 126 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ba50de..24daaf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,9 +209,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" dependencies = [ "libc", ] @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.7" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" dependencies = [ "clap_builder", "clap_derive", @@ -247,9 +247,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.7" +version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" dependencies = [ "anstream", "anstyle", @@ -353,24 +353,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", @@ -379,21 +379,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", "futures-macro", @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -475,9 +475,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -590,9 +590,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.2", @@ -622,9 +622,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -637,9 +637,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "lock_api" @@ -941,18 +941,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", @@ -961,9 +961,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -972,9 +972,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", @@ -1003,7 +1003,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_json", "time", @@ -1059,9 +1059,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" @@ -1113,9 +1113,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -1198,9 +1198,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes", @@ -1217,9 +1217,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", @@ -1280,9 +1280,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", @@ -1291,9 +1291,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -1407,9 +1407,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1417,9 +1417,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", @@ -1432,9 +1432,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1442,9 +1442,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", @@ -1455,9 +1455,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "winapi" @@ -1558,18 +1558,18 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "zerocopy" -version = "0.7.15" +version = "0.7.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.15" +version = "0.7.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9adde00..2621213 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ clap = { version = "4.4", features = ["derive", "unicode", "color"] } crossterm = "0.27" futures-util = "0.3" parking_lot = { version= "0.12" } -tokio = { version = "1.33", features = ["full"] } +tokio = { version = "1.34", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" ratatui = "0.24" From 6a4cf6490d08b976734e2bc8186d94c095700558 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:43:14 +0000 Subject: [PATCH 06/40] chore: workflow dependencies updated --- .github/workflows/create_release_and_build.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml index 81f7265..d861736 100644 --- a/.github/workflows/create_release_and_build.yml +++ b/.github/workflows/create_release_and_build.yml @@ -77,7 +77,7 @@ jobs: uses: actions/download-artifact@v3 - name: Update Release - uses: ncipollo/release-action@v1.12.0 + uses: ncipollo/release-action@v1 with: makeLatest: true name: ${{ github.ref_name }} @@ -87,7 +87,6 @@ jobs: artifacts: | **/oxker_*.zip **/oxker_*.tar.gz - ######################### ## Publish to crates.io # ######################### @@ -103,11 +102,10 @@ jobs: uses: katyo/publish-crates@v1 with: registry-token: ${{ secrets.CRATES_IO_TOKEN }} - + ######################################### ## Build images for Dockerhub & ghcr.io # ######################################### - image_build: needs: [cargo_publish] runs-on: ubuntu-latest @@ -116,14 +114,14 @@ jobs: uses: actions/checkout@v4 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -133,7 +131,7 @@ jobs: CURRENT_SEMVER=${GITHUB_REF_NAME#v} echo "CURRENT_SEMVER=$CURRENT_SEMVER" >> $GITHUB_ENV - - uses: docker/setup-buildx-action@v2 + - uses: docker/setup-buildx-action@v3 id: buildx with: install: true @@ -146,6 +144,4 @@ jobs: -t ghcr.io/${{ github.repository_owner }}/oxker:${{env.CURRENT_SEMVER}} \ --provenance=false --sbom=false \ --push \ - -f containerised/Dockerfile . - - + -f containerised/Dockerfile . \ No newline at end of file From 650aa0fc919713857f613b8e74c49cde147cee00 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:12:18 +0000 Subject: [PATCH 07/40] refactor: DockerData refactors Use a croner in the docker_data, instead of in the ui thread, as this thread will be paused when in exec mode. is_initilised is again done in docker_data, after stats have been calculated use bollard from git, waiting for new release due to Docker changes --- Cargo.toml | 4 +- src/app_data/container_state.rs | 11 ++- src/docker_data/mod.rs | 167 +++++++++++++++++++++----------- 3 files changed, 122 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2621213..1fd235d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,9 @@ categories = ["command-line-utilities"] [dependencies] anyhow = "1.0" -bollard = "0.15" +# bollard = "0.15" +bollard = { git = "https://www.github.com/fussybeaver/bollard.git", rev = "cef1cd5" } + cansi = "2.2" clap = { version = "4.4", features = ["derive", "unicode", "color"] } crossterm = "0.27" diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 08f0785..4ca9b72 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -121,6 +121,9 @@ pub enum State { } impl State { + pub const fn is_alive(self) -> bool { + matches!(self, Self::Running) + } pub const fn get_color(self) -> Color { match self { Self::Paused => Color::Yellow, @@ -158,6 +161,12 @@ impl From<&str> for State { } } +impl From> for State { + fn from(input: Option) -> Self { + input.map_or(Self::Unknown, |input| Self::from(input.as_str())) + } +} + impl fmt::Display for State { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let disp = match self { @@ -216,7 +225,7 @@ impl fmt::Display for DockerControls { Self::Restart => "restart", Self::Start => "start", Self::Stop => "stop", - Self::Unpause => "unpause", + Self::Unpause => "resume", }; write!(f, "{disp}") } diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 0f69d97..b88d915 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -10,13 +10,19 @@ use futures_util::StreamExt; use parking_lot::Mutex; use std::{ collections::HashMap, - sync::{atomic::AtomicBool, Arc}, + sync::{ + atomic::{AtomicBool, AtomicUsize}, + Arc, + }, +}; +use tokio::{ + sync::mpsc::{Receiver, Sender}, + task::JoinHandle, }; -use tokio::{sync::mpsc::Receiver, task::JoinHandle}; use uuid::Uuid; use crate::{ - app_data::{AppData, ContainerId, DockerControls}, + app_data::{AppData, ContainerId, DockerControls, State}, app_error::AppError, parse_args::CliArgs, ui::{GuiState, Status}, @@ -50,6 +56,11 @@ impl Binate { } } +// struct Init { +// done: , +// len: +// } + pub struct DockerData { app_data: Arc>, args: CliArgs, @@ -57,6 +68,7 @@ pub struct DockerData { docker: Arc, gui_state: Arc>, is_running: Arc, + init: Option>, receiver: Receiver, spawns: Arc>>>, } @@ -96,67 +108,93 @@ impl DockerData { app_data: Arc>, docker: Arc, id: ContainerId, - is_running: bool, + init: Option<(Arc, usize)>, + state: State, spawn_id: SpawnId, spawns: Arc>>>, ) { + // if dead and !init then inspect! - let mut stream = docker - .stats( - id.get(), - Some(StatsOptions { - stream: false, - one_shot: !is_running, - }), - ) - .take(1); + if state.is_alive() || init.is_some() { + // // if state == State::Paused && init.is_some() { + // // app_data.lock().debug_string.push_str("is paused"); - while let Some(Ok(stats)) = stream.next().await { - let mem_stat = stats.memory_stats.usage.unwrap_or_default(); - let mem_limit = stats.memory_stats.limit.unwrap_or_default(); + // // if let Ok(result) = docker.inspect_container(id.get(), Some(InspectContainerOptions{size:false})).await { + // // let mem_limit = format!("{}", result.host_config.map_or(0, |i|i.memory.unwrap_or_default())); - let op_key = stats - .networks - .as_ref() - .and_then(|networks| networks.keys().next().cloned()); + // // app_data.lock().debug_string.push_str(&mem_limit); + // // } - let cpu_stats = Self::calculate_usage(&stats); + // // }else if state.is_alive() || init.is_some() { + let mut stream = docker + .stats( + id.get(), + Some(StatsOptions { + stream: false, + one_shot: false, + }), + ) + .take(1); - let (rx, tx) = if let Some(key) = op_key { - stats + // let a = stream.next().await; + // app_data.lock().debug_string.push_str(&format!("{:?}", a.is_some())); + // let bb = a.unwrap().unwrap(); + + // // } + + // app_data.lock().debug_string.push_str("jkl"); + + while let Some(Ok(stats)) = stream.next().await { + let mem_stat = if state.is_alive() { + Some(stats.memory_stats.usage.unwrap_or_default()) + } else { + None + }; + + let mem_limit = stats.memory_stats.limit.unwrap_or_default(); + + let op_key = stats .networks - .unwrap_or_default() - .get(&key) - .map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes)) - } else { - (0, 0) - }; + .as_ref() + .and_then(|networks| networks.keys().next().cloned()); + + let cpu_stats = if state.is_alive() { + Some(Self::calculate_usage(&stats)) + } else { + None + }; + let (rx, tx) = if let Some(key) = op_key { + stats + .networks + .unwrap_or_default() + .get(&key) + .map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes)) + } else { + (0, 0) + }; - if is_running { - app_data.lock().update_stats( - &id, - Some(cpu_stats), - Some(mem_stat), - mem_limit, - rx, - tx, - ); - } else { app_data .lock() - .update_stats(&id, None, None, mem_limit, rx, tx); + .update_stats(&id, cpu_stats, mem_stat, mem_limit, rx, tx); } - spawns.lock().remove(&spawn_id); + } + spawns.lock().remove(&spawn_id); + if let Some((target, _)) = init { + target.fetch_add(1, std::sync::atomic::Ordering::SeqCst); } } /// Update all stats, spawn each container into own tokio::spawn thread - fn update_all_container_stats(&mut self, all_ids: &[(bool, ContainerId)]) { - for (is_running, id) in all_ids { + fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) { + // let thing =all_ids.len(); + for (state, id) in all_ids { + // let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len()))); let docker = Arc::clone(&self.docker); let app_data = Arc::clone(&self.app_data); let spawns = Arc::clone(&self.spawns); let spawn_id = SpawnId::Stats((id.clone(), self.binate)); + + let init = self.init.as_ref().map(|i| (Arc::clone(i), all_ids.len())); self.spawns .lock() .entry(spawn_id.clone()) @@ -165,7 +203,8 @@ impl DockerData { app_data, docker, id.clone(), - *is_running, + init, + *state, spawn_id, spawns, )) @@ -177,7 +216,7 @@ impl DockerData { /// Get all current containers, handle into ContainerItem in the app_data struct rather than here /// Just make sure that items sent are guaranteed to have an id /// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set - pub async fn update_all_containers(&mut self) -> Vec<(bool, ContainerId)> { + pub async fn update_all_containers(&mut self) -> Vec<(State, ContainerId)> { let containers = self .docker .list_containers(Some(ListContainersOptions:: { @@ -212,13 +251,7 @@ impl DockerData { output .into_iter() .filter_map(|i| { - i.id.map(|id| { - ( - i.state == Some("running".to_owned()) - || i.state == Some("restarting".to_owned()), - ContainerId::from(id.as_str()), - ) - }) + i.id.map(|id| (State::from(i.state), ContainerId::from(id.as_str()))) }) .collect::>() } @@ -253,7 +286,7 @@ impl DockerData { } /// Update all logs, spawn each container into own tokio::spawn thread - fn init_all_logs(&mut self, all_ids: &[(bool, ContainerId)]) { + fn init_all_logs(&mut self, all_ids: &[(State, ContainerId)]) { for (_, id) in all_ids { let docker = Arc::clone(&self.docker); let app_data = Arc::clone(&self.app_data); @@ -275,6 +308,7 @@ impl DockerData { .lock() .entry(SpawnId::Log(container.id.clone())) .or_insert_with(|| { + // TODO make a struct that can create this data let app_data = Arc::clone(&self.app_data); let docker = Arc::clone(&self.docker); let id = container.id.clone(); @@ -291,17 +325,17 @@ impl DockerData { self.gui_state.lock().status_push(Status::Init); let loading_uuid = Uuid::new_v4(); let loading_handle = GuiState::start_loading_animation(&self.gui_state, loading_uuid); - // let handle = self.gui_state.lock().st - let all_ids = self.update_all_containers().await; self.update_all_container_stats(&all_ids); self.init_all_logs(&all_ids); - // wait until all logs have initialised - while !self.app_data.lock().initialised(&all_ids) { + while let Some(x) = self.init.as_ref() { tokio::time::sleep(std::time::Duration::from_millis(100)).await; + if x.load(std::sync::atomic::Ordering::SeqCst) == all_ids.len() { + self.init = None; + } } self.gui_state .lock() @@ -422,11 +456,26 @@ impl DockerData { } } + /// Send an update message every x ms, where x is the args.docker_interval + fn croner(args: &CliArgs, docker_tx: Sender) { + let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval)); + let mut now = std::time::Instant::now(); + tokio::spawn(async move { + loop { + let to_sleep = update_duration.saturating_sub(now.elapsed()); + tokio::time::sleep(to_sleep).await; + docker_tx.send(DockerMessage::Update).await.ok(); + now = std::time::Instant::now(); + } + }); + } + /// Initialise self, and start the message receiving loop pub async fn init( app_data: Arc>, docker: Docker, docker_rx: Receiver, + docker_tx: Sender, gui_state: Arc>, is_running: Arc, ) { @@ -434,15 +483,17 @@ impl DockerData { if app_data.lock().get_error().is_none() { let mut inner = Self { app_data, - args, + args: args.clone(), binate: Binate::One, docker: Arc::new(docker), gui_state, + init: Some(Arc::new(AtomicUsize::new(0))), is_running, receiver: docker_rx, spawns: Arc::new(Mutex::new(HashMap::new())), }; inner.initialise_container_data().await; + Self::croner(&args, docker_tx); inner.message_handler().await; } } From 40090865fd6f11e8c52e105290d0aeebadc89b3e Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:54:01 +0000 Subject: [PATCH 08/40] refactor: Multiple UI improvements; Use FrameData struct to store commonly accessed data, in order to reduce mutex locks. rename unpause to resume use get_selected_panel() function instead of directly gui_state.selected_panel debug mode now shows some usefull information --- src/app_data/container_state.rs | 19 +++ src/app_data/mod.rs | 48 +++++-- src/docker_data/mod.rs | 28 +--- src/input_handler/mod.rs | 28 ++-- src/main.rs | 51 +++++-- src/ui/draw_blocks.rs | 240 +++++++++++++++++--------------- src/ui/gui_state.rs | 19 ++- src/ui/mod.rs | 169 ++++++++++++---------- 8 files changed, 338 insertions(+), 264 deletions(-) diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 4ca9b72..8c4973f 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -28,6 +28,11 @@ impl ContainerId { pub fn get(&self) -> &str { self.0.as_str() } + + /// Only return first 8 chars of id, is usually more than enough for uniqueness + pub fn get_short(&self) -> String { + self.0.chars().take(8).collect::() + } } impl Ord for ContainerId { @@ -443,6 +448,20 @@ pub struct ContainerItem { pub is_oxker: bool, } +/// Basic display information, for when running in debug mode +impl fmt::Display for ContainerItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}, {}, {}, {}", + self.id.get_short(), + self.name, + self.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)), + self.mem_stats.back().unwrap_or(&ByteStats::new(0)) + ) + } +} + impl ContainerItem { /// Create a new container item pub fn new( diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 30a44e7..3ad7a44 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -17,6 +17,7 @@ use crate::{ }; pub use container_state::*; +#[cfg(not(debug_assertions))] /// Global app_state, stored in an Arc #[derive(Debug, Clone)] pub struct AppData { @@ -26,6 +27,17 @@ pub struct AppData { pub args: CliArgs, } +#[cfg(debug_assertions)] +/// Global app_state, stored in an Arc +#[derive(Debug, Clone)] +pub struct AppData { + containers: StatefulList, + error: Option, + sorted_by: Option<(Header, SortedOrder)>, + debug_string: String, + pub args: CliArgs, +} + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum SortedOrder { Asc, @@ -64,6 +76,17 @@ impl fmt::Display for Header { } impl AppData { + #[cfg(debug_assertions)] + pub fn get_debug_string(&self) -> &str { + &self.debug_string + } + + #[cfg(debug_assertions)] + #[allow(unused)] + pub fn set_debug_string(&mut self, x: &str) { + self.debug_string.push_str(x); + } + /// 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; @@ -86,6 +109,7 @@ impl AppData { } /// Generate a default app_state + #[cfg(not(debug_assertions))] pub fn default(args: CliArgs) -> Self { Self { args, @@ -95,6 +119,18 @@ impl AppData { } } + /// Generate a default app_state + #[cfg(debug_assertions)] + pub fn default(args: CliArgs) -> Self { + Self { + args, + containers: StatefulList::new(vec![]), + error: None, + sorted_by: None, + debug_string: String::new(), + } + } + /// Container sort related methods /// Remove the sorted header & order, and sort by default - created datetime @@ -410,18 +446,6 @@ impl AppData { 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(); - let number_with_cpu_status = self - .containers - .items - .iter() - .filter(|i| !i.cpu_stats.is_empty()) - .count(); - count_is_running == number_with_cpu_status - } - /// Find the widths for the strings in the containers panel. /// So can display nicely and evenly pub fn get_width(&self) -> Columns { diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index b88d915..41de793 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -56,11 +56,6 @@ impl Binate { } } -// struct Init { -// done: , -// len: -// } - pub struct DockerData { app_data: Arc>, args: CliArgs, @@ -113,19 +108,7 @@ impl DockerData { spawn_id: SpawnId, spawns: Arc>>>, ) { - // if dead and !init then inspect! - if state.is_alive() || init.is_some() { - // // if state == State::Paused && init.is_some() { - // // app_data.lock().debug_string.push_str("is paused"); - - // // if let Ok(result) = docker.inspect_container(id.get(), Some(InspectContainerOptions{size:false})).await { - // // let mem_limit = format!("{}", result.host_config.map_or(0, |i|i.memory.unwrap_or_default())); - - // // app_data.lock().debug_string.push_str(&mem_limit); - // // } - - // // }else if state.is_alive() || init.is_some() { let mut stream = docker .stats( id.get(), @@ -136,14 +119,6 @@ impl DockerData { ) .take(1); - // let a = stream.next().await; - // app_data.lock().debug_string.push_str(&format!("{:?}", a.is_some())); - // let bb = a.unwrap().unwrap(); - - // // } - - // app_data.lock().debug_string.push_str("jkl"); - while let Some(Ok(stats)) = stream.next().await { let mem_stat = if state.is_alive() { Some(stats.memory_stats.usage.unwrap_or_default()) @@ -186,7 +161,6 @@ impl DockerData { /// Update all stats, spawn each container into own tokio::spawn thread fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) { - // let thing =all_ids.len(); for (state, id) in all_ids { // let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len()))); let docker = Arc::clone(&self.docker); @@ -308,7 +282,7 @@ impl DockerData { .lock() .entry(SpawnId::Log(container.id.clone())) .or_insert_with(|| { - // TODO make a struct that can create this data + // MAYBE make a struct that can create this data? let app_data = Arc::clone(&self.app_data); let docker = Arc::clone(&self.docker); let id = container.id.clone(); diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 8e9ba2b..0f8cb91 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -21,7 +21,6 @@ use crate::{ app_error::AppError, docker_data::DockerMessage, ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui, DOCKER_COMMAND}, - value_capture, }; pub use message::InputMessages; @@ -201,14 +200,13 @@ impl InputHandler { } /// Handle any keyboard button events + // TODO refactor this #[allow(clippy::too_many_lines)] async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) { - value_capture!( - contains_delete, - self.gui_state - .lock() - .status_contains(&[Status::DeleteConfirm]) - ); + let contains_delete = self + .gui_state + .lock() + .status_contains(&[Status::DeleteConfirm]); let contains = |s: Status| self.gui_state.lock().status_contains(&[s]); @@ -258,8 +256,8 @@ impl InputHandler { KeyCode::Char('m' | 'M') => self.m_key(), KeyCode::Tab => { // Skip control panel if no containers, could be refactored - let is_containers = - self.gui_state.lock().selected_panel == SelectablePanel::Containers; + let is_containers = self.gui_state.lock().get_selected_panel() + == SelectablePanel::Containers; let count = if self.app_data.lock().get_container_len() == 0 && is_containers { 2 @@ -273,7 +271,7 @@ impl InputHandler { KeyCode::BackTab => { // Skip control panel if no containers, could be refactored let is_containers = - self.gui_state.lock().selected_panel == SelectablePanel::Logs; + self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs; let count = if self.app_data.lock().get_container_len() == 0 && is_containers { 2 @@ -286,7 +284,7 @@ impl InputHandler { } KeyCode::Home => { let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; + let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => locked_data.containers_start(), SelectablePanel::Logs => locked_data.log_start(), @@ -295,7 +293,7 @@ impl InputHandler { } KeyCode::End => { let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; + let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => locked_data.containers_end(), SelectablePanel::Logs => locked_data.log_end(), @@ -316,7 +314,7 @@ impl InputHandler { } KeyCode::Enter => { // 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; + let panel = self.gui_state.lock().get_selected_panel(); if panel == SelectablePanel::Commands { let option_command = self.app_data.lock().selected_docker_command(); @@ -417,7 +415,7 @@ impl InputHandler { /// Change state to next, depending which panel is currently in focus fn next(&mut self) { let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; + let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => locked_data.containers_next(), SelectablePanel::Logs => locked_data.log_next(), @@ -428,7 +426,7 @@ impl InputHandler { /// Change state to previous, depending which panel is currently in focus fn previous(&mut self) { let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; + let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => locked_data.containers_previous(), SelectablePanel::Logs => locked_data.log_previous(), diff --git a/src/main.rs b/src/main.rs index bad3e04..b2387cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,6 +66,7 @@ fn read_docker_host(args: &CliArgs) -> Option { async fn docker_init( app_data: &Arc>, docker_rx: Receiver, + docker_tx: Sender, gui_state: &Arc>, is_running: &Arc, host: Option, @@ -79,8 +80,9 @@ async fn docker_init( 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, + app_data, docker, docker_rx, docker_tx, gui_state, is_running, )); } else { app_data @@ -102,15 +104,15 @@ fn handler_init( 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 app_data = Arc::clone(app_data); + let gui_state = Arc::clone(gui_state); + let is_running = Arc::clone(is_running); tokio::spawn(input_handler::InputHandler::init( - input_app_data, + app_data, input_rx, docker_sx.clone(), - input_gui_state, - input_is_running, + gui_state, + is_running, )); } @@ -124,28 +126,49 @@ async fn main() { let app_data = Arc::new(Mutex::new(AppData::default(args.clone()))); let gui_state = Arc::new(Mutex::new(GuiState::default())); let is_running = Arc::new(AtomicBool::new(true)); - let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(32); + let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32); - docker_init(&app_data, docker_rx, &gui_state, &is_running, host).await; + docker_init( + &app_data, + docker_rx, + docker_tx.clone(), + &gui_state, + &is_running, + host, + ) + .await; if args.gui { let (input_sx, input_rx) = tokio::sync::mpsc::channel(32); - handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running); - Ui::create(app_data, docker_sx, gui_state, is_running, input_sx).await; + handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running); + Ui::create(app_data, gui_state, is_running, input_sx).await; } else { - info!("in debug mode"); - // Debug mode for testing, mostly pointless, doesn't take terminal + info!("in debug mode\n"); + // Debug mode for testing, less pointless now, will diplay some basic information while is_running.load(Ordering::SeqCst) { loop { if let Some(err) = app_data.lock().get_error() { error!("{}", err); process::exit(1); } - docker_sx.send(DockerMessage::Update).await.ok(); tokio::time::sleep(std::time::Duration::from_millis(u64::from( args.docker_interval, ))) .await; + let containers = app_data + .lock() + .get_container_items() + .clone() + .iter() + .map(|i| format!("{i}")) + .collect::>(); + + if !containers.is_empty() { + for item in containers { + info!("{item}"); + } + println!(); + } } } } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 8d227de..9fe3cf0 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -13,14 +13,16 @@ use ratatui::{ use std::default::Default; use std::{fmt::Display, sync::Arc}; -use crate::app_data::{Header, SortedOrder}; -use crate::ui::Status; +use crate::app_data::{ContainerItem, Header, SortedOrder}; use crate::{ app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats}, app_error::AppError, }; -use super::gui_state::{BoxLocation, DeleteButton, Region}; +use super::{ + gui_state::{BoxLocation, DeleteButton, Region}, + FrameData, +}; use super::{GuiState, SelectablePanel}; const NAME_TEXT: &str = r#" @@ -39,7 +41,7 @@ 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 = " "; -const ARROW: &str = "â–¶ "; +const RIGHT_ARROW: &str = "â–¶ "; const CIRCLE: &str = "⚪ "; /// From a given &str, return the maximum number of chars on a single line @@ -55,6 +57,7 @@ fn max_line_width(text: &str) -> usize { fn generate_block<'a>( app_data: &Arc>, area: Rect, + fd: &FrameData, gui_state: &Arc>, panel: SelectablePanel, ) -> Block<'a> { @@ -77,7 +80,7 @@ fn generate_block<'a>( .borders(Borders::ALL) .border_type(BorderType::Rounded) .title(title); - if gui_state.lock().selected_panel == panel { + if fd.selected_panel == panel { block = block.border_style(Style::default().fg(Color::LightCyan)); } block @@ -88,9 +91,10 @@ pub fn commands( app_data: &Arc>, area: Rect, f: &mut Frame, + fd: &FrameData, gui_state: &Arc>, ) { - let block = || generate_block(app_data, area, gui_state, SelectablePanel::Commands); + let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands); let items = app_data.lock().get_control_items().map_or(vec![], |i| { i.iter() .map(|c| { @@ -106,7 +110,7 @@ pub fn commands( let items = List::new(items) .block(block()) .highlight_style(Style::default().add_modifier(Modifier::BOLD)) - .highlight_symbol(ARROW); + .highlight_symbol(RIGHT_ARROW); if let Some(i) = app_data.lock().get_control_state() { f.render_stateful_widget(items, area, i); @@ -118,88 +122,91 @@ pub fn commands( } } +/// Format the container data to display nicely on the screen +fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { + let state_style = Style::default().fg(i.state.get_color()); + let blue = Style::default().fg(Color::Blue); + + Line::from(vec![ + Span::styled( + format!( + "{:width$}", + i.status, + width = &widths.status.1.into() + ), + state_style, + ), + Span::styled( + format!( + "{}{:>width$}", + MARGIN, + i.cpu_stats.back().unwrap_or(&CpuStats::default()), + width = &widths.cpu.1.into() + ), + state_style, + ), + Span::styled( + format!( + "{MARGIN}{:>width_current$} / {:>width_limit$}", + i.mem_stats.back().unwrap_or(&ByteStats::default()), + i.mem_limit, + width_current = &widths.mem.1.into(), + width_limit = &widths.mem.2.into() + ), + state_style, + ), + Span::styled( + format!( + "{}{:>width$}", + MARGIN, + i.id.get_short(), + width = &widths.id.1.into() + ), + blue, + ), + Span::styled( + format!("{MARGIN}{:>width$}", i.name, width = widths.name.1.into()), + blue, + ), + Span::styled( + format!("{MARGIN}{:>width$}", i.image, width = widths.image.1.into()), + blue, + ), + Span::styled( + format!("{MARGIN}{:>width$}", i.rx, width = widths.net_rx.1.into()), + Style::default().fg(Color::Rgb(255, 233, 193)), + ), + Span::styled( + format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()), + Style::default().fg(Color::Rgb(205, 140, 140)), + ), + ]) +} + /// Draw the containers panel pub fn containers( app_data: &Arc>, area: Rect, f: &mut Frame, + fd: &FrameData, gui_state: &Arc>, widths: &Columns, ) { - let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers); + let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers); let items = app_data .lock() .get_container_items() .iter() - .map(|i| { - let state_style = Style::default().fg(i.state.get_color()); - let blue = Style::default().fg(Color::Blue); - - let lines = Line::from(vec![ - Span::styled( - format!( - "{:width$}", - i.status, - width = &widths.status.1.into() - ), - state_style, - ), - Span::styled( - format!( - "{}{:>width$}", - MARGIN, - i.cpu_stats.back().unwrap_or(&CpuStats::default()), - width = &widths.cpu.1.into() - ), - state_style, - ), - Span::styled( - format!( - "{MARGIN}{:>width_current$} / {:>width_limit$}", - i.mem_stats.back().unwrap_or(&ByteStats::default()), - i.mem_limit, - width_current = &widths.mem.1.into(), - width_limit = &widths.mem.2.into() - ), - state_style, - ), - Span::styled( - format!( - "{}{:>width$}", - MARGIN, - i.id.get().chars().take(8).collect::(), - width = &widths.id.1.into() - ), - blue, - ), - Span::styled( - format!("{MARGIN}{:>width$}", i.name, width = widths.name.1.into()), - blue, - ), - Span::styled( - format!("{MARGIN}{:>width$}", i.image, width = widths.image.1.into()), - blue, - ), - Span::styled( - format!("{MARGIN}{:>width$}", i.rx, width = widths.net_rx.1.into()), - Style::default().fg(Color::Rgb(255, 233, 193)), - ), - Span::styled( - format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()), - Style::default().fg(Color::Rgb(205, 140, 140)), - ), - ]); - ListItem::new(lines) - }) + .map(|i| ListItem::new(format_containers(i, widths))) .collect::>(); if items.is_empty() { @@ -212,7 +219,6 @@ pub fn containers( .block(block) .highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_symbol(CIRCLE); - f.render_stateful_widget(items, area, app_data.lock().get_container_state()); } } @@ -222,12 +228,12 @@ pub fn logs( app_data: &Arc>, area: Rect, f: &mut Frame, + fd: &FrameData, gui_state: &Arc>, - loading_icon: &str, ) { - 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}")) + let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Logs); + if fd.init { + let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon)) .style(Style::default()) .block(block()) .alignment(Alignment::Center); @@ -243,12 +249,11 @@ pub fn logs( } else { let items = List::new(logs) .block(block()) - .highlight_symbol(ARROW) + .highlight_symbol(RIGHT_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); + if let Some(log_state) = app_data.lock().get_log_state() { + f.render_stateful_widget(items, area, log_state); } } } @@ -338,24 +343,20 @@ fn make_chart<'a, T: Stats + Display>( #[allow(clippy::too_many_lines)] pub fn heading_bar( area: Rect, - columns: &Columns, - f: &mut Frame, - has_containers: bool, - loading_icon: &str, - sorted_by: Option<(Header, SortedOrder)>, + frame: &mut Frame, + data: &FrameData, gui_state: &Arc>, ) { let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg)); - let help_visible = gui_state.lock().status_contains(&[Status::Help]); - f.render_widget(block(Color::Black), area); + frame.render_widget(block(Color::Black), area); // Generate a block for the header, if the header is currently being used to sort a column, then highlight it white let header_block = |x: &Header| { let mut color = Color::Black; let mut suffix = ""; let mut suffix_margin = 0; - if let Some((a, b)) = sorted_by.as_ref() { + if let Some((a, b)) = data.sorted_by.as_ref() { if x == a { match b { SortedOrder::Asc => suffix = " ⌃", @@ -407,15 +408,15 @@ pub fn heading_bar( // Meta data to iterate over to create blocks with correct widths let header_meta = [ - (Header::State, columns.state.1), - (Header::Status, columns.status.1), - (Header::Cpu, columns.cpu.1), - (Header::Memory, columns.mem.1 + columns.mem.2 + 3), - (Header::Id, columns.id.1), - (Header::Name, columns.name.1), - (Header::Image, columns.image.1), - (Header::Rx, columns.net_rx.1), - (Header::Tx, columns.net_tx.1), + (Header::State, data.columns.state.1), + (Header::Status, data.columns.status.1), + (Header::Cpu, data.columns.cpu.1), + (Header::Memory, data.columns.mem.1 + data.columns.mem.2 + 3), + (Header::Id, data.columns.id.1), + (Header::Name, data.columns.name.1), + (Header::Image, data.columns.image.1), + (Header::Rx, data.columns.net_rx.1), + (Header::Tx, data.columns.net_tx.1), ]; let header_data = header_meta @@ -426,13 +427,13 @@ pub fn heading_bar( }) .collect::>(); - let suffix = if help_visible { "exit" } else { "show" }; + let suffix = if data.help_visible { "exit" } else { "show" }; let info_text = format!("( h ) {suffix} help {MARGIN}",); let info_width = info_text.chars().count(); let column_width = usize::from(area.width).saturating_sub(info_width); let column_width = if column_width > 0 { column_width } else { 1 }; - let splits = if has_containers { + let splits = if data.has_containers { vec![ Constraint::Min(2), Constraint::Min(column_width.try_into().unwrap_or_default()), @@ -446,13 +447,12 @@ pub fn heading_bar( .direction(Direction::Horizontal) .constraints(splits) .split(area); - if has_containers { + if data.has_containers { // Draw loading icon, or not, and a prefix with a single space - let loading_icon = format!("{loading_icon:>2}"); - let loading_paragraph = Paragraph::new(loading_icon) + let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon)) .block(block(Color::White)) .alignment(Alignment::Center); - f.render_widget(loading_paragraph, split_bar[0]); + frame.render_widget(loading_paragraph, split_bar[0]); let container_splits = header_data.iter().map(|i| i.2).collect::>(); let headers_section = Layout::default() @@ -466,12 +466,12 @@ pub fn heading_bar( gui_state .lock() .update_region_map(Region::Header(header), rect); - f.render_widget(paragraph, rect); + frame.render_widget(paragraph, rect); } } // show/hide help - let color = if help_visible { + let color = if data.help_visible { Color::Black } else { Color::White @@ -481,8 +481,8 @@ pub fn heading_bar( .alignment(Alignment::Right); // If no containers, don't display the headers, could maybe do this first? - let help_index = if has_containers { 2 } else { 0 }; - f.render_widget(help_paragraph, split_bar[help_index]); + let help_index = if data.has_containers { 2 } else { 0 }; + frame.render_widget(help_paragraph, split_bar[help_index]); } /// Help popup box needs these three pieces of information @@ -586,7 +586,7 @@ impl HelpInfo { button_item("enter"), button_desc("to send docker container command"), ]), - Line::from(vec![ + Line::from(vec![ space(), button_item("e"), button_desc("exec into a container"), @@ -876,13 +876,13 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) { } /// Draw info box in one of the 9 BoxLocations -pub fn info(f: &mut Frame, text: String) { +pub fn info(f: &mut Frame, text: &str) { let block = Block::default() .title("") .title_alignment(Alignment::Center) .borders(Borders::NONE); - let mut max_line_width = max_line_width(&text); + let mut max_line_width = max_line_width(text); let mut lines = text.lines().count(); // Add some horizontal & vertical margins @@ -927,6 +927,16 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio .split(popup_layout[indexes.0])[indexes.1] } +#[cfg(debug_assertions)] +// Single row at the top of the screen for debugging +pub fn debug_bar(area: Rect, f: &mut Frame, debug_string: &str) { + let block = Block::default().style(Style::default().bg(Color::Red)); + let paragraph = Paragraph::new(debug_string) + .style(Style::default().fg(Color::White)) + .block(block); + f.render_widget(paragraph, area); +} + // Draw nothing, as in a blank screen // pub fn nothing(f: &mut Frame) { // let whole_layout = Layout::default() diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 967aff9..1a6bb26 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -166,15 +166,15 @@ pub enum Status { /// Global gui_state, stored in an Arc #[derive(Debug, Default, Clone)] pub struct GuiState { + delete_container: Option, + delete_map: HashMap, heading_map: HashMap, is_loading: HashSet, loading_index: u8, panel_map: HashMap, - delete_map: HashMap, + selected_panel: SelectablePanel, status: HashSet, - delete_container: Option, pub info_box_text: Option, - pub selected_panel: SelectablePanel, } impl GuiState { /// Clear panels hash map, so on resize can fix the sizes for mouse clicks @@ -182,6 +182,11 @@ impl GuiState { self.panel_map.clear(); } + /// Get the currently selected panel + pub const fn get_selected_panel(&self) -> SelectablePanel { + self.selected_panel + } + /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels pub fn panel_intersect(&mut self, rect: Rect) { if let Some(data) = self @@ -293,7 +298,7 @@ impl GuiState { } /// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' ' - pub fn get_loading(&mut self) -> char { + pub fn get_loading(&self) -> char { if self.is_loading.is_empty() { ' ' } else { @@ -314,11 +319,11 @@ impl GuiState { gui_state: &Arc>, loading_uuid: Uuid, ) -> JoinHandle<()> { - gui_state.lock().next_loading(loading_uuid); - let gui_state = Arc::clone(gui_state); + gui_state.lock().next_loading(loading_uuid); + let gui_state = Arc::clone(gui_state); tokio::spawn(async move { loop { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; gui_state.lock().next_loading(loading_uuid); } }) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7d81228..3cc8248 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,7 +4,7 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use parking_lot::Mutex; +use parking_lot::{Mutex, MutexGuard}; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, @@ -26,16 +26,16 @@ mod gui_state; pub use self::color_match::*; pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ - app_data::AppData, app_error::AppError, docker_data::DockerMessage, - input_handler::InputMessages, parse_args::CliArgs, + app_data::{AppData, Columns, ContainerId, Header, SortedOrder}, + app_error::AppError, + input_handler::InputMessages, }; pub const DOCKER_COMMAND: &str = "docker"; pub struct Ui { - args: CliArgs, + // args: CliArgs, app_data: Arc>, - docker_sx: Sender, gui_state: Arc>, input_poll_rate: Duration, is_running: Arc, @@ -60,17 +60,14 @@ impl Ui { /// Create a new Ui struct, and execute the drawing loop pub async fn create( app_data: Arc>, - docker_sx: Sender, gui_state: Arc>, is_running: Arc, sender: Sender, ) { if let Ok(terminal) = Self::setup_terminal() { - let args = app_data.lock().args.clone(); + // let args = app_data.lock().args.clone(); let mut ui = Self { - args, app_data, - docker_sx, gui_state, input_poll_rate: std::time::Duration::from_millis(100), is_running, @@ -158,10 +155,8 @@ impl Ui { if child.kill().is_err() { std::process::exit(1) } - // } } } - self.terminal.clear().ok(); self.reset_terminal().ok(); Self::init_terminal().ok(); @@ -170,16 +165,10 @@ impl Ui { /// The loop for drawing the main UI to the terminal async fn gui_loop(&mut self) -> Result<(), AppError> { - let update_duration = - std::time::Duration::from_millis(u64::from(self.args.docker_interval)); - while self.is_running.load(Ordering::SeqCst) { let exec = self.gui_state.lock().status_contains(&[Status::Exec]); - if exec { self.exec(); - self.docker_sx.send(DockerMessage::Update).await.ok(); - continue; } if self @@ -212,12 +201,6 @@ impl Ui { } } } - - // Should this be done in the docker thread instead? - if self.now.elapsed() >= update_duration { - self.docker_sx.send(DockerMessage::Update).await.ok(); - self.now = Instant::now(); - } } Ok(()) } @@ -237,48 +220,94 @@ impl Ui { } } -#[macro_export] -/// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression. -macro_rules! value_capture { - ($name:ident, $lock_expr:expr) => { - let $name = || $lock_expr; - let $name = $name(); - }; +// #[macro_export] +// /// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression. +// macro_rules! value_capture { +// ($name:ident, $lock_expr:expr) => { +// let $name = || $lock_expr; +// let $name = $name(); +// }; +// } + +#[cfg(not(debug_assertions))] +fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> { + Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref()) + .split(f.size()) +} + +#[cfg(debug_assertions)] +fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> { + Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Min(1), Constraint::Min(100)].as_ref()) + .split(f.size()) +} + +/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here +#[derive(Debug)] +pub struct FrameData { + columns: Columns, + delete_confirm: Option, + has_containers: bool, + has_error: Option, + height: u16, + help_visible: bool, + init: bool, + info_text: Option, + loading_icon: String, + selected_panel: SelectablePanel, + sorted_by: Option<(Header, SortedOrder)>, +} + +impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData { + fn from(data: (MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)) -> Self { + // set max height for container section, needs +5 to deal with docker commands list and borders + let height = data.0.get_container_len(); + let height = if height < 12 { + u16::try_from(height + 5).unwrap_or_default() + } else { + 12 + }; + + Self { + columns: data.0.get_width(), + delete_confirm: data.1.get_delete_container(), + has_containers: data.0.get_container_len() > 1, + has_error: data.0.get_error(), + height, + help_visible: data.1.status_contains(&[Status::Help]), + init: data.1.status_contains(&[Status::Init]), + info_text: data.1.info_box_text.clone(), + loading_icon: data.1.get_loading().to_string(), + selected_panel: data.1.get_selected_panel(), + sorted_by: data.0.get_sorted(), + } + } } /// Draw the main ui to a frame of the terminal -/// TODO add a single line area for debug message - if not in release mode? fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) { - value_capture!(height, app_data.lock().get_container_len()); - value_capture!(column_widths, app_data.lock().get_width()); - value_capture!(has_containers, app_data.lock().get_container_len() > 0); - value_capture!(sorted_by, app_data.lock().get_sorted()); - value_capture!(delete_confirm, gui_state.lock().get_delete_container()); - value_capture!(has_error, app_data.lock().get_error()); - value_capture!(info_text, gui_state.lock().info_box_text.clone()); - value_capture!(loading_icon, gui_state.lock().get_loading().to_string()); + let fd = FrameData::from((app_data.lock(), gui_state.lock())); - // set max height for container section, needs +5 to deal with docker commands list and borders - let height = if height < 12 { height + 5 } else { 12 }; + let whole_layout = get_wholelayout(f); + #[cfg(debug_assertions)] + draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string()); - let whole_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref()) - .split(f.size()); + #[cfg(debug_assertions)] + let whole_layout_split = (1, 2); + + #[cfg(not(debug_assertions))] + let whole_layout_split = (0, 1); // Split into 3, containers+controls, logs, then graphs let upper_main = Layout::default() .direction(Direction::Vertical) - .constraints( - [ - Constraint::Max(height.try_into().unwrap_or_default()), - Constraint::Percentage(50), - ] - .as_ref(), - ) - .split(whole_layout[1]); + .constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref()) + .split(whole_layout[whole_layout_split.1]); - let top_split = if has_containers { + let top_split = if fd.has_containers { vec![Constraint::Percentage(90), Constraint::Percentage(10)] } else { vec![Constraint::Percentage(100)] @@ -289,7 +318,7 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>, gui_state: &Arc>, gui_state: &Arc Date: Thu, 16 Nov 2023 11:11:34 +0000 Subject: [PATCH 09/40] chore: dependencies updated --- Cargo.lock | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24daaf0..44890b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,8 +146,7 @@ checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bollard" version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03db470b3c0213c47e978da93200259a1eb4dae2e5512cba9955e2b540a6fc6" +source = "git+https://www.github.com/fussybeaver/bollard.git?rev=cef1cd5#cef1cd568684d0c3c497ce56221ff22ca18334f4" dependencies = [ "base64", "bollard-stubs", @@ -175,8 +174,7 @@ dependencies = [ [[package]] name = "bollard-stubs" version = "1.43.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58071e8fd9ec1e930efd28e3a90c1251015872a2ce49f81f36421b86466932e" +source = "git+https://www.github.com/fussybeaver/bollard.git?rev=cef1cd5#cef1cd568684d0c3c497ce56221ff22ca18334f4" dependencies = [ "serde", "serde_repr", From 6109bef5710d60bdef2e06ff4165ed2210ea09a8 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:32:31 +0000 Subject: [PATCH 10/40] docs: changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1481fbc..f20fb6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +### Chores ++ workflow dependencies updated, [6a4cf6490d08b976734e2bc8186d94c095700558] ++ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53] + +### Features ++ Docker exec mode close #28 [c8077bca0b673478cfbb417e677a885136ba9eff] + +### Fixes ++ `as_ref()` fixed, #31 merged, thanks [Daniel-Boll](https://github.com/Daniel-Boll) [0e06c9c172629dc7f7e7766f5372da9466e786d8] + + # v0.3.3 ### 2023-10-21 From 1cdcbf6a7f795c1808702c98fd4ea091daeac198 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:35:08 +0000 Subject: [PATCH 11/40] docs: changelog --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f20fb6d..df2c738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,10 @@ + dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53] ### Features -+ Docker exec mode close #28 [c8077bca0b673478cfbb417e677a885136ba9eff] ++ Docker exec mode, closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff] ### Fixes -+ `as_ref()` fixed, #31 merged, thanks [Daniel-Boll](https://github.com/Daniel-Boll) [0e06c9c172629dc7f7e7766f5372da9466e786d8] - ++ `as_ref()` fixed, #31 merged, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8] # v0.3.3 ### 2023-10-21 From aafe89d0eb1059c4721aabdb73b76af0d0bcce14 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:40:01 +0000 Subject: [PATCH 12/40] docs: changelog --- CHANGELOG.md | 5 +++++ src/main.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df2c738..9a8d46c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ ### Features + Docker exec mode, closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff] +You are now able to attempt to exec into a container by pressing the `e` key. This just pipes `docker exec -it [id] sh` into the oxker view. +As such, the DockerImage now needs docker installed, so the runtime step has switched from scratch to Alpine. Using a multistage build has reduced the size, but the docker image +has now grown from ~1.5mb to ~11.5mb. It is possible to use the Rust [bollard](https://github.com/fussybeaver/bollard) library to enable this functionality in *pure* Rust, +but so far there are multiple issues with this approach - see the feat/tty branch for the current attempt. + ### Fixes + `as_ref()` fixed, #31 merged, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8] diff --git a/src/main.rs b/src/main.rs index b2387cb..13cf23d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -144,7 +144,7 @@ async fn main() { Ui::create(app_data, gui_state, is_running, input_sx).await; } else { info!("in debug mode\n"); - // Debug mode for testing, less pointless now, will diplay some basic information + // Debug mode for testing, less pointless now, will display some basic information while is_running.load(Ordering::SeqCst) { loop { if let Some(err) = app_data.lock().get_error() { From 914ff93dd3d49625736dafcee2c41d2cafcca16f Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:03:00 +0000 Subject: [PATCH 13/40] chore: devcontainer update --- .devcontainer/Dockerfile | 4 +--- .devcontainer/devcontainer.json | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 4893600..18c6da1 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,6 +9,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends build-essential pkg-config libssl-dev USER vscode -RUN curl --proto '=https' --tlsv1.2 -sSf curl https://sh.rustup.rs | sh -s -- -y -# RUN rustup target add x86_64-unknown-linux-musl - +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 502db66..4ab9ab4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,7 +16,7 @@ "seccomp=unconfined" ], - "postCreateCommand": "rustup target add x86_64-unknown-linux-musl && cargo install cross typos-cli", + "postCreateCommand": "rustup target add x86_64-unknown-linux-musl && cargo install cross typos-cli cargo-expand", "mounts": [ "source=/etc/timezone,target=/etc/timezone,type=bind,readonly" From d08cbb66404755a4bed6908bf855958cc7a7d1ee Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:03:11 +0000 Subject: [PATCH 14/40] refactor: remove redundant loop --- src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 13cf23d..a1b68c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -146,7 +146,6 @@ async fn main() { info!("in debug mode\n"); // Debug mode for testing, less pointless now, will display some basic information while is_running.load(Ordering::SeqCst) { - loop { if let Some(err) = app_data.lock().get_error() { error!("{}", err); process::exit(1); @@ -169,7 +168,6 @@ async fn main() { } println!(); } - } } } } From 28b0315dd7252cf77bf519646b97e4f1438214a5 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:03:56 +0000 Subject: [PATCH 15/40] docs: changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8d46c..4a4cad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ has now grown from ~1.5mb to ~11.5mb. It is possible to use the Rust [bollard](h but so far there are multiple issues with this approach - see the feat/tty branch for the current attempt. ### Fixes -+ `as_ref()` fixed, #31 merged, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8] ++ `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8] # v0.3.3 ### 2023-10-21 From 0e5ee143b008c9d0ee0b681231a1568be227150b Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 17 Nov 2023 02:49:30 +0000 Subject: [PATCH 16/40] feat: exec mode improvements Use Bollard library to exec in pure Rust. `--use-cli` cli arg, will then only try to exec into containers using Docker. Only try to exec into a container if the state == Running. --- .../workflows/create_release_and_build.yml | 2 + README.md | 15 +- containerised/Dockerfile | 19 +- containerised/Dockerfile_dev | 26 +- src/app_data/mod.rs | 8 +- src/app_error.rs | 2 +- src/docker_data/message.rs | 11 +- src/docker_data/mod.rs | 4 + src/exec.rs | 319 ++++++++++++++++++ src/input_handler/mod.rs | 52 ++- src/main.rs | 49 +-- src/parse_args.rs | 16 +- src/ui/draw_blocks.rs | 5 +- src/ui/gui_state.rs | 37 +- src/ui/mod.rs | 54 ++- 15 files changed, 477 insertions(+), 142 deletions(-) create mode 100644 src/exec.rs diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml index d861736..6acc896 100644 --- a/.github/workflows/create_release_and_build.yml +++ b/.github/workflows/create_release_and_build.yml @@ -87,6 +87,7 @@ jobs: artifacts: | **/oxker_*.zip **/oxker_*.tar.gz + ######################### ## Publish to crates.io # ######################### @@ -106,6 +107,7 @@ jobs: ######################################### ## Build images for Dockerhub & ghcr.io # ######################################### + image_build: needs: [cargo_publish] runs-on: ubuntu-latest diff --git a/README.md b/README.md index aaffc2e..fd6f0d5 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,14 @@ In application controls Available command line arguments | argument|result| |--|--| -|```-d [number > 0]```| set the minimum update interval for docker information, in ms, defaults to 1000 (1 second) | -|```--host [hostname]```| connect to Docker with a custom hostname, defaults to `/var/run/docker.sock`, will use `$DOCKER_HOST` env if set | -|```-r```| show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) | -|```-c```| attempt to color the logs (conflicts with -r) | -|```-t```| remove timestamps from each log entry | -|```-s```| if running via docker, will show the oxker container | -|```-g```| no tui, basically a pointless debugging mode, for now | +|```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. Defaults to 1000 (1 second).| +|```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| +|```--use-cli```| When executing into a container, use the external Docker CLI application.| +|```-r```| Show raw logs. By default, removes ANSI formatting (conflicts with `-c`).| +|```-c```| Attempt to color the logs (conflicts with `-r`).| +|```-t```| Remove timestamps from each log entry.| +|```-s```| If running via Docker, will display the oxker container.| +|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.| ## Build step diff --git a/containerised/Dockerfile b/containerised/Dockerfile index 99242b6..7c12c96 100644 --- a/containerised/Dockerfile +++ b/containerised/Dockerfile @@ -45,32 +45,17 @@ RUN cargo build --release --target $(cat /.platform) RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker / -################ -## MUSL SETUP ## -################ - -FROM alpine:3.18 as MUSL_SETUP - -RUN apk add --update --no-cache docker-cli upx - -# Compress the docker executable, to reduce final image size -RUN upx -9 /usr/bin/docker - ############# ## Runtime ## ############# -FROM alpine:3.18 as RUNTIME +FROM scratch as RUNTIME # Set an ENV to indicate that we're running in a container ENV OXKER_RUNTIME=container COPY --from=BUILDER /oxker /app/ -COPY --from=MUSL_SETUP /usr/bin/docker /usr/bin/ - -# remove sh and busybox, probably pointless -RUN rm /bin/sh /bin/busybox # Run the application # this is used in the application itself so DO NOT EDIT -ENTRYPOINT [ "/app/oxker"] +ENTRYPOINT [ "/app/oxker"] \ No newline at end of file diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev index 0a3118c..7984af8 100644 --- a/containerised/Dockerfile_dev +++ b/containerised/Dockerfile_dev @@ -1,28 +1,12 @@ -################ -## MUSL SETUP ## -################ - -FROM alpine:3.18 as MUSL_SETUP - -RUN apk add --update --no-cache docker-cli upx - -# Copy application binary from builder image -RUN upx -9 /usr/bin/docker - ############# ## Runtime ## ############# +FROM scratch -FROM alpine:3.18 as RUNTIME - -# Set an ENV that we're running in a container, so that the application can sleep for 250ms at start +# Set env that we're running in a container, so that the application can sleep for 250ms at start ENV OXKER_RUNTIME=container -COPY --from=MUSL_SETUP /usr/bin/docker /usr/bin/ - - -RUN rm /bin/sh /bin/busybox - +# Copy application binary from builder image COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/ # Run the application @@ -44,3 +28,7 @@ ENTRYPOINT [ "/app/oxker"] # Buildx command to build musl version for all three platforms, should probably be executed in create_release # docker buildx create --use # docker buildx build --platform linux/arm/v6,linux/arm64,linux/amd64 -t oxker_dev_all -o type=tar,dest=/tmp/oxker_dev_all.tar -f containerised/Dockerfile . + + +# Build production version for x86 only, then run +# docker build --platform linux/amd64 -t oxker_dev -f containerised/Dockerfile . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 3ad7a44..88f93c1 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -83,7 +83,7 @@ impl AppData { #[cfg(debug_assertions)] #[allow(unused)] - pub fn set_debug_string(&mut self, x: &str) { + pub fn push_debug_string(&mut self, x: &str) { self.debug_string.push_str(x); } @@ -506,6 +506,12 @@ impl AppData { self.get_selected_container().map(|i| i.id.clone()) } + /// Get the Id and State for the currently selected container - used by the exec check method + pub fn get_selected_container_id_state(&self) -> Option<(ContainerId, State)> { + self.get_selected_container() + .map(|i| (i.id.clone(), i.state)) + } + /// 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( diff --git a/src/app_error.rs b/src/app_error.rs index 5a9a9aa..e001645 100644 --- a/src/app_error.rs +++ b/src/app_error.rs @@ -27,7 +27,7 @@ impl fmt::Display for AppError { let reason = if *x { "en" } else { "dis" }; write!(f, "Unable to {reason}able mouse capture") } - Self::Terminal => write!(f, "Unable to draw to terminal"), + Self::Terminal => write!(f, "Unable to fully render to terminal"), } } } diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs index 0a6b67e..e1066c1 100644 --- a/src/docker_data/message.rs +++ b/src/docker_data/message.rs @@ -1,9 +1,14 @@ -use crate::app_data::ContainerId; +use std::sync::Arc; -#[derive(Debug, Clone)] +use crate::app_data::ContainerId; +use bollard::Docker; +use tokio::sync::oneshot::Sender; + +#[derive(Debug)] pub enum DockerMessage { - Delete(ContainerId), ConfirmDelete(ContainerId), + Delete(ContainerId), + Exec(Sender>), Pause(ContainerId), Quit, Restart(ContainerId), diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 41de793..2817930 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -330,6 +330,7 @@ impl DockerData { /// Handle incoming messages, container controls & all container information update /// Spawn Docker commands off into own thread + #[allow(clippy::too_many_lines)] async fn message_handler(&mut self) { while let Some(message) = self.receiver.recv().await { let docker = Arc::clone(&self.docker); @@ -338,6 +339,9 @@ impl DockerData { let uuid = Uuid::new_v4(); // TODO need to refactor these match message { + DockerMessage::Exec(sender) => { + sender.send(Arc::clone(&self.docker)).ok(); + } DockerMessage::Pause(id) => { tokio::spawn(async move { let handle = GuiState::start_loading_animation(&gui_state, uuid); diff --git a/src/exec.rs b/src/exec.rs new file mode 100644 index 0000000..db37bff --- /dev/null +++ b/src/exec.rs @@ -0,0 +1,319 @@ +use std::{ + fmt, + hash::{Hash, Hasher}, + io::{Read, Write}, + sync::{atomic::AtomicBool, Arc}, +}; + +use bollard::{ + container, + exec::{CreateExecOptions, StartExecOptions, StartExecResults}, + Docker, +}; +use crossterm::terminal::enable_raw_mode; +use futures_util::StreamExt; +use parking_lot::Mutex; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::{ + app_data::{AppData, ContainerId, State}, + app_error::AppError, + parse_args::CliArgs, + ui::{GuiState, Status}, +}; + +/// TTY location +const TTY: &str = "/dev/tty"; + +/// This will be the start of a docker exec emssage if one is unable to actually exec into the container +const OCI_ERROR: &str = "OCI runtime exec failed"; + +/// Set the cursor position on the screen to (0,0) +pub const CURSOR_POS: &str = "\x1B[J\x1B[H"; + +/// This needs to be written to stdout when exiting the exec mode, else the input handler thread gets confused, +/// see https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +const KEYBOARD_PROTO: &str = "\x1B[?u\x1B[c"; + +mod command { + pub const PWD: &str = "pwd"; + pub const DOCKER: &str = "docker"; + pub const EXEC: &str = "exec"; + pub const SH: &str = "sh"; + pub const IT: &str = "-it"; +} + +/// Currently known byte output after writing KEYBOARD_PROTO to stdout +/// valid arm: [91, 63, 54, 49, 59, 54, 59, 55, 59, 50, 50, 59, 50, 51, 59, 50, 52, 59, 50, 56, 59, 51, 50, 59,52, 50] => [?61;6;7;22;23;24;28;32;2 +/// valid x86: [91, 63, 49, 59, 50, 99] => [?1;2c +/// invalid x86: [91, 63, 49, 59, 48, 99] => [?1;0c +enum ByteOutput { + Arm, + X86, +} + +impl ByteOutput { + const fn len(&self) -> usize { + match self { + Self::Arm => 26, + Self::X86 => 6, + } + } + const fn last(&self) -> &[u8] { + match self { + Self::Arm => &[50], + Self::X86 => &[99], + } + } +} + +/// Check the output from tty to see if it matches known sequence. +/// At the moment we only need to check the length and end digit, as x86 valid and invalid match in these two regards +fn byte_sequence_valid(bytes: &[u8]) -> bool { + [ByteOutput::Arm, ByteOutput::X86] + .iter() + .any(|i| i.len() == bytes.len() && bytes.ends_with(i.last())) +} + +/// Check if tty is able to be written to, aka not windows +pub fn tty_readable() -> bool { + std::fs::OpenOptions::new() + .read(true) + .write(false) + .open(TTY) + .is_ok() +} + +/// Async tty reading, spawned into its own tokio thread +fn tty(run: Arc) -> Option { + if tty_readable() { + let (tx, rx) = std::sync::mpsc::channel(); + tokio::spawn(async move { + if let Ok(mut f) = tokio::fs::File::open(TTY).await { + while run.load(std::sync::atomic::Ordering::SeqCst) { + let mut buf = [0]; + if tokio::time::timeout( + std::time::Duration::from_millis(10), + f.read_exact(&mut buf), + ) + .await + .is_ok() + && tx.send(buf[0]).is_err() + { + run.store(false, std::sync::atomic::Ordering::SeqCst); + } + } + } + }); + Some(AsyncTTY { rx }) + } else { + None + } +} + +struct AsyncTTY { + rx: std::sync::mpsc::Receiver, +} + +#[derive(Debug, Clone)] +pub enum ExecMode { + // use Bollard Rust library + Internal((ContainerId, Arc)), + // use the external `docker-cli` + External(ContainerId), +} + +impl ExecMode { + /// Test if we can exec into the selected container, first via the Internal methods, then by the External + /// If the container is oxker, it will always return None + pub async fn new(app_data: &Arc>, docker: &Arc) -> Option { + let is_oxker = app_data.lock().is_oxker(); + if is_oxker { + return None; + } + + let use_cli = app_data.lock().args.use_cli; + let container = app_data.lock().get_selected_container_id_state(); + + if let Some((id, state)) = container { + if state == State::Running { + if tty_readable() && !use_cli { + if let Ok(exec) = docker + .create_exec( + id.get(), + CreateExecOptions { + attach_stdout: Some(true), + attach_stderr: Some(true), + cmd: Some(vec![command::PWD]), + ..Default::default() + }, + ) + .await + { + if let Ok(StartExecResults::Attached { mut output, .. }) = + docker.start_exec(&exec.id, None).await + { + if let Some(Ok(msg)) = output.next().await { + if !msg.to_string().starts_with(OCI_ERROR) { + return Some(Self::Internal((id.clone(), Arc::clone(docker)))); + } + } + } + } + } + + if let Ok(output) = std::process::Command::new(command::DOCKER) + .args([command::EXEC, id.get(), command::PWD]) + .output() + { + if let Ok(output) = String::from_utf8(output.stdout) { + if !output.starts_with(OCI_ERROR) { + return Some(Self::External(id.clone())); + } + } + } + } + } + None + } + + /// exec into the container using the external docker cli, the result it just piped into oxker + fn exec_external(id: &ContainerId) { + let mut stdout = std::io::stdout(); + stdout.write_all(CURSOR_POS.as_bytes()).ok(); + if let Ok(mut child) = std::process::Command::new(command::DOCKER) + .args([command::EXEC, command::IT, id.get(), command::SH]) + .stdin(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .spawn() + { + child.wait().ok(); + if child.kill().is_err() { + std::process::exit(1) + } + } + } + + /// Exec into the container via the Bollard library, stdout & stdin on different threads + /// Have to deal with strange output once dropped, hence the use of internal_cleanup() method + async fn exec_internal(&self, id: &ContainerId, docker: &Arc) -> Result<(), AppError> { + let run = Arc::new(AtomicBool::new(true)); + + if let Ok(exec_result) = docker + .create_exec( + id.get(), + CreateExecOptions { + attach_stdout: Some(true), + attach_stderr: Some(false), + attach_stdin: Some(true), + tty: Some(true), + cmd: Some(vec![command::SH]), + ..Default::default() + }, + ) + .await + { + if let Ok(StartExecResults::Attached { + mut output, + mut input, + }) = docker + .start_exec( + &exec_result.id, + Some(StartExecOptions { + detach: false, + ..Default::default() + }), + ) + .await + { + if let Some(async_tty) = tty(Arc::clone(&run)) { + let run_thread = Arc::clone(&run); + tokio::spawn(async move { + enable_raw_mode().ok(); + let mut stdout = std::io::stdout(); + stdout.write_all(CURSOR_POS.as_bytes()).ok(); + stdout.flush().ok(); + + while run_thread.load(std::sync::atomic::Ordering::SeqCst) { + while let Some(Ok(x)) = output.next().await { + stdout.write_all(&x.into_bytes()).ok(); + stdout.flush().ok(); + } + run_thread.store(false, std::sync::atomic::Ordering::SeqCst); + } + }); + + while let Ok(x) = async_tty.rx.recv() { + input.write(&[x]).await.ok(); + } + + self.internal_cleanup()?; + } + } else { + return Err(AppError::Terminal); + } + } + Ok(()) + } + + // This is the fix for key pressed not being handled correctly on quit + // It writes a special message to the stdout, and then listens out for a valid response + // afterwhich it's assumes that we're completely done with TTY + fn internal_cleanup(&self) -> Result<(), AppError> { + match self { + Self::External(_) => Ok(()), + Self::Internal(_) => { + let waiting = Arc::new(AtomicBool::new(true)); + let waiting_thread = Arc::clone(&waiting); + + std::thread::spawn(move || { + // At the moment the known max length is 26 + let mut bytes = Vec::with_capacity(26); + while waiting_thread.load(std::sync::atomic::Ordering::SeqCst) { + let mut buf = [0]; + if let Ok(mut f) = std::fs::File::open(TTY) { + if f.read_exact(&mut buf).is_err() { + waiting_thread.store(false, std::sync::atomic::Ordering::SeqCst); + } + bytes.push(buf[0]); + if byte_sequence_valid(&bytes) { + waiting_thread.store(false, std::sync::atomic::Ordering::SeqCst); + } + }; + } + }); + + let mut stdout = std::io::stdout(); + stdout.write_all(KEYBOARD_PROTO.as_bytes()).ok(); + stdout.flush().ok(); + + let start = std::time::Instant::now(); + while waiting.load(std::sync::atomic::Ordering::SeqCst) { + if start.elapsed().as_millis() > 1500 { + waiting.store(false, std::sync::atomic::Ordering::SeqCst); + return Err(AppError::Terminal); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + Ok(()) + } + } + } + + // RESET TERMINAL BEFROEHAND + pub async fn run( + &self, + app_data: &Arc>, + gui_state: &Arc>, + ) -> Result<(), AppError> { + match self { + Self::External(id) => { + Self::exec_external(id); + Ok(()) + } + + Self::Internal((id, docker)) => self.exec_internal(id, docker).await, + } + } +} diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 0f8cb91..aaf812d 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -3,6 +3,7 @@ use std::sync::{ Arc, }; +use bollard::Docker; use crossterm::{ event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}, execute, @@ -20,12 +21,11 @@ use crate::{ app_data::{AppData, DockerControls, Header}, app_error::AppError, docker_data::DockerMessage, - ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui, DOCKER_COMMAND}, + exec::{tty_readable, ExecMode}, + ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui}, }; pub use message::InputMessages; -const OCI_ERROR: &str = "OCI runtime exec failed"; - /// Handle all input events #[derive(Debug)] pub struct InputHandler { @@ -164,36 +164,28 @@ impl InputHandler { } /// Validate that one can exec into a Docker container - fn e_key(&self) { + async fn e_key(&self) { let is_oxker = self.app_data.lock().is_oxker(); - if !is_oxker { + let mut exec_err = Some(()); + if !is_oxker && tty_readable() { let uuid = Uuid::new_v4(); let handle = GuiState::start_loading_animation(&self.gui_state, uuid); - let mut exec_err = Some(()); + let (sx, rx) = tokio::sync::oneshot::channel::>(); + self.docker_sender.send(DockerMessage::Exec(sx)).await.ok(); - let id = self.app_data.lock().get_selected_container_id(); - - if let Some(id) = id { - if let Ok(output) = std::process::Command::new(DOCKER_COMMAND) - .args(["exec", id.get(), "pwd"]) - .output() - { - if let Ok(output) = String::from_utf8(output.stdout) { - if !output.starts_with(OCI_ERROR) { - exec_err = None; - } - } - } - - if exec_err.is_some() { - self.app_data.lock().set_error( - AppError::DockerExec, - &self.gui_state, - Status::Error, - ); - } else { - self.gui_state.lock().status_push(Status::Exec); - } + if let Ok(docker) = rx.await { + (ExecMode::new(&self.app_data, &docker).await).map_or_else( + || { + self.app_data.lock().set_error( + AppError::DockerExec, + &self.gui_state, + Status::Error, + ); + }, + |mode| { + self.gui_state.lock().set_exec_mode(mode); + }, + ); } self.gui_state.lock().stop_loading_animation(&handle, uuid); } @@ -251,7 +243,7 @@ impl InputHandler { KeyCode::Char('7') => self.sort(Header::Image), KeyCode::Char('8') => self.sort(Header::Rx), KeyCode::Char('9') => self.sort(Header::Tx), - KeyCode::Char('e' | 'E') => self.e_key(), + KeyCode::Char('e' | 'E') => self.e_key().await, KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), KeyCode::Char('m' | 'M') => self.m_key(), KeyCode::Tab => { diff --git a/src/main.rs b/src/main.rs index a1b68c8..059076c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ clippy::similar_names )] // Only allow when debugging -// #![allow(unused)] +#![allow(unused)] use app_data::AppData; use app_error::AppError; @@ -35,6 +35,7 @@ use tracing::{error, info, Level}; mod app_data; mod app_error; mod docker_data; +mod exec; mod input_handler; mod parse_args; mod ui; @@ -121,6 +122,10 @@ async fn main() { setup_tracing(); let args = CliArgs::new(); + + if args.in_container { + std::thread::sleep(std::time::Duration::from_millis(250)); + } let host = read_docker_host(&args); let app_data = Arc::new(Mutex::new(AppData::default(args.clone()))); @@ -141,33 +146,33 @@ async fn main() { if args.gui { let (input_sx, input_rx) = tokio::sync::mpsc::channel(32); handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running); - Ui::create(app_data, gui_state, is_running, input_sx).await; + Ui::create(app_data, docker_tx.clone(), gui_state, is_running, input_sx).await; } else { info!("in debug mode\n"); // Debug mode for testing, less pointless now, will display some basic information while is_running.load(Ordering::SeqCst) { - if let Some(err) = app_data.lock().get_error() { - error!("{}", err); - process::exit(1); - } - tokio::time::sleep(std::time::Duration::from_millis(u64::from( - args.docker_interval, - ))) - .await; - let containers = app_data - .lock() - .get_container_items() - .clone() - .iter() - .map(|i| format!("{i}")) - .collect::>(); + if let Some(err) = app_data.lock().get_error() { + error!("{}", err); + process::exit(1); + } + tokio::time::sleep(std::time::Duration::from_millis(u64::from( + args.docker_interval, + ))) + .await; + let containers = app_data + .lock() + .get_container_items() + .clone() + .iter() + .map(|i| format!("{i}")) + .collect::>(); - if !containers.is_empty() { - for item in containers { - info!("{item}"); - } - println!(); + if !containers.is_empty() { + for item in containers { + info!("{item}"); } + println!(); + } } } } diff --git a/src/parse_args.rs b/src/parse_args.rs index dc4f8a8..0b4e96d 100644 --- a/src/parse_args.rs +++ b/src/parse_args.rs @@ -21,10 +21,6 @@ pub struct Args { #[clap(short = 'c', conflicts_with = "raw")] pub color: bool, - /// Docker host, defaults to `/var/run/docker.sock` - #[clap(long, short = None)] - pub host: Option, - /// Show raw logs, default is to remove ansi formatting, conflicts with "-c" #[clap(short = 'r', conflicts_with = "color")] pub raw: bool, @@ -36,16 +32,25 @@ pub struct Args { /// Don't draw gui - for debugging - mostly pointless #[clap(short = 'g')] pub gui: bool, + + /// Docker host, defaults to `/var/run/docker.sock` + #[clap(long, short = None)] + pub host: Option, + + /// Use "docker" cli for execing + #[clap(long="use-cli", short = None)] + pub use_cli: bool, } #[derive(Debug, Clone)] #[allow(clippy::struct_excessive_bools)] pub struct CliArgs { - pub in_container: bool, pub color: bool, pub docker_interval: u32, + pub use_cli: bool, pub gui: bool, pub host: Option, + pub in_container: bool, pub raw: bool, pub show_self: bool, pub timestamp: bool, @@ -76,6 +81,7 @@ impl CliArgs { Self { color: args.color, docker_interval: args.docker_interval, + use_cli: args.use_cli, gui: !args.gui, host: args.host, in_container: Self::check_if_in_container(), diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 9fe3cf0..c6b3359 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -279,6 +279,7 @@ pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc>) { .data(&mem.0)]; let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1)); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] 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); @@ -359,8 +360,8 @@ pub fn heading_bar( if let Some((a, b)) = data.sorted_by.as_ref() { if x == a { match b { - SortedOrder::Asc => suffix = " ⌃", - SortedOrder::Desc => suffix = " ⌄", + SortedOrder::Asc => suffix = " â–²", + SortedOrder::Desc => suffix = " â–¼", } suffix_margin = 2; color = Color::White; diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 1a6bb26..a7240f2 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -7,7 +7,10 @@ use std::{ use tokio::task::JoinHandle; use uuid::Uuid; -use crate::app_data::{ContainerId, Header}; +use crate::{ + app_data::{ContainerId, Header}, + exec::ExecMode, +}; #[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] pub enum SelectablePanel { @@ -174,6 +177,7 @@ pub struct GuiState { panel_map: HashMap, selected_panel: SelectablePanel, status: HashSet, + exec_mode: Option, pub info_box_text: Option, } impl GuiState { @@ -265,16 +269,41 @@ impl GuiState { } /// Remove a gui_status into the current gui_status HashSet + /// Remove exec mode & deleteConfirm is required pub fn status_del(&mut self, status: Status) { self.status.remove(&status); - if status == Status::DeleteConfirm { - self.status.remove(&Status::DeleteConfirm); + match status { + Status::DeleteConfirm => { + self.status.remove(&Status::DeleteConfirm); + } + Status::Exec => { + self.exec_mode = None; + } + _ => (), } } + /// Inset the ExecMode into self, and set the Status as exec + /// Using StatusPush with Status::Exec won't insert into the hash map + /// To force self.exec_mode to be set + pub fn set_exec_mode(&mut self, mode: ExecMode) { + self.exec_mode = Some(mode); + self.status.insert(Status::Exec); + } + + pub fn get_exec_mode(&mut self) -> Option { + self.exec_mode.clone() + } + /// Insert a gui_status into the current gui_status HashSet + /// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead pub fn status_push(&mut self, status: Status) { - self.status.insert(status); + match status { + Status::Exec => (), + _ => { + self.status.insert(status); + } + } } /// Change to next selectable panel diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3cc8248..97f0225 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use bollard::Docker; use crossterm::{ event::{self, DisableMouseCapture, Event}, execute, @@ -28,20 +29,20 @@ pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ app_data::{AppData, Columns, ContainerId, Header, SortedOrder}, app_error::AppError, + docker_data::DockerMessage, input_handler::InputMessages, }; -pub const DOCKER_COMMAND: &str = "docker"; - pub struct Ui { - // args: CliArgs, app_data: Arc>, + docker_sx: Sender, gui_state: Arc>, input_poll_rate: Duration, is_running: Arc, now: Instant, sender: Sender, terminal: Terminal>, + cursor_position: (u16, u16), } impl Ui { @@ -60,20 +61,24 @@ impl Ui { /// Create a new Ui struct, and execute the drawing loop pub async fn create( app_data: Arc>, + docker_sx: Sender, gui_state: Arc>, is_running: Arc, sender: Sender, ) { - if let Ok(terminal) = Self::setup_terminal() { + if let Ok(mut terminal) = Self::setup_terminal() { // let args = app_data.lock().args.clone(); + let cursor_position = terminal.get_cursor().unwrap_or_default(); let mut ui = Self { app_data, + docker_sx, gui_state, input_poll_rate: std::time::Duration::from_millis(100), is_running, now: Instant::now(), sender, terminal, + cursor_position, }; if let Err(e) = ui.draw_ui().await { error!("{e}"); @@ -111,6 +116,9 @@ impl Ui { DisableMouseCapture )?; disable_raw_mode()?; + self.terminal.clear().ok(); + self.terminal + .set_cursor(self.cursor_position.0, self.cursor_position.1)?; Ok(self.terminal.show_cursor()?) } @@ -138,24 +146,17 @@ impl Ui { } /// Use exeternal docker cli to exec into a container - fn exec(&mut self) { - let id = self.app_data.lock().get_selected_container_id(); + async fn exec(&mut self) { + let mut exec_mode = self.gui_state.lock().get_exec_mode(); - if let Some(id) = id { - // if Self::can_exec(&id).is_some() { - if let Ok(mut child) = std::process::Command::new(DOCKER_COMMAND) - .args(["exec", "-it", id.get(), "sh"]) - .stdin(std::process::Stdio::inherit()) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::inherit()) - .spawn() - { - self.reset_terminal().ok(); - child.wait().ok(); - if child.kill().is_err() { - std::process::exit(1) - } - } + if let Some(mode) = exec_mode { + self.reset_terminal().ok(); + self.terminal.clear().ok(); + if let Err(e) = mode.run(&self.app_data, &self.gui_state).await { + self.app_data + .lock() + .set_error(e, &self.gui_state, Status::Error); + }; } self.terminal.clear().ok(); self.reset_terminal().ok(); @@ -168,7 +169,7 @@ impl Ui { while self.is_running.load(Ordering::SeqCst) { let exec = self.gui_state.lock().status_contains(&[Status::Exec]); if exec { - self.exec(); + self.exec().await; } if self @@ -220,15 +221,6 @@ impl Ui { } } -// #[macro_export] -// /// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression. -// macro_rules! value_capture { -// ($name:ident, $lock_expr:expr) => { -// let $name = || $lock_expr; -// let $name = $name(); -// }; -// } - #[cfg(not(debug_assertions))] fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> { Layout::default() From 4b06117176cbd8906a9231c1e26fd7dd6f7f570e Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 18 Nov 2023 22:46:41 +0000 Subject: [PATCH 17/40] docs: changelog --- CHANGELOG.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a4cad0..d3d64b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,7 @@ + dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53] ### Features -+ Docker exec mode, closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff] - -You are now able to attempt to exec into a container by pressing the `e` key. This just pipes `docker exec -it [id] sh` into the oxker view. -As such, the DockerImage now needs docker installed, so the runtime step has switched from scratch to Alpine. Using a multistage build has reduced the size, but the docker image -has now grown from ~1.5mb to ~11.5mb. It is possible to use the Rust [bollard](https://github.com/fussybeaver/bollard) library to enable this functionality in *pure* Rust, -but so far there are multiple issues with this approach - see the feat/tty branch for the current attempt. ++ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key - closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff], [0e5ee143b008c9d0ee0b681231a1568be227150b] ### Fixes + `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8] From 81d5b326db8881263f2c9072e1426948e41b4a0f Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 18 Nov 2023 22:47:30 +0000 Subject: [PATCH 18/40] chore: dependencies updated --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 1 - 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44890b5..2dacf6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,9 +207,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.84" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -420,9 +420,9 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -430,7 +430,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -1556,18 +1556,18 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "zerocopy" -version = "0.7.25" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.25" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1fd235d..4f8c703 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ categories = ["command-line-utilities"] anyhow = "1.0" # bollard = "0.15" bollard = { git = "https://www.github.com/fussybeaver/bollard.git", rev = "cef1cd5" } - cansi = "2.2" clap = { version = "4.4", features = ["derive", "unicode", "color"] } crossterm = "0.27" From 0e9b65f6c543a2416c1c92b101e77a245aa631f4 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 18 Nov 2023 22:48:55 +0000 Subject: [PATCH 19/40] docs: changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d64b0..6677361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ### Chores + workflow dependencies updated, [6a4cf6490d08b976734e2bc8186d94c095700558] -+ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53] ++ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53], [81d5b326db8881263f2c9072e1426948e41b4a0f] ### Features + Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key - closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff], [0e5ee143b008c9d0ee0b681231a1568be227150b] From 3a6489396e87702ce94b349a7f47028ece7922f6 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 19 Nov 2023 11:05:17 +0000 Subject: [PATCH 20/40] fix: sort created_at clash, closes #22 Additionally sort by name, so that if a clash of first comparison, the order will be consistent --- src/app_data/mod.rs | 135 ++++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 66 deletions(-) diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 88f93c1..7695058 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -156,78 +156,81 @@ impl AppData { self.sorted_by } - /// Sort the containers vec, based on a heading, either ascending or descending, + /// Sort the containers vec, based on a heading (and if clash, then by name), either ascending or descending, /// If not sort set, then sort by created time pub fn sort_containers(&mut self) { if let Some((head, ord)) = self.sorted_by { - match head { - Header::State => match ord { - SortedOrder::Asc => self - .containers - .items - .sort_by(|a, b| b.state.order().cmp(&a.state.order())), - SortedOrder::Desc => self - .containers - .items - .sort_by(|a, b| a.state.order().cmp(&b.state.order())), - }, - Header::Status => match ord { - SortedOrder::Asc => self - .containers - .items - .sort_by(|a, b| a.status.cmp(&b.status)), - SortedOrder::Desc => self - .containers - .items - .sort_by(|a, b| b.status.cmp(&a.status)), - }, - Header::Cpu => match ord { - SortedOrder::Asc => self - .containers - .items - .sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())), - SortedOrder::Desc => self - .containers - .items - .sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())), - }, - Header::Memory => match ord { - SortedOrder::Asc => self - .containers - .items - .sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())), - SortedOrder::Desc => self - .containers - .items - .sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())), - }, - Header::Id => match ord { - SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)), - SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)), - }, - Header::Image => match ord { - SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)), - SortedOrder::Desc => { - self.containers.items.sort_by(|a, b| b.image.cmp(&a.image)); - } - }, - Header::Name => match ord { - SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)), - SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)), - }, - Header::Rx => match ord { - SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.rx.cmp(&b.rx)), - SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.rx.cmp(&a.rx)), - }, - Header::Tx => match ord { - SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.tx.cmp(&b.tx)), - SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.tx.cmp(&a.tx)), - }, - } + let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering { + match head { + Header::State => match ord { + SortedOrder::Asc => { + a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name)) + } + SortedOrder::Desc => { + b.status.cmp(&a.status).then_with(|| b.name.cmp(&a.name)) + } + }, + Header::Status => match ord { + SortedOrder::Asc => { + a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name)) + } + SortedOrder::Desc => { + b.status.cmp(&a.status).then_with(|| b.name.cmp(&a.name)) + } + }, + Header::Cpu => match ord { + SortedOrder::Asc => a + .cpu_stats + .back() + .cmp(&b.cpu_stats.back()) + .then_with(|| a.name.cmp(&b.name)), + SortedOrder::Desc => b + .cpu_stats + .back() + .cmp(&a.cpu_stats.back()) + .then_with(|| b.name.cmp(&a.name)), + }, + Header::Memory => match ord { + SortedOrder::Asc => a + .mem_stats + .back() + .cmp(&b.mem_stats.back()) + .then_with(|| a.name.cmp(&b.name)), + SortedOrder::Desc => b + .mem_stats + .back() + .cmp(&a.mem_stats.back()) + .then_with(|| b.name.cmp(&a.name)), + }, + Header::Id => match ord { + SortedOrder::Asc => a.id.cmp(&b.id).then_with(|| a.name.cmp(&b.name)), + SortedOrder::Desc => b.id.cmp(&a.id).then_with(|| b.name.cmp(&a.name)), + }, + Header::Image => match ord { + SortedOrder::Asc => a.image.cmp(&b.image).then_with(|| a.name.cmp(&b.name)), + SortedOrder::Desc => { + b.image.cmp(&a.image).then_with(|| b.name.cmp(&a.name)) + } + }, + Header::Name => match ord { + SortedOrder::Asc => a.name.cmp(&b.name).then_with(|| a.name.cmp(&b.name)), + SortedOrder::Desc => b.name.cmp(&a.name).then_with(|| b.name.cmp(&a.name)), + }, + Header::Rx => match ord { + SortedOrder::Asc => a.rx.cmp(&b.rx).then_with(|| a.name.cmp(&b.name)), + SortedOrder::Desc => b.rx.cmp(&a.rx).then_with(|| b.name.cmp(&a.name)), + }, + Header::Tx => match ord { + SortedOrder::Asc => a.tx.cmp(&b.tx).then_with(|| a.name.cmp(&b.name)), + SortedOrder::Desc => b.tx.cmp(&a.tx).then_with(|| b.name.cmp(&a.name)), + }, + } + }; + self.containers.items.sort_by(sort_closure); } else { self.containers .items - .sort_by(|a, b| a.created.cmp(&b.created)); + .sort_by(|a, b| a.created.cmp(&b.created).then_with(|| a.name.cmp(&b.name))); } } From a15da5ed43d07852504a4dd1884a189e3f5b9d84 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 19 Nov 2023 16:03:18 +0000 Subject: [PATCH 21/40] feat: export logs feature, closes #1 Save logs to a file. `--logs-dir` cli arg to change from the default location. Refactor of input_handler --- Cargo.lock | 50 ++++++ Cargo.toml | 1 + README.md | 24 +-- src/app_data/mod.rs | 4 +- src/app_error.rs | 2 + src/exec.rs | 4 +- src/input_handler/mod.rs | 372 ++++++++++++++++++++++++--------------- src/parse_args.rs | 15 +- src/ui/draw_blocks.rs | 9 +- src/ui/gui_state.rs | 8 +- src/ui/mod.rs | 6 +- 11 files changed, 325 insertions(+), 170 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dacf6b..b388a8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,6 +322,27 @@ dependencies = [ "serde", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + [[package]] name = "either" version = "1.9.0" @@ -639,6 +660,17 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "lock_api" version = "0.4.11" @@ -735,6 +767,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -750,6 +788,7 @@ dependencies = [ "cansi", "clap", "crossterm", + "directories", "futures-util", "parking_lot", "ratatui", @@ -913,6 +952,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index 4f8c703..e938ac5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ tracing = "0.1" tracing-subscriber = "0.3" ratatui = "0.24" uuid = { version = "1.5", features = ["v4", "fast-rng"] } +directories = "5.0.1" [dev-dependencies] diff --git a/README.md b/README.md index fd6f0d5..00f8b97 100644 --- a/README.md +++ b/README.md @@ -92,28 +92,30 @@ oxker In application controls | button| result| |--|--| -| ```( tab )``` or ```( shift+tab )``` | change panel, clicking on a panel also changes the selected panel| -| ```( ↑ ↓ )``` or ```( j k )``` or ```( PgUp PgDown )``` or ```( Home End )```| change selected line in selected panel, mouse scroll also changes selected line | -| ```( enter )```| execute selected docker command| -| ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column | -| ```( 0 )``` | stop sorting | -| ```( e )``` | (attempt) to exec into the selected container | -| ```( h )``` | toggle help menu | -| ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected| -| ```( q )``` | to quit at any time | +| ```( tab )``` or ```( shift+tab )``` | Change panel, clicking on a panel also changes the selected panel.| +| ```( ↑ ↓ )``` or ```( j k )``` or ```( PgUp PgDown )``` or ```( Home End )```| Change selected line in selected panel, mouse scroll also changes selected line.| +| ```( enter )```| Run selected docker command.| +| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. | +| ```( 0 )``` | Stop sorting.| +| ```( e )``` | Attempt to exec into the selected container.| +| ```( h )``` | Toggle help menu.| +| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.| +| ```( q )``` | Quit.| +| ```( s )``` | Save logs to `$HOME/[container_name]_[timestamp].log`, or the directory set by `--logs-dir`.| Available command line arguments | argument|result| |--|--| |```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. Defaults to 1000 (1 second).| -|```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| -|```--use-cli```| When executing into a container, use the external Docker CLI application.| |```-r```| Show raw logs. By default, removes ANSI formatting (conflicts with `-c`).| |```-c```| Attempt to color the logs (conflicts with `-r`).| |```-t```| Remove timestamps from each log entry.| |```-s```| If running via Docker, will display the oxker container.| |```-g```| No TUI, essentially a debugging mode with limited functionality, for now.| +|```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| +|```--use-cli```| When executing into a container, use the external Docker CLI application.| +|```--logs-dir```| Set a custom location to save exportings logs into. Defaults to `$HOME`.| ## Build step diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 7695058..22d9a17 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -510,9 +510,9 @@ impl AppData { } /// Get the Id and State for the currently selected container - used by the exec check method - pub fn get_selected_container_id_state(&self) -> Option<(ContainerId, State)> { + pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> { self.get_selected_container() - .map(|i| (i.id.clone(), i.state)) + .map(|i| (i.id.clone(), i.state, i.name.clone())) } /// Update container mem, cpu, & network stats, in single function so only need to call .lock() once diff --git a/src/app_error.rs b/src/app_error.rs index e001645..ba0d66f 100644 --- a/src/app_error.rs +++ b/src/app_error.rs @@ -7,6 +7,7 @@ use std::fmt; pub enum AppError { DockerCommand(DockerControls), DockerExec, + DockerLogs, DockerConnect, DockerInterval, InputPoll, @@ -20,6 +21,7 @@ impl fmt::Display for AppError { match self { Self::DockerCommand(s) => write!(f, "Unable to {s} container"), Self::DockerExec => write!(f, "Unable to exec into container"), + Self::DockerLogs => write!(f, "Unable to save logs"), Self::DockerConnect => write!(f, "Unable to access docker daemon"), Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"), Self::InputPoll => write!(f, "Unable to poll user input"), diff --git a/src/exec.rs b/src/exec.rs index db37bff..6a36d94 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -133,9 +133,9 @@ impl ExecMode { } let use_cli = app_data.lock().args.use_cli; - let container = app_data.lock().get_selected_container_id_state(); + let container = app_data.lock().get_selected_container_id_state_name(); - if let Some((id, state)) = container { + if let Some((id, state, _)) = container { if state == State::Running { if tty_readable() && !use_cli { if let Ok(exec) = docker diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index aaf812d..6c7e52d 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -1,13 +1,21 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, +use std::{ + fs::OpenOptions, + io::{BufWriter, Write}, + path::Path, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::SystemTime, }; -use bollard::Docker; +use bollard::{container::LogsOptions, Docker}; +use cansi::v3::categorise_text; use crossterm::{ event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}, execute, }; +use futures_util::StreamExt; use parking_lot::Mutex; use ratatui::layout::Rect; use tokio::{ @@ -87,48 +95,6 @@ impl InputHandler { } } - /// Toggle the mouse capture (via input of the 'm' key) - fn m_key(&mut self) { - if self.mouse_capture { - if execute!(std::io::stdout(), DisableMouseCapture).is_ok() { - self.gui_state - .lock() - .set_info_box("✖ mouse capture disabled"); - } else { - self.app_data.lock().set_error( - AppError::MouseCapture(false), - &self.gui_state, - Status::Error, - ); - } - } else if Ui::enable_mouse_capture().is_ok() { - self.gui_state - .lock() - .set_info_box("✓ mouse capture enabled"); - } else { - self.app_data.lock().set_error( - AppError::MouseCapture(true), - &self.gui_state, - Status::Error, - ); - }; - - // 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 let Some(info_sleep_timer) = self.info_sleep.as_ref() { - 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 { - tokio::time::sleep(std::time::Duration::from_millis(4000)).await; - gui_state.lock().reset_info_box(); - })); - - self.mouse_capture = !self.mouse_capture; - } - /// Sort the containers by a given header fn sort(&self, selected_header: Header) { self.app_data.lock().set_sort_by_header(selected_header); @@ -191,9 +157,216 @@ impl InputHandler { } } - /// Handle any keyboard button events - // TODO refactor this - #[allow(clippy::too_many_lines)] + /// Toggle the mouse capture (via input of the 'm' key) + fn m_key(&mut self) { + if self.mouse_capture { + if execute!(std::io::stdout(), DisableMouseCapture).is_ok() { + self.gui_state + .lock() + .set_info_box("✖ mouse capture disabled"); + } else { + self.app_data.lock().set_error( + AppError::MouseCapture(false), + &self.gui_state, + Status::Error, + ); + } + } else if Ui::enable_mouse_capture().is_ok() { + self.gui_state + .lock() + .set_info_box("✓ mouse capture enabled"); + } else { + self.app_data.lock().set_error( + AppError::MouseCapture(true), + &self.gui_state, + Status::Error, + ); + }; + + self.mouse_capture = !self.mouse_capture; + } + + /// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file + async fn s_key(&mut self) { + /// This is the inner workings, *inlined* here to return a Result + async fn save_logs( + app_data: &Arc>, + gui_state: &Arc>, + docker_sender: &Sender, + ) -> Result<(), Box> { + let args = app_data.lock().args.clone(); + let container = app_data.lock().get_selected_container_id_state_name(); + if let Some((id, _, name)) = container { + if let Some(log_path) = args.logs_dir { + let (sx, rx) = tokio::sync::oneshot::channel::>(); + docker_sender.send(DockerMessage::Exec(sx)).await?; + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_or(0, |i| i.as_secs()); + + let path = log_path.join(format!("{name}_{now}.log")); + + let docker = rx.await?; + let options = Some(LogsOptions:: { + stdout: true, + timestamps: args.timestamp, + since: 0, + ..Default::default() + }); + let mut logs = docker.logs(id.get(), options); + let mut output = vec![]; + + while let Some(Ok(value)) = logs.next().await { + let data = value.to_string(); + if !data.trim().is_empty() { + output.push( + categorise_text(&data) + .into_iter() + .map(|i| i.text) + .collect::(), + ); + } + } + if !output.is_empty() { + let mut stream = BufWriter::new( + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&path)?, + ); + + for line in &output { + stream.write_all(line.as_bytes())?; + } + stream.flush()?; + + gui_state + .lock() + .set_info_box(&format!("logs saved to {}", path.display())); + } + } + } + Ok(()) + } + + let log_status = Status::Logs; + let status = self.gui_state.lock().status_contains(&[log_status]); + if !status { + self.gui_state.lock().status_push(log_status); + + let uuid = Uuid::new_v4(); + let handle = GuiState::start_loading_animation(&self.gui_state, uuid); + if save_logs(&self.app_data, &self.gui_state, &self.docker_sender) + .await + .is_err() + { + self.app_data.lock().set_error( + AppError::DockerLogs, + &self.gui_state, + Status::Error, + ); + } + self.gui_state.lock().status_del(log_status); + self.gui_state.lock().stop_loading_animation(&handle, uuid); + } + } + + /// Send docker command, if the Commands panel is selected + async fn enter_key(&mut self) { + // This isn't great, just means you can't send docker commands before full initialization of the program + let panel = self.gui_state.lock().get_selected_panel(); + if panel == SelectablePanel::Commands { + let option_command = self.app_data.lock().selected_docker_command(); + + if let Some(command) = option_command { + // Poor way of disallowing commands to be sent to a containerised okxer + if self.app_data.lock().is_oxker() { + return; + }; + let option_id = self.app_data.lock().get_selected_container_id(); + if let Some(id) = option_id { + match command { + DockerControls::Delete => self + .docker_sender + .send(DockerMessage::ConfirmDelete(id)) + .await + .ok(), + DockerControls::Pause => { + self.docker_sender.send(DockerMessage::Pause(id)).await.ok() + } + DockerControls::Unpause => self + .docker_sender + .send(DockerMessage::Unpause(id)) + .await + .ok(), + DockerControls::Start => { + self.docker_sender.send(DockerMessage::Start(id)).await.ok() + } + DockerControls::Stop => { + self.docker_sender.send(DockerMessage::Stop(id)).await.ok() + } + DockerControls::Restart => self + .docker_sender + .send(DockerMessage::Restart(id)) + .await + .ok(), + }; + } + } + } + } + + /// Change the the "next" seletable panel + fn tab_key(&mut self) { + let is_containers = + self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers; + let count = if self.app_data.lock().get_container_len() == 0 && is_containers { + 2 + } else { + 1 + }; + for _ in 0..count { + self.gui_state.lock().next_panel(); + } + } + + /// Change to previously selected panel + fn back_tab_key(&mut self) { + let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs; + let count = if self.app_data.lock().get_container_len() == 0 && is_containers { + 2 + } else { + 1 + }; + for _ in 0..count { + self.gui_state.lock().previous_panel(); + } + } + + fn home_key(&mut self) { + let mut locked_data = self.app_data.lock(); + let selected_panel = self.gui_state.lock().get_selected_panel(); + match selected_panel { + SelectablePanel::Containers => locked_data.containers_start(), + SelectablePanel::Logs => locked_data.log_start(), + SelectablePanel::Commands => locked_data.docker_command_start(), + } + } + + /// Go to end of the list of the currently selected panel + fn end_key(&mut self) { + let mut locked_data = self.app_data.lock(); + let selected_panel = self.gui_state.lock().get_selected_panel(); + match selected_panel { + SelectablePanel::Containers => locked_data.containers_end(), + SelectablePanel::Logs => locked_data.log_end(), + SelectablePanel::Commands => locked_data.docker_command_end(), + } + } + + /// Handle keyboard button events async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) { let contains_delete = self .gui_state @@ -246,52 +419,11 @@ impl InputHandler { KeyCode::Char('e' | 'E') => self.e_key().await, KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), KeyCode::Char('m' | 'M') => self.m_key(), - KeyCode::Tab => { - // Skip control panel if no containers, could be refactored - let is_containers = self.gui_state.lock().get_selected_panel() - == SelectablePanel::Containers; - let count = - if self.app_data.lock().get_container_len() == 0 && is_containers { - 2 - } else { - 1 - }; - for _ in 0..count { - self.gui_state.lock().next_panel(); - } - } - KeyCode::BackTab => { - // Skip control panel if no containers, could be refactored - let is_containers = - self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs; - let count = - if self.app_data.lock().get_container_len() == 0 && is_containers { - 2 - } else { - 1 - }; - for _ in 0..count { - self.gui_state.lock().previous_panel(); - } - } - KeyCode::Home => { - let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().get_selected_panel(); - match selected_panel { - SelectablePanel::Containers => locked_data.containers_start(), - SelectablePanel::Logs => locked_data.log_start(), - SelectablePanel::Commands => locked_data.docker_command_start(), - } - } - KeyCode::End => { - let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().get_selected_panel(); - match selected_panel { - SelectablePanel::Containers => locked_data.containers_end(), - SelectablePanel::Logs => locked_data.log_end(), - SelectablePanel::Commands => locked_data.docker_command_end(), - } - } + KeyCode::Char('s' | 'S') => self.s_key().await, + KeyCode::Tab => self.tab_key(), + KeyCode::BackTab => self.back_tab_key(), + KeyCode::Home => self.home_key(), + KeyCode::End => self.end_key(), KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(), KeyCode::PageUp => { for _ in 0..=6 { @@ -304,55 +436,7 @@ impl InputHandler { self.next(); } } - KeyCode::Enter => { - // This isn't great, just means you can't send docker commands before full initialization of the program - let panel = self.gui_state.lock().get_selected_panel(); - if panel == SelectablePanel::Commands { - let option_command = self.app_data.lock().selected_docker_command(); - - if let Some(command) = option_command { - // Poor way of disallowing commands to be sent to a containerised okxer - if self.app_data.lock().is_oxker() { - return; - }; - let option_id = self.app_data.lock().get_selected_container_id(); - if let Some(id) = option_id { - match command { - DockerControls::Delete => self - .docker_sender - .send(DockerMessage::ConfirmDelete(id)) - .await - .ok(), - DockerControls::Pause => self - .docker_sender - .send(DockerMessage::Pause(id)) - .await - .ok(), - DockerControls::Unpause => self - .docker_sender - .send(DockerMessage::Unpause(id)) - .await - .ok(), - DockerControls::Start => self - .docker_sender - .send(DockerMessage::Start(id)) - .await - .ok(), - DockerControls::Stop => self - .docker_sender - .send(DockerMessage::Stop(id)) - .await - .ok(), - DockerControls::Restart => self - .docker_sender - .send(DockerMessage::Restart(id)) - .await - .ok(), - }; - } - } - } - } + KeyCode::Enter => self.enter_key().await, _ => (), } } diff --git a/src/parse_args.rs b/src/parse_args.rs index 0b4e96d..a5d007d 100644 --- a/src/parse_args.rs +++ b/src/parse_args.rs @@ -1,4 +1,4 @@ -use std::process; +use std::{path::PathBuf, process}; use clap::Parser; use tracing::error; @@ -40,6 +40,10 @@ pub struct Args { /// Use "docker" cli for execing #[clap(long="use-cli", short = None)] pub use_cli: bool, + + /// Directory for exporting logs, defaults to `$HOME` + #[clap(long="logs-dir", short = None)] + pub logs_dir: Option, } #[derive(Debug, Clone)] @@ -47,13 +51,14 @@ pub struct Args { pub struct CliArgs { pub color: bool, pub docker_interval: u32, - pub use_cli: bool, pub gui: bool, pub host: Option, pub in_container: bool, + pub logs_dir: Option, pub raw: bool, pub show_self: bool, pub timestamp: bool, + pub use_cli: bool, } impl CliArgs { @@ -72,6 +77,11 @@ impl CliArgs { pub fn new() -> Self { let args = Args::parse(); + let logs_dir = args.logs_dir.map_or_else( + || directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned()), + |logs_dir| Some(std::path::Path::new(&logs_dir).to_owned()), + ); + // Quit the program if the docker update argument is 0 // Should maybe change it to check if less than 100 if args.docker_interval == 0 { @@ -85,6 +95,7 @@ impl CliArgs { gui: !args.gui, host: args.host, in_container: Self::check_if_in_container(), + logs_dir, raw: args.raw, show_self: !args.show_self, timestamp: !args.timestamp, diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index c6b3359..5008d3c 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -10,7 +10,7 @@ use ratatui::{ }, Frame, }; -use std::default::Default; +use std::{default::Default, time::Instant}; use std::{fmt::Display, sync::Arc}; use crate::app_data::{ContainerItem, Header, SortedOrder}; @@ -20,7 +20,7 @@ use crate::{ }; use super::{ - gui_state::{BoxLocation, DeleteButton, Region}, + gui_state::{self, BoxLocation, DeleteButton, Region}, FrameData, }; use super::{GuiState, SelectablePanel}; @@ -877,7 +877,7 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) { } /// Draw info box in one of the 9 BoxLocations -pub fn info(f: &mut Frame, text: &str) { +pub fn info(f: &mut Frame, text: &str, instant: Instant, gui_state: &Arc>) { let block = Block::default() .title("") .title_alignment(Alignment::Center) @@ -898,6 +898,9 @@ pub fn info(f: &mut Frame, text: &str) { let area = popup(lines, max_line_width, f.size(), BoxLocation::BottomRight); f.render_widget(Clear, area); f.render_widget(paragraph, area); + if instant.elapsed().as_millis() > 4000 { + gui_state.lock().reset_info_box(); + } } /// draw a box in the one of the BoxLocations, based on max line width + number of lines diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index a7240f2..fa2d229 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -3,6 +3,7 @@ use ratatui::layout::{Constraint, Rect}; use std::{ collections::{HashMap, HashSet}, sync::Arc, + time::Instant, }; use tokio::task::JoinHandle; use uuid::Uuid; @@ -158,12 +159,13 @@ const FRAMES_LEN: u8 = 9; /// Various functions (e.g input handler), operate differently depending upon current Status #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Status { - Exec, DeleteConfirm, DockerConnect, Error, + Exec, Help, Init, + Logs, } /// Global gui_state, stored in an Arc @@ -178,7 +180,7 @@ pub struct GuiState { selected_panel: SelectablePanel, status: HashSet, exec_mode: Option, - pub info_box_text: Option, + pub info_box_text: Option<(String, Instant)>, } impl GuiState { /// Clear panels hash map, so on resize can fix the sizes for mouse clicks @@ -366,7 +368,7 @@ impl GuiState { /// Set info box content pub fn set_info_box(&mut self, text: &str) { - self.info_box_text = Some(text.to_owned()); + self.info_box_text = Some((text.to_owned(), std::time::Instant::now())); } /// Remove info box content diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 97f0225..d5aeac5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -247,7 +247,7 @@ pub struct FrameData { height: u16, help_visible: bool, init: bool, - info_text: Option, + info_text: Option<(String, Instant)>, loading_icon: String, selected_panel: SelectablePanel, sorted_by: Option<(Header, SortedOrder)>, @@ -347,8 +347,8 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc Date: Sun, 19 Nov 2023 22:38:26 +0000 Subject: [PATCH 22/40] chore: .devcontainer updated --- .devcontainer/devcontainer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4ab9ab4..28b0715 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,9 +21,6 @@ "mounts": [ "source=/etc/timezone,target=/etc/timezone,type=bind,readonly" ], - "containerEnv": { - "CARGO_REGISTRIES_CRATES_IO_PROTOCOL": "sparse" - }, "customizations": { "vscode": { From 2de76e2f358be9c1500ca3dc4f9df0979ed8ed28 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 19 Nov 2023 22:48:40 +0000 Subject: [PATCH 23/40] chore: lints moved from main.rs to Cargo.toml --- Cargo.toml | 14 ++++++++++++++ src/main.rs | 22 +++++----------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e938ac5..7257fa2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,20 @@ readme = "README.md" keywords = ["docker", "tui", "tokio", "terminal", "podman"] categories = ["command-line-utilities"] +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +expect_used = "warn" +nursery = "warn" +pedantic ="warn" +todo = "warn" +unused_async = "warn" +unwrap_used = "warn" +module_name_repetitions = "allow" +doc_markdown = "allow" +similar_names = "allow" + [dependencies] anyhow = "1.0" # bollard = "0.15" diff --git a/src/main.rs b/src/main.rs index 059076c..c3199da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,5 @@ -#![forbid(unsafe_code)] -#![warn( - clippy::expect_used, - clippy::nursery, - clippy::pedantic, - clippy::todo, - clippy::unused_async, - clippy::unwrap_used -)] -#![allow( - clippy::module_name_repetitions, - clippy::doc_markdown, - clippy::similar_names -)] // Only allow when debugging -#![allow(unused)] +// #![allow(unused)] use app_data::AppData; use app_error::AppError; @@ -123,6 +109,8 @@ async fn main() { let args = CliArgs::new(); + // If running via Docker image, need to sleep else program will just quit straight away, no real idea why + // So just sleep for small while if args.in_container { std::thread::sleep(std::time::Duration::from_millis(250)); } @@ -144,9 +132,9 @@ async fn main() { .await; if args.gui { - let (input_sx, input_rx) = tokio::sync::mpsc::channel(32); + let (sx, input_rx) = tokio::sync::mpsc::channel(32); handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running); - Ui::create(app_data, docker_tx.clone(), gui_state, is_running, input_sx).await; + Ui::create(app_data, gui_state, is_running, sx).await; } else { info!("in debug mode\n"); // Debug mode for testing, less pointless now, will display some basic information From d200d13c26d7f4a0d1eae75e77beaeab4859a8ce Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 19 Nov 2023 22:48:49 +0000 Subject: [PATCH 24/40] refactor: dead code removed --- src/exec.rs | 7 ------- src/input_handler/mod.rs | 9 +-------- src/ui/draw_blocks.rs | 2 +- src/ui/mod.rs | 9 ++------- 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/exec.rs b/src/exec.rs index 6a36d94..7092505 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -1,12 +1,9 @@ use std::{ - fmt, - hash::{Hash, Hasher}, io::{Read, Write}, sync::{atomic::AtomicBool, Arc}, }; use bollard::{ - container, exec::{CreateExecOptions, StartExecOptions, StartExecResults}, Docker, }; @@ -18,8 +15,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ app_data::{AppData, ContainerId, State}, app_error::AppError, - parse_args::CliArgs, - ui::{GuiState, Status}, }; /// TTY location @@ -304,8 +299,6 @@ impl ExecMode { // RESET TERMINAL BEFROEHAND pub async fn run( &self, - app_data: &Arc>, - gui_state: &Arc>, ) -> Result<(), AppError> { match self { Self::External(id) => { diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 6c7e52d..4d19829 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -1,7 +1,6 @@ use std::{ fs::OpenOptions, io::{BufWriter, Write}, - path::Path, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -18,10 +17,7 @@ use crossterm::{ use futures_util::StreamExt; use parking_lot::Mutex; use ratatui::layout::Rect; -use tokio::{ - sync::mpsc::{Receiver, Sender}, - task::JoinHandle, -}; +use tokio::sync::mpsc::{Receiver, Sender}; use uuid::Uuid; mod message; @@ -40,7 +36,6 @@ pub struct InputHandler { app_data: Arc>, docker_sender: Sender, gui_state: Arc>, - info_sleep: Option>, is_running: Arc, mouse_capture: bool, rec: Receiver, @@ -62,7 +57,6 @@ impl InputHandler { is_running, rec, mouse_capture: true, - info_sleep: None, }; inner.start().await; } @@ -132,7 +126,6 @@ impl InputHandler { /// Validate that one can exec into a Docker container async fn e_key(&self) { let is_oxker = self.app_data.lock().is_oxker(); - let mut exec_err = Some(()); if !is_oxker && tty_readable() { let uuid = Uuid::new_v4(); let handle = GuiState::start_loading_animation(&self.gui_state, uuid); diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 5008d3c..6658105 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -20,7 +20,7 @@ use crate::{ }; use super::{ - gui_state::{self, BoxLocation, DeleteButton, Region}, + gui_state::{BoxLocation, DeleteButton, Region}, FrameData, }; use super::{GuiState, SelectablePanel}; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d5aeac5..e49c435 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use bollard::Docker; use crossterm::{ event::{self, DisableMouseCapture, Event}, execute, @@ -29,13 +28,11 @@ pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ app_data::{AppData, Columns, ContainerId, Header, SortedOrder}, app_error::AppError, - docker_data::DockerMessage, input_handler::InputMessages, }; pub struct Ui { app_data: Arc>, - docker_sx: Sender, gui_state: Arc>, input_poll_rate: Duration, is_running: Arc, @@ -61,7 +58,6 @@ impl Ui { /// Create a new Ui struct, and execute the drawing loop pub async fn create( app_data: Arc>, - docker_sx: Sender, gui_state: Arc>, is_running: Arc, sender: Sender, @@ -71,7 +67,6 @@ impl Ui { let cursor_position = terminal.get_cursor().unwrap_or_default(); let mut ui = Self { app_data, - docker_sx, gui_state, input_poll_rate: std::time::Duration::from_millis(100), is_running, @@ -147,12 +142,12 @@ impl Ui { /// Use exeternal docker cli to exec into a container async fn exec(&mut self) { - let mut exec_mode = self.gui_state.lock().get_exec_mode(); + let exec_mode = self.gui_state.lock().get_exec_mode(); if let Some(mode) = exec_mode { self.reset_terminal().ok(); self.terminal.clear().ok(); - if let Err(e) = mode.run(&self.app_data, &self.gui_state).await { + if let Err(e) = mode.run().await { self.app_data .lock() .set_error(e, &self.gui_state, Status::Error); From cd1da2ad96fc4ed80c269ec8d23d1525739d38f5 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 19 Nov 2023 22:52:43 +0000 Subject: [PATCH 25/40] fix: State ordering use .order() --- src/app_data/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 22d9a17..10b4532 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -164,10 +164,10 @@ impl AppData { match head { Header::State => match ord { SortedOrder::Asc => { - a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name)) + a.state.order().cmp(&b.state.order()).then_with(|| a.name.cmp(&b.name)) } SortedOrder::Desc => { - b.status.cmp(&a.status).then_with(|| b.name.cmp(&a.name)) + b.state.order().cmp(&a.state.order()).then_with(|| b.name.cmp(&a.name)) } }, Header::Status => match ord { @@ -213,8 +213,8 @@ impl AppData { } }, Header::Name => match ord { - SortedOrder::Asc => a.name.cmp(&b.name).then_with(|| a.name.cmp(&b.name)), - SortedOrder::Desc => b.name.cmp(&a.name).then_with(|| b.name.cmp(&a.name)), + SortedOrder::Asc => a.name.cmp(&b.name).then_with(|| a.id.cmp(&b.id)), + SortedOrder::Desc => b.name.cmp(&a.name).then_with(|| b.id.cmp(&a.id)), }, Header::Rx => match ord { SortedOrder::Asc => a.rx.cmp(&b.rx).then_with(|| a.name.cmp(&b.name)), From 56dba91e9a3b72b7547b3e17255a809b16c7ccf2 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 19 Nov 2023 23:03:41 +0000 Subject: [PATCH 26/40] refactor: variable name changes --- src/app_data/mod.rs | 16 ++++++++------ src/docker_data/mod.rs | 4 ++-- src/exec.rs | 4 +--- src/input_handler/mod.rs | 45 +++++++++++++++++----------------------- src/main.rs | 8 +++---- src/ui/mod.rs | 12 +++++------ 6 files changed, 42 insertions(+), 47 deletions(-) diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 10b4532..745572f 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -163,12 +163,16 @@ impl AppData { let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering { match head { Header::State => match ord { - SortedOrder::Asc => { - a.state.order().cmp(&b.state.order()).then_with(|| a.name.cmp(&b.name)) - } - SortedOrder::Desc => { - b.state.order().cmp(&a.state.order()).then_with(|| b.name.cmp(&a.name)) - } + SortedOrder::Asc => a + .state + .order() + .cmp(&b.state.order()) + .then_with(|| a.name.cmp(&b.name)), + SortedOrder::Desc => b + .state + .order() + .cmp(&a.state.order()) + .then_with(|| b.name.cmp(&a.name)), }, Header::Status => match ord { SortedOrder::Asc => { diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 2817930..99d0bcf 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -339,8 +339,8 @@ impl DockerData { let uuid = Uuid::new_v4(); // TODO need to refactor these match message { - DockerMessage::Exec(sender) => { - sender.send(Arc::clone(&self.docker)).ok(); + DockerMessage::Exec(docker_tx) => { + docker_tx.send(Arc::clone(&self.docker)).ok(); } DockerMessage::Pause(id) => { tokio::spawn(async move { diff --git a/src/exec.rs b/src/exec.rs index 7092505..6dcc5db 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -297,9 +297,7 @@ impl ExecMode { } // RESET TERMINAL BEFROEHAND - pub async fn run( - &self, - ) -> Result<(), AppError> { + pub async fn run(&self) -> Result<(), AppError> { match self { Self::External(id) => { Self::exec_external(id); diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 4d19829..a84b0d4 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -34,7 +34,7 @@ pub use message::InputMessages; #[derive(Debug)] pub struct InputHandler { app_data: Arc>, - docker_sender: Sender, + docker_tx: Sender, gui_state: Arc>, is_running: Arc, mouse_capture: bool, @@ -46,13 +46,13 @@ impl InputHandler { pub async fn init( app_data: Arc>, rec: Receiver, - docker_sender: Sender, + docker_tx: Sender, gui_state: Arc>, is_running: Arc, ) { let mut inner = Self { app_data, - docker_sender, + docker_tx, gui_state, is_running, rec, @@ -101,7 +101,7 @@ impl InputHandler { .gui_state .lock() .status_contains(&[Status::Error, Status::Init]); - if error_init || self.docker_sender.send(DockerMessage::Quit).await.is_err() { + if error_init || self.docker_tx.send(DockerMessage::Quit).await.is_err() { self.is_running .store(false, std::sync::atomic::Ordering::SeqCst); } @@ -111,10 +111,7 @@ impl InputHandler { async fn confirm_delete(&self) { let id = self.gui_state.lock().get_delete_container(); if let Some(id) = id { - self.docker_sender - .send(DockerMessage::Delete(id)) - .await - .ok(); + self.docker_tx.send(DockerMessage::Delete(id)).await.ok(); } } @@ -130,7 +127,7 @@ impl InputHandler { let uuid = Uuid::new_v4(); let handle = GuiState::start_loading_animation(&self.gui_state, uuid); let (sx, rx) = tokio::sync::oneshot::channel::>(); - self.docker_sender.send(DockerMessage::Exec(sx)).await.ok(); + self.docker_tx.send(DockerMessage::Exec(sx)).await.ok(); if let Ok(docker) = rx.await { (ExecMode::new(&self.app_data, &docker).await).map_or_else( @@ -185,14 +182,14 @@ impl InputHandler { async fn save_logs( app_data: &Arc>, gui_state: &Arc>, - docker_sender: &Sender, + docker_tx: &Sender, ) -> Result<(), Box> { let args = app_data.lock().args.clone(); let container = app_data.lock().get_selected_container_id_state_name(); if let Some((id, _, name)) = container { if let Some(log_path) = args.logs_dir { let (sx, rx) = tokio::sync::oneshot::channel::>(); - docker_sender.send(DockerMessage::Exec(sx)).await?; + docker_tx.send(DockerMessage::Exec(sx)).await?; let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -251,7 +248,7 @@ impl InputHandler { let uuid = Uuid::new_v4(); let handle = GuiState::start_loading_animation(&self.gui_state, uuid); - if save_logs(&self.app_data, &self.gui_state, &self.docker_sender) + if save_logs(&self.app_data, &self.gui_state, &self.docker_tx) .await .is_err() { @@ -282,29 +279,25 @@ impl InputHandler { if let Some(id) = option_id { match command { DockerControls::Delete => self - .docker_sender + .docker_tx .send(DockerMessage::ConfirmDelete(id)) .await .ok(), DockerControls::Pause => { - self.docker_sender.send(DockerMessage::Pause(id)).await.ok() + self.docker_tx.send(DockerMessage::Pause(id)).await.ok() + } + DockerControls::Unpause => { + self.docker_tx.send(DockerMessage::Unpause(id)).await.ok() } - DockerControls::Unpause => self - .docker_sender - .send(DockerMessage::Unpause(id)) - .await - .ok(), DockerControls::Start => { - self.docker_sender.send(DockerMessage::Start(id)).await.ok() + self.docker_tx.send(DockerMessage::Start(id)).await.ok() } DockerControls::Stop => { - self.docker_sender.send(DockerMessage::Stop(id)).await.ok() + self.docker_tx.send(DockerMessage::Stop(id)).await.ok() + } + DockerControls::Restart => { + self.docker_tx.send(DockerMessage::Restart(id)).await.ok() } - DockerControls::Restart => self - .docker_sender - .send(DockerMessage::Restart(id)) - .await - .ok(), }; } } diff --git a/src/main.rs b/src/main.rs index c3199da..d976325 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,8 +109,8 @@ async fn main() { let args = CliArgs::new(); - // If running via Docker image, need to sleep else program will just quit straight away, no real idea why - // So just sleep for small while + // If running via Docker image, need to sleep else program will just quit straight away, no real idea why + // So just sleep for small while if args.in_container { std::thread::sleep(std::time::Duration::from_millis(250)); } @@ -132,9 +132,9 @@ async fn main() { .await; if args.gui { - let (sx, input_rx) = tokio::sync::mpsc::channel(32); + let (input_tx, input_rx) = tokio::sync::mpsc::channel(32); handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running); - Ui::create(app_data, gui_state, is_running, sx).await; + Ui::create(app_data, gui_state, input_tx, is_running).await; } else { info!("in debug mode\n"); // Debug mode for testing, less pointless now, will display some basic information diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e49c435..867e0d2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -35,9 +35,9 @@ pub struct Ui { app_data: Arc>, gui_state: Arc>, input_poll_rate: Duration, + input_tx: Sender, is_running: Arc, now: Instant, - sender: Sender, terminal: Terminal>, cursor_position: (u16, u16), } @@ -59,21 +59,21 @@ impl Ui { pub async fn create( app_data: Arc>, gui_state: Arc>, + input_tx: Sender, is_running: Arc, - sender: Sender, ) { if let Ok(mut terminal) = Self::setup_terminal() { // let args = app_data.lock().args.clone(); let cursor_position = terminal.get_cursor().unwrap_or_default(); let mut ui = Self { app_data, + cursor_position, gui_state, input_poll_rate: std::time::Duration::from_millis(100), + input_tx, is_running, now: Instant::now(), - sender, terminal, - cursor_position, }; if let Err(e) = ui.draw_ui().await { error!("{e}"); @@ -178,7 +178,7 @@ impl Ui { if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) { if let Ok(event) = event::read() { if let Event::Key(key) = event { - self.sender + self.input_tx .send(InputMessages::ButtonPress((key.code, key.modifiers))) .await .ok(); @@ -187,7 +187,7 @@ impl Ui { event::MouseEventKind::Down(_) | event::MouseEventKind::ScrollDown | event::MouseEventKind::ScrollUp => { - self.sender.send(InputMessages::MouseEvent(m)).await.ok(); + self.input_tx.send(InputMessages::MouseEvent(m)).await.ok(); } _ => (), } From 294cc2684f42daab9d51601e235a384f55617678 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 08:21:55 +0000 Subject: [PATCH 27/40] chore: dependencies updated --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b388a8b..6c010f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1418,9 +1418,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "c58fe91d841bc04822c9801002db4ea904b9e4b8e6bbad25127b46eff8dc516b" dependencies = [ "getrandom", "rand", diff --git a/Cargo.toml b/Cargo.toml index 7257fa2..40cadd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,8 @@ tokio = { version = "1.34", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" ratatui = "0.24" -uuid = { version = "1.5", features = ["v4", "fast-rng"] } -directories = "5.0.1" +uuid = { version = "1.6", features = ["v4", "fast-rng"] } +directories = "5.0" [dev-dependencies] From b608432865f00093615839252bd69fdec4debf48 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:27:54 +0000 Subject: [PATCH 28/40] refactor: variable names updated --- README.md | 6 ++--- src/exec.rs | 51 +++++++++++++++++++++++++++++++++------- src/input_handler/mod.rs | 2 +- src/parse_args.rs | 14 +++++------ src/ui/mod.rs | 3 ++- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 00f8b97..513a4dc 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ In application controls | ```( h )``` | Toggle help menu.| | ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.| | ```( q )``` | Quit.| -| ```( s )``` | Save logs to `$HOME/[container_name]_[timestamp].log`, or the directory set by `--logs-dir`.| +| ```( s )``` | Save logs to `$HOME/oxker_[container_name].log`, or the directory set by `--save-dir`.| Available command line arguments @@ -114,8 +114,8 @@ Available command line arguments |```-s```| If running via Docker, will display the oxker container.| |```-g```| No TUI, essentially a debugging mode with limited functionality, for now.| |```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| -|```--use-cli```| When executing into a container, use the external Docker CLI application.| -|```--logs-dir```| Set a custom location to save exportings logs into. Defaults to `$HOME`.| +|```--save-dir```| Set a custom location to save exported logs into. Defaults to `$HOME`.| +|```--use-cli```| When executing into a container, force use of the external Docker CLI application.| ## Build step diff --git a/src/exec.rs b/src/exec.rs index 6dcc5db..cb81c28 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -1,15 +1,16 @@ use std::{ - io::{Read, Write}, + io::{Read, Stdout, Write}, sync::{atomic::AtomicBool, Arc}, }; use bollard::{ - exec::{CreateExecOptions, StartExecOptions, StartExecResults}, + exec::{CreateExecOptions, ResizeExecOptions, StartExecOptions, StartExecResults}, Docker, }; use crossterm::terminal::enable_raw_mode; use futures_util::StreamExt; use parking_lot::Mutex; +use ratatui::{backend::CrosstermBackend, Terminal}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::{ @@ -24,7 +25,7 @@ const TTY: &str = "/dev/tty"; const OCI_ERROR: &str = "OCI runtime exec failed"; /// Set the cursor position on the screen to (0,0) -pub const CURSOR_POS: &str = "\x1B[J\x1B[H"; +const CURSOR_POS: &str = "\x1B[J\x1B[H"; /// This needs to be written to stdout when exiting the exec mode, else the input handler thread gets confused, /// see https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement @@ -110,6 +111,24 @@ struct AsyncTTY { rx: std::sync::mpsc::Receiver, } +/// This is used to set the terminal size when exec via the Internal method +#[derive(Debug, Clone)] +pub struct TerminalSize { + width: u16, + height: u16, +} + +impl TerminalSize { + pub fn new(terminal: &Terminal>) -> Option { + terminal.size().map_or(None, |i| { + Some(Self { + width: i.width, + height: i.height, + }) + }) + } +} + #[derive(Debug, Clone)] pub enum ExecMode { // use Bollard Rust library @@ -192,7 +211,12 @@ impl ExecMode { /// Exec into the container via the Bollard library, stdout & stdin on different threads /// Have to deal with strange output once dropped, hence the use of internal_cleanup() method - async fn exec_internal(&self, id: &ContainerId, docker: &Arc) -> Result<(), AppError> { + async fn exec_internal( + &self, + id: &ContainerId, + docker: &Arc, + terminal_size: Option, + ) -> Result<(), AppError> { let run = Arc::new(AtomicBool::new(true)); if let Ok(exec_result) = docker @@ -239,6 +263,19 @@ impl ExecMode { } }); + if let Some(terminal_size) = terminal_size { + docker + .resize_exec( + &exec_result.id, + ResizeExecOptions { + height: terminal_size.height, + width: terminal_size.width, + }, + ) + .await + .ok(); + } + while let Ok(x) = async_tty.rx.recv() { input.write(&[x]).await.ok(); } @@ -263,7 +300,6 @@ impl ExecMode { let waiting_thread = Arc::clone(&waiting); std::thread::spawn(move || { - // At the moment the known max length is 26 let mut bytes = Vec::with_capacity(26); while waiting_thread.load(std::sync::atomic::Ordering::SeqCst) { let mut buf = [0]; @@ -296,15 +332,14 @@ impl ExecMode { } } - // RESET TERMINAL BEFROEHAND - pub async fn run(&self) -> Result<(), AppError> { + pub async fn run(&self, tty_size: Option) -> Result<(), AppError> { match self { Self::External(id) => { Self::exec_external(id); Ok(()) } - Self::Internal((id, docker)) => self.exec_internal(id, docker).await, + Self::Internal((id, docker)) => self.exec_internal(id, docker, tty_size).await, } } } diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index a84b0d4..8da3c3c 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -187,7 +187,7 @@ impl InputHandler { let args = app_data.lock().args.clone(); let container = app_data.lock().get_selected_container_id_state_name(); if let Some((id, _, name)) = container { - if let Some(log_path) = args.logs_dir { + if let Some(log_path) = args.save_dir { let (sx, rx) = tokio::sync::oneshot::channel::>(); docker_tx.send(DockerMessage::Exec(sx)).await?; diff --git a/src/parse_args.rs b/src/parse_args.rs index a5d007d..e29d84c 100644 --- a/src/parse_args.rs +++ b/src/parse_args.rs @@ -37,13 +37,13 @@ pub struct Args { #[clap(long, short = None)] pub host: Option, - /// Use "docker" cli for execing + /// Force use of docker cli when execing into containers #[clap(long="use-cli", short = None)] pub use_cli: bool, - /// Directory for exporting logs, defaults to `$HOME` - #[clap(long="logs-dir", short = None)] - pub logs_dir: Option, + /// Directory for saving exported logs, defaults to `$HOME` + #[clap(long="save-dir", short = None)] + pub save_dir: Option, } #[derive(Debug, Clone)] @@ -54,7 +54,7 @@ pub struct CliArgs { pub gui: bool, pub host: Option, pub in_container: bool, - pub logs_dir: Option, + pub save_dir: Option, pub raw: bool, pub show_self: bool, pub timestamp: bool, @@ -77,7 +77,7 @@ impl CliArgs { pub fn new() -> Self { let args = Args::parse(); - let logs_dir = args.logs_dir.map_or_else( + let logs_dir = args.save_dir.map_or_else( || directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned()), |logs_dir| Some(std::path::Path::new(&logs_dir).to_owned()), ); @@ -95,7 +95,7 @@ impl CliArgs { gui: !args.gui, host: args.host, in_container: Self::check_if_in_container(), - logs_dir, + save_dir: logs_dir, raw: args.raw, show_self: !args.show_self, timestamp: !args.timestamp, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 867e0d2..2df6c51 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -28,6 +28,7 @@ pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ app_data::{AppData, Columns, ContainerId, Header, SortedOrder}, app_error::AppError, + exec::TerminalSize, input_handler::InputMessages, }; @@ -147,7 +148,7 @@ impl Ui { if let Some(mode) = exec_mode { self.reset_terminal().ok(); self.terminal.clear().ok(); - if let Err(e) = mode.run().await { + if let Err(e) = mode.run(TerminalSize::new(&self.terminal)).await { self.app_data .lock() .set_error(e, &self.gui_state, Status::Error); From aba43859fdb0ebf9223b24d467b8074040fa9d22 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:31:34 +0000 Subject: [PATCH 29/40] docs: README.md typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 513a4dc..27ce3f3 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ In application controls | ```( h )``` | Toggle help menu.| | ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.| | ```( q )``` | Quit.| -| ```( s )``` | Save logs to `$HOME/oxker_[container_name].log`, or the directory set by `--save-dir`.| +| ```( s )``` | Save logs to `$HOME/[container_name]_[timestamp].log`, or the directory set by `--save-dir`.| Available command line arguments @@ -114,7 +114,7 @@ Available command line arguments |```-s```| If running via Docker, will display the oxker container.| |```-g```| No TUI, essentially a debugging mode with limited functionality, for now.| |```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| -|```--save-dir```| Set a custom location to save exported logs into. Defaults to `$HOME`.| +|```--save-dir [directory]```| Save exported logs into a custom directory. Defaults to `$HOME`.| |```--use-cli```| When executing into a container, force use of the external Docker CLI application.| ## Build step From 5ef24b840b80d93b8f292e319c763ec2fd31cced Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:44:14 +0000 Subject: [PATCH 30/40] fix: Help menu added missing keys --- src/ui/draw_blocks.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 6658105..534fe0f 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -597,11 +597,10 @@ impl HelpInfo { button_item("h"), button_desc("to toggle this help information"), ]), - Line::from(vec![space(), button_item("0"), button_desc("to stop sort")]), Line::from(vec![ space(), - button_item("1 - 9"), - button_desc("sort by header - or click header"), + button_item("s"), + button_desc("Save logs of selected container to file"), ]), Line::from(vec![ space(), @@ -610,6 +609,12 @@ impl HelpInfo { "to toggle mouse capture - if disabled, text on screen can be selected & copied", ), ]), + Line::from(vec![space(), button_item("0"), button_desc("to stop sort")]), + Line::from(vec![ + space(), + button_item("1 - 9"), + button_desc("sort by header - or click header"), + ]), Line::from(vec![ space(), button_item("q"), From d328072f276b09a81b0de419a9c064b4a13810a1 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:55:20 +0000 Subject: [PATCH 31/40] docs: changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6677361..42a83c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,16 @@ ### Chores + workflow dependencies updated, [6a4cf6490d08b976734e2bc8186d94c095700558] -+ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53], [81d5b326db8881263f2c9072e1426948e41b4a0f] ++ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53], [81d5b326db8881263f2c9072e1426948e41b4a0f], [294cc2684f42daab9d51601e235a384f55617678] ++ lints moved from main.rs to Cargo.toml, [2de76e2f358be9c1500ca3dc4f9df0979ed8ed28] ++ .devcontainer updated, [37d2ee915625806dd11c2cc816a892aae12a777c] ### Features -+ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key - closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff], [0e5ee143b008c9d0ee0b681231a1568be227150b] ++ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key, closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff], [0e5ee143b008c9d0ee0b681231a1568be227150b], [0e5ee143b008c9d0ee0b681231a1568be227150b] ++ Export logs feature, press `s` to save logs, use `--save-dir` cli-arg to customise output location, closes #1, [a15da5ed43d07852504a4dd1884a189e3f5b9d84] ### Fixes + `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8] ++ sorted created_at clash, closes #22, [3a6489396e87702ce94b349a7f47028ece7922f6] # v0.3.3 ### 2023-10-21 From 7997015d36e0d1567dbe17f5c91a04a6d1536136 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:56:12 +0000 Subject: [PATCH 32/40] docs: README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 27ce3f3..4a0ede8 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Available command line arguments |```-g```| No TUI, essentially a debugging mode with limited functionality, for now.| |```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| |```--save-dir [directory]```| Save exported logs into a custom directory. Defaults to `$HOME`.| -|```--use-cli```| When executing into a container, force use of the external Docker CLI application.| +|```--use-cli```| When executing into a container, force use of the external Docker CLI application, instead of by the Docker api.| ## Build step From 39943645c7331cbe3368e158fdd312dfd63964aa Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:19:05 +0000 Subject: [PATCH 33/40] style: UI text changes --- src/ui/draw_blocks.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 534fe0f..bd5bb07 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -569,7 +569,7 @@ impl HelpInfo { button_item("tab"), or(), button_item("shift+tab"), - button_desc("to change panels"), + button_desc("change panels"), ]), Line::from(vec![ space(), @@ -580,12 +580,12 @@ impl HelpInfo { button_item("PgUp PgDown"), or(), button_item("Home End"), - button_desc("to change selected line"), + button_desc("change selected line"), ]), Line::from(vec![ space(), button_item("enter"), - button_desc("to send docker container command"), + button_desc("send docker container command"), ]), Line::from(vec![ space(), @@ -595,21 +595,21 @@ impl HelpInfo { Line::from(vec![ space(), button_item("h"), - button_desc("to toggle this help information"), + button_desc("toggle this help information"), ]), Line::from(vec![ space(), button_item("s"), - button_desc("Save logs of selected container to file"), + button_desc("save logs to file"), ]), Line::from(vec![ space(), button_item("m"), button_desc( - "to toggle mouse capture - if disabled, text on screen can be selected & copied", + "toggle mouse capture - if disabled, text on screen can be selected & copied", ), ]), - Line::from(vec![space(), button_item("0"), button_desc("to stop sort")]), + Line::from(vec![space(), button_item("0"), button_desc("stop sort")]), Line::from(vec![ space(), button_item("1 - 9"), @@ -618,7 +618,7 @@ impl HelpInfo { Line::from(vec![ space(), button_item("q"), - button_desc("to quit at any time"), + button_desc("quit at any time"), ]), ]; @@ -856,7 +856,7 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) { seconds.unwrap_or(5) ) } - _ => String::from("\n\n ( c ) to clear error\n ( q ) to quit oxker"), + _ => String::from("\n\n ( c ) clear error\n ( q ) quit oxker "), }; let mut text = format!("\n{error}"); From 7969db93e2ff2c6b9f0bbf640bf49883efe42673 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:19:15 +0000 Subject: [PATCH 34/40] docs: README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a0ede8..2f1f3cc 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ In application controls | ```( enter )```| Run selected docker command.| | ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. | | ```( 0 )``` | Stop sorting.| -| ```( e )``` | Attempt to exec into the selected container.| +| ```( e )``` | Exec into the selected container.| | ```( h )``` | Toggle help menu.| | ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.| | ```( q )``` | Quit.| From 66dabb84a547105bccb5e8d180c97087c64249df Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:02:03 +0000 Subject: [PATCH 35/40] chore: Dev Docker File comments/examples added --- containerised/Dockerfile_dev | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev index 7984af8..8a47e04 100644 --- a/containerised/Dockerfile_dev +++ b/containerised/Dockerfile_dev @@ -32,3 +32,19 @@ ENTRYPOINT [ "/app/oxker"] # Build production version for x86 only, then run # docker build --platform linux/amd64 -t oxker_dev -f containerised/Dockerfile . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev + +# docker build --platform linux/arm/v6 -t oxker_dev -f containerised/Dockerfile . + +### Build docker files and save to .tar file + +# docker build --platform linux/amd64 -t oxker_dev_amd64 -f containerised/Dockerfile .; docker save -o ./oxker_dev_amd64.tar oxker_dev_amd64 +# docker load -i oxker_dev_amd64.tar +# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev_amd64 + +# docker build --platform linux/arm64 -t oxker_dev_arm64 -f containerised/Dockerfile .; docker save -o ./oxker_dev_arm64.tar oxker_dev_arm64 +# docker load -i oxker_dev_arm64.tar +# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev oxker_dev_arm64 + +# docker build --platform linux/arm/v6 -t oxker_dev_armv6 -f containerised/Dockerfile .; docker save -o ./oxker_dev_armv6.tar oxker_dev_armv6 +# docker load -i oxker_dev_armv6.tar +# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev_armv6 \ No newline at end of file From 9e83d48641c37d70b9e28272c1bd0bb640922aaa Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:24:35 +0000 Subject: [PATCH 36/40] fix: typo --- src/input_handler/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 8da3c3c..d226f67 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -234,7 +234,7 @@ impl InputHandler { gui_state .lock() - .set_info_box(&format!("logs saved to {}", path.display())); + .set_info_box(&format!("saved to {}", path.display())); } } } From 1c173d023091b730d81045fd7af2a0d21075c5df Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:40:41 +0000 Subject: [PATCH 37/40] docs: README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f1f3cc..593ef29 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Available command line arguments |```-g```| No TUI, essentially a debugging mode with limited functionality, for now.| |```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| |```--save-dir [directory]```| Save exported logs into a custom directory. Defaults to `$HOME`.| -|```--use-cli```| When executing into a container, force use of the external Docker CLI application, instead of by the Docker api.| +|```--use-cli```| Use the Docker application when execing into a container, instead of the Docker API.| ## Build step From a4b8163ce3883396903943ef775324d9058f062f Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:42:17 +0000 Subject: [PATCH 38/40] docs: screenshot updated --- .github/screenshot_01.png | Bin 110828 -> 97535 bytes README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png index 06b5086bd582d854cbde3282c85dcb317ad43c59..2b5195179fa55314c535a5cc9db46fc27281a3e9 100644 GIT binary patch literal 97535 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 z +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 z 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ⅅg@D^r&J3wZj`oE;mu`ZCnXm!zVn! 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%0DK1#pc;XF@r{6$vS}`=coJH_~ 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 B*hT;V diff --git a/README.md b/README.md index 593ef29..fe2ca2d 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Available command line arguments |```-g```| No TUI, essentially a debugging mode with limited functionality, for now.| |```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| |```--save-dir [directory]```| Save exported logs into a custom directory. Defaults to `$HOME`.| -|```--use-cli```| Use the Docker application when execing into a container, instead of the Docker API.| +|```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.| ## Build step From 7e7ce557924f32078a91d976cf71ccfa318ba423 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:01:22 +0000 Subject: [PATCH 39/40] docs: README.md table of contents --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index fe2ca2d..f093869 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@

+- [Download & install](#download--install) +- [Run](#run) +- [Build step](#build-step) +- [Tests](#tests) ## Download & install From bd7df7983d15cdc2bb413ff0d48c07af14b85ebd Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 20 Nov 2023 17:10:25 +0000 Subject: [PATCH 40/40] chore: release v0.4.0 --- .github/release-body.md | 25 +++++++++++-------------- CHANGELOG.md | 19 +++++++++++-------- Cargo.lock | 2 +- Cargo.toml | 2 +- src/ui/draw_blocks.rs | 12 ++++++------ 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/release-body.md b/.github/release-body.md index 26e3358..ff13266 100644 --- a/.github/release-body.md +++ b/.github/release-body.md @@ -1,20 +1,17 @@ -### 2023-10-21 +### 2023-11-20 ### Chores -+ docker-compose Alpine bump, [d46c425fa29f3c1d27bd57764748bae7e0b82f69] -+ dependencies updated, [e6eecbbdce9c0ccff42aa8806dddb6e3364f990c], [ec93115ece83002fa127f3358f573319e29357e1], [b36daa5aeaa354b6c4f45b7ae67ac1a6345ea1c0], [9c0de1f0feff3165d0f5b6cb5dda843c124bcfa4], [6dd953df458096aee5914411ce40e46c3f600ede] -+ Rust 1.73 linting, [21234c66c3935330ccd58543dd3a915a293ac776] ++ workflow dependencies updated, [6a4cf6490d08b976734e2bc8186d94c095700558] ++ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53], [81d5b326db8881263f2c9072e1426948e41b4a0f], [294cc2684f42daab9d51601e235a384f55617678] ++ lints moved from main.rs to Cargo.toml, [2de76e2f358be9c1500ca3dc4f9df0979ed8ed28] ++ .devcontainer updated, [37d2ee915625806dd11c2cc816a892aae12a777c] -### Docs -+ README.md updated, [3fd3915b3e929742d8007109fd4c7b4a345eb0fa] - -### Refactors -+ LogsTZ from `&str`, [44f581f5b3652cc4e623fe145141878754dca292] -+ from string impl, [ca79893df5f05ebf445ce194d578cb8213c9755e] -+ env handling, [18c3ed43376a8b5e2d285d1b34a9f96843357d53] -+ `parse_args/mod.rs` > `parse_args.rs`, [a6ff4124319ed17d3f1c46c916418f850ef1d3b0] -+ set_info_box take `&str`, [faeaca0cd1bb243c7f4a7112b928be776b877ca1] -+ GitHub action use concurrency matrix, re-roder workflow, [85f1982f4066bfdbc764ab7b88588eded6a17f96] +### Features ++ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key, closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff], [0e5ee143b008c9d0ee0b681231a1568be227150b], [0e5ee143b008c9d0ee0b681231a1568be227150b] ++ Export logs feature, press `s` to save logs, use `--save-dir` cli-arg to customise output location, closes #1, [a15da5ed43d07852504a4dd1884a189e3f5b9d84] +### Fixes ++ `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8] ++ sorted created_at clash, closes #22, [3a6489396e87702ce94b349a7f47028ece7922f6] see CHANGELOG.md for more details diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a83c8..e420888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,19 @@ +# v0.4.0 +### 2023-11-20 + ### Chores -+ workflow dependencies updated, [6a4cf6490d08b976734e2bc8186d94c095700558] -+ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53], [81d5b326db8881263f2c9072e1426948e41b4a0f], [294cc2684f42daab9d51601e235a384f55617678] -+ lints moved from main.rs to Cargo.toml, [2de76e2f358be9c1500ca3dc4f9df0979ed8ed28] -+ .devcontainer updated, [37d2ee915625806dd11c2cc816a892aae12a777c] ++ workflow dependencies updated, [6a4cf649](https://github.com/mrjackwills/oxker/commit/6a4cf6490d08b976734e2bc8186d94c095700558) ++ dependencies updated, [e301b518](https://github.com/mrjackwills/oxker/commit/e301b51891e03ea40b2f904583119da3bc4daf53), [81d5b326](https://github.com/mrjackwills/oxker/commit/81d5b326db8881263f2c9072e1426948e41b4a0f), [294cc268](https://github.com/mrjackwills/oxker/commit/294cc2684f42daab9d51601e235a384f55617678) ++ lints moved from main.rs to Cargo.toml, [2de76e2f](https://github.com/mrjackwills/oxker/commit/2de76e2f358be9c1500ca3dc4f9df0979ed8ed28) ++ .devcontainer updated, [37d2ee91](https://github.com/mrjackwills/oxker/commit/37d2ee915625806dd11c2cc816a892aae12a777c) ### Features -+ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key, closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff], [0e5ee143b008c9d0ee0b681231a1568be227150b], [0e5ee143b008c9d0ee0b681231a1568be227150b] -+ Export logs feature, press `s` to save logs, use `--save-dir` cli-arg to customise output location, closes #1, [a15da5ed43d07852504a4dd1884a189e3f5b9d84] ++ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key, closes [#28](https://github.com/mrjackwills/oxker/issues/28), [c8077bca](https://github.com/mrjackwills/oxker/commit/c8077bca0b673478cfbb417e677a885136ba9eff), [0e5ee143](https://github.com/mrjackwills/oxker/commit/0e5ee143b008c9d0ee0b681231a1568be227150b), [0e5ee143](https://github.com/mrjackwills/oxker/commit/0e5ee143b008c9d0ee0b681231a1568be227150b) ++ Export logs feature, press `s` to save logs, use `--save-dir` cli-arg to customise output location, closes [#1](https://github.com/mrjackwills/oxker/issues/1), [a15da5ed](https://github.com/mrjackwills/oxker/commit/a15da5ed43d07852504a4dd1884a189e3f5b9d84) ### Fixes -+ `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8] -+ sorted created_at clash, closes #22, [3a6489396e87702ce94b349a7f47028ece7922f6] ++ `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c1](https://github.com/mrjackwills/oxker/commit/0e06c9c172629dc7f7e7766f5372da9466e786d8) ++ sorted created_at clash, closes [#22](https://github.com/mrjackwills/oxker/issues/22), [3a648939](https://github.com/mrjackwills/oxker/commit/3a6489396e87702ce94b349a7f47028ece7922f6) # v0.3.3 ### 2023-10-21 diff --git a/Cargo.lock b/Cargo.lock index 6c010f8..aa369d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -781,7 +781,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "bollard", diff --git a/Cargo.toml b/Cargo.toml index 40cadd6..9ce8d4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxker" -version = "0.3.3" +version = "0.4.0" edition = "2021" authors = ["Jack Wills "] description = "A simple tui to view & control docker containers" diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index bd5bb07..b38be52 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -603,12 +603,12 @@ impl HelpInfo { button_desc("save logs to file"), ]), Line::from(vec![ - space(), - button_item("m"), - button_desc( - "toggle mouse capture - if disabled, text on screen can be selected & copied", - ), - ]), + space(), + button_item("m"), + button_desc( + "toggle mouse capture - if disabled, text on screen can be selected & copied", + ), + ]), Line::from(vec![space(), button_item("0"), button_desc("stop sort")]), Line::from(vec![ space(),