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()