feat: Docker exec mode, closes #28
This commit is contained in:
+26
-24
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+19
-39
@@ -54,7 +54,6 @@ pub struct DockerData {
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
args: CliArgs,
|
||||
binate: Binate,
|
||||
containerised: bool,
|
||||
docker: Arc<Docker>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
@@ -101,6 +100,7 @@ impl DockerData {
|
||||
spawn_id: SpawnId,
|
||||
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
||||
) {
|
||||
|
||||
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<Mutex<GuiState>>) -> 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<Mutex<GuiState>>,
|
||||
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::<StartContainerOptions<String>>)
|
||||
.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<Mutex<AppData>>,
|
||||
containerised: bool,
|
||||
docker: Docker,
|
||||
docker_rx: Receiver<DockerMessage>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
@@ -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),
|
||||
|
||||
+185
-138
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-30
@@ -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<String> {
|
||||
args.host
|
||||
@@ -77,7 +65,6 @@ fn read_docker_host(args: &CliArgs) -> Option<String> {
|
||||
/// 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<Mutex<AppData>>,
|
||||
containerised: bool,
|
||||
docker_rx: Receiver<DockerMessage>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
is_running: &Arc<AtomicBool>,
|
||||
@@ -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);
|
||||
|
||||
+31
-4
@@ -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<String>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Mutex<GuiState>>,
|
||||
name: &str,
|
||||
) {
|
||||
pub fn delete_confirm(f: &mut Frame, gui_state: &Arc<Mutex<GuiState>>, name: &str) {
|
||||
let block = Block::default()
|
||||
.title(" Confirm Delete ")
|
||||
.border_type(BorderType::Rounded)
|
||||
|
||||
+32
-5
@@ -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<Mutex>
|
||||
@@ -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<Mutex<Self>>,
|
||||
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());
|
||||
|
||||
+53
-17
@@ -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<Mutex<AppData>>,
|
||||
docker_sx: Sender<DockerMessage>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
@@ -63,7 +66,9 @@ impl Ui {
|
||||
sender: Sender<InputMessages>,
|
||||
) {
|
||||
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<Terminal<CrosstermBackend<Stdout>>> {
|
||||
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<Stdout> {
|
||||
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<Mutex<AppData>>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user