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
This commit is contained in:
Generated
+50
@@ -322,6 +322,27 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -639,6 +660,17 @@ version = "0.2.150"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
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]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.11"
|
version = "0.4.11"
|
||||||
@@ -735,6 +767,12 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -750,6 +788,7 @@ dependencies = [
|
|||||||
"cansi",
|
"cansi",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"directories",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
@@ -913,6 +952,17 @@ dependencies = [
|
|||||||
"bitflags 1.3.2",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ tracing = "0.1"
|
|||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
ratatui = "0.24"
|
ratatui = "0.24"
|
||||||
uuid = { version = "1.5", features = ["v4", "fast-rng"] }
|
uuid = { version = "1.5", features = ["v4", "fast-rng"] }
|
||||||
|
directories = "5.0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
||||||
|
|||||||
@@ -92,28 +92,30 @@ oxker
|
|||||||
In application controls
|
In application controls
|
||||||
| button| result|
|
| button| result|
|
||||||
|--|--|
|
|--|--|
|
||||||
| ```( tab )``` or ```( shift+tab )``` | change panel, clicking on a panel also changes the selected panel|
|
| ```( 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 |
|
| ```( ↑ ↓ )``` 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|
|
| ```( enter )```| Run selected docker command.|
|
||||||
| ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column |
|
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
|
||||||
| ```( 0 )``` | stop sorting |
|
| ```( 0 )``` | Stop sorting.|
|
||||||
| ```( e )``` | (attempt) to exec into the selected container |
|
| ```( e )``` | Attempt to exec into the selected container.|
|
||||||
| ```( h )``` | toggle help menu |
|
| ```( h )``` | Toggle help menu.|
|
||||||
| ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected|
|
| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
|
||||||
| ```( q )``` | to quit at any time |
|
| ```( q )``` | Quit.|
|
||||||
|
| ```( s )``` | Save logs to `$HOME/[container_name]_[timestamp].log`, or the directory set by `--logs-dir`.|
|
||||||
|
|
||||||
|
|
||||||
Available command line arguments
|
Available command line arguments
|
||||||
| argument|result|
|
| argument|result|
|
||||||
|--|--|
|
|--|--|
|
||||||
|```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. Defaults to 1000 (1 second).|
|
|```-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`).|
|
|```-r```| Show raw logs. By default, removes ANSI formatting (conflicts with `-c`).|
|
||||||
|```-c```| Attempt to color the logs (conflicts with `-r`).|
|
|```-c```| Attempt to color the logs (conflicts with `-r`).|
|
||||||
|```-t```| Remove timestamps from each log entry.|
|
|```-t```| Remove timestamps from each log entry.|
|
||||||
|```-s```| If running via Docker, will display the oxker container.|
|
|```-s```| If running via Docker, will display the oxker container.|
|
||||||
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|
|```-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
|
## Build step
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -510,9 +510,9 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the Id and State for the currently selected container - used by the exec check method
|
/// 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()
|
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
|
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::fmt;
|
|||||||
pub enum AppError {
|
pub enum AppError {
|
||||||
DockerCommand(DockerControls),
|
DockerCommand(DockerControls),
|
||||||
DockerExec,
|
DockerExec,
|
||||||
|
DockerLogs,
|
||||||
DockerConnect,
|
DockerConnect,
|
||||||
DockerInterval,
|
DockerInterval,
|
||||||
InputPoll,
|
InputPoll,
|
||||||
@@ -20,6 +21,7 @@ impl fmt::Display for AppError {
|
|||||||
match self {
|
match self {
|
||||||
Self::DockerCommand(s) => write!(f, "Unable to {s} container"),
|
Self::DockerCommand(s) => write!(f, "Unable to {s} container"),
|
||||||
Self::DockerExec => write!(f, "Unable to exec into 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::DockerConnect => write!(f, "Unable to access docker daemon"),
|
||||||
Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
|
Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
|
||||||
Self::InputPoll => write!(f, "Unable to poll user input"),
|
Self::InputPoll => write!(f, "Unable to poll user input"),
|
||||||
|
|||||||
+2
-2
@@ -133,9 +133,9 @@ impl ExecMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let use_cli = app_data.lock().args.use_cli;
|
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 state == State::Running {
|
||||||
if tty_readable() && !use_cli {
|
if tty_readable() && !use_cli {
|
||||||
if let Ok(exec) = docker
|
if let Ok(exec) = docker
|
||||||
|
|||||||
+228
-144
@@ -1,13 +1,21 @@
|
|||||||
use std::sync::{
|
use std::{
|
||||||
atomic::{AtomicBool, Ordering},
|
fs::OpenOptions,
|
||||||
Arc,
|
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::{
|
use crossterm::{
|
||||||
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
||||||
execute,
|
execute,
|
||||||
};
|
};
|
||||||
|
use futures_util::StreamExt;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use tokio::{
|
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
|
/// Sort the containers by a given header
|
||||||
fn sort(&self, selected_header: Header) {
|
fn sort(&self, selected_header: Header) {
|
||||||
self.app_data.lock().set_sort_by_header(selected_header);
|
self.app_data.lock().set_sort_by_header(selected_header);
|
||||||
@@ -191,9 +157,216 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle any keyboard button events
|
/// Toggle the mouse capture (via input of the 'm' key)
|
||||||
// TODO refactor this
|
fn m_key(&mut self) {
|
||||||
#[allow(clippy::too_many_lines)]
|
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<Mutex<AppData>>,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
docker_sender: &Sender<DockerMessage>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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::<Arc<Docker>>();
|
||||||
|
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::<String> {
|
||||||
|
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::<String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
|
||||||
let contains_delete = self
|
let contains_delete = self
|
||||||
.gui_state
|
.gui_state
|
||||||
@@ -246,52 +419,11 @@ impl InputHandler {
|
|||||||
KeyCode::Char('e' | 'E') => self.e_key().await,
|
KeyCode::Char('e' | 'E') => self.e_key().await,
|
||||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
||||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||||
KeyCode::Tab => {
|
KeyCode::Char('s' | 'S') => self.s_key().await,
|
||||||
// Skip control panel if no containers, could be refactored
|
KeyCode::Tab => self.tab_key(),
|
||||||
let is_containers = self.gui_state.lock().get_selected_panel()
|
KeyCode::BackTab => self.back_tab_key(),
|
||||||
== SelectablePanel::Containers;
|
KeyCode::Home => self.home_key(),
|
||||||
let count =
|
KeyCode::End => self.end_key(),
|
||||||
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::Up | KeyCode::Char('k' | 'K') => self.previous(),
|
KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
|
||||||
KeyCode::PageUp => {
|
KeyCode::PageUp => {
|
||||||
for _ in 0..=6 {
|
for _ in 0..=6 {
|
||||||
@@ -304,55 +436,7 @@ impl InputHandler {
|
|||||||
self.next();
|
self.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => self.enter_key().await,
|
||||||
// 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(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-2
@@ -1,4 +1,4 @@
|
|||||||
use std::process;
|
use std::{path::PathBuf, process};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
@@ -40,6 +40,10 @@ pub struct Args {
|
|||||||
/// Use "docker" cli for execing
|
/// Use "docker" cli for execing
|
||||||
#[clap(long="use-cli", short = None)]
|
#[clap(long="use-cli", short = None)]
|
||||||
pub use_cli: bool,
|
pub use_cli: bool,
|
||||||
|
|
||||||
|
/// Directory for exporting logs, defaults to `$HOME`
|
||||||
|
#[clap(long="logs-dir", short = None)]
|
||||||
|
pub logs_dir: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -47,13 +51,14 @@ pub struct Args {
|
|||||||
pub struct CliArgs {
|
pub struct CliArgs {
|
||||||
pub color: bool,
|
pub color: bool,
|
||||||
pub docker_interval: u32,
|
pub docker_interval: u32,
|
||||||
pub use_cli: bool,
|
|
||||||
pub gui: bool,
|
pub gui: bool,
|
||||||
pub host: Option<String>,
|
pub host: Option<String>,
|
||||||
pub in_container: bool,
|
pub in_container: bool,
|
||||||
|
pub logs_dir: Option<PathBuf>,
|
||||||
pub raw: bool,
|
pub raw: bool,
|
||||||
pub show_self: bool,
|
pub show_self: bool,
|
||||||
pub timestamp: bool,
|
pub timestamp: bool,
|
||||||
|
pub use_cli: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CliArgs {
|
impl CliArgs {
|
||||||
@@ -72,6 +77,11 @@ impl CliArgs {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let args = Args::parse();
|
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
|
// Quit the program if the docker update argument is 0
|
||||||
// Should maybe change it to check if less than 100
|
// Should maybe change it to check if less than 100
|
||||||
if args.docker_interval == 0 {
|
if args.docker_interval == 0 {
|
||||||
@@ -85,6 +95,7 @@ impl CliArgs {
|
|||||||
gui: !args.gui,
|
gui: !args.gui,
|
||||||
host: args.host,
|
host: args.host,
|
||||||
in_container: Self::check_if_in_container(),
|
in_container: Self::check_if_in_container(),
|
||||||
|
logs_dir,
|
||||||
raw: args.raw,
|
raw: args.raw,
|
||||||
show_self: !args.show_self,
|
show_self: !args.show_self,
|
||||||
timestamp: !args.timestamp,
|
timestamp: !args.timestamp,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use ratatui::{
|
|||||||
},
|
},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use std::default::Default;
|
use std::{default::Default, time::Instant};
|
||||||
use std::{fmt::Display, sync::Arc};
|
use std::{fmt::Display, sync::Arc};
|
||||||
|
|
||||||
use crate::app_data::{ContainerItem, Header, SortedOrder};
|
use crate::app_data::{ContainerItem, Header, SortedOrder};
|
||||||
@@ -20,7 +20,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
gui_state::{BoxLocation, DeleteButton, Region},
|
gui_state::{self, BoxLocation, DeleteButton, Region},
|
||||||
FrameData,
|
FrameData,
|
||||||
};
|
};
|
||||||
use super::{GuiState, SelectablePanel};
|
use super::{GuiState, SelectablePanel};
|
||||||
@@ -877,7 +877,7 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option<u8>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw info box in one of the 9 BoxLocations
|
/// 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<Mutex<GuiState>>) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title("")
|
.title("")
|
||||||
.title_alignment(Alignment::Center)
|
.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);
|
let area = popup(lines, max_line_width, f.size(), BoxLocation::BottomRight);
|
||||||
f.render_widget(Clear, area);
|
f.render_widget(Clear, area);
|
||||||
f.render_widget(paragraph, 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
|
/// draw a box in the one of the BoxLocations, based on max line width + number of lines
|
||||||
|
|||||||
+5
-3
@@ -3,6 +3,7 @@ use ratatui::layout::{Constraint, Rect};
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -158,12 +159,13 @@ const FRAMES_LEN: u8 = 9;
|
|||||||
/// Various functions (e.g input handler), operate differently depending upon current Status
|
/// Various functions (e.g input handler), operate differently depending upon current Status
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Exec,
|
|
||||||
DeleteConfirm,
|
DeleteConfirm,
|
||||||
DockerConnect,
|
DockerConnect,
|
||||||
Error,
|
Error,
|
||||||
|
Exec,
|
||||||
Help,
|
Help,
|
||||||
Init,
|
Init,
|
||||||
|
Logs,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global gui_state, stored in an Arc<Mutex>
|
/// Global gui_state, stored in an Arc<Mutex>
|
||||||
@@ -178,7 +180,7 @@ pub struct GuiState {
|
|||||||
selected_panel: SelectablePanel,
|
selected_panel: SelectablePanel,
|
||||||
status: HashSet<Status>,
|
status: HashSet<Status>,
|
||||||
exec_mode: Option<ExecMode>,
|
exec_mode: Option<ExecMode>,
|
||||||
pub info_box_text: Option<String>,
|
pub info_box_text: Option<(String, Instant)>,
|
||||||
}
|
}
|
||||||
impl GuiState {
|
impl GuiState {
|
||||||
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||||
@@ -366,7 +368,7 @@ impl GuiState {
|
|||||||
|
|
||||||
/// Set info box content
|
/// Set info box content
|
||||||
pub fn set_info_box(&mut self, text: &str) {
|
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
|
/// Remove info box content
|
||||||
|
|||||||
+3
-3
@@ -247,7 +247,7 @@ pub struct FrameData {
|
|||||||
height: u16,
|
height: u16,
|
||||||
help_visible: bool,
|
help_visible: bool,
|
||||||
init: bool,
|
init: bool,
|
||||||
info_text: Option<String>,
|
info_text: Option<(String, Instant)>,
|
||||||
loading_icon: String,
|
loading_icon: String,
|
||||||
selected_panel: SelectablePanel,
|
selected_panel: SelectablePanel,
|
||||||
sorted_by: Option<(Header, SortedOrder)>,
|
sorted_by: Option<(Header, SortedOrder)>,
|
||||||
@@ -347,8 +347,8 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
draw_blocks::chart(f, lower_main[1], app_data);
|
draw_blocks::chart(f, lower_main[1], app_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(info) = fd.info_text {
|
if let Some((text, instant)) = fd.info_text {
|
||||||
draw_blocks::info(f, &info);
|
draw_blocks::info(f, &text, instant, gui_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if error, and show popup if so
|
// Check if error, and show popup if so
|
||||||
|
|||||||
Reference in New Issue
Block a user