use std::{
fs::OpenOptions,
io::{BufWriter, Write},
sync::{Arc, atomic::AtomicBool},
time::SystemTime,
};
use bollard::query_parameters::LogsOptions;
use cansi::v3::categorise_text;
use crossterm::{
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
execute,
};
use futures_util::StreamExt;
use parking_lot::Mutex;
use ratatui::layout::Rect;
use tokio::sync::mpsc::{Receiver, Sender};
use uuid::Uuid;
mod message;
use crate::{
app_data::{AppData, DockerCommand, Header, ScrollDirection},
app_error::AppError,
config,
docker_data::DockerMessage,
exec::{ExecMode, tty_readable},
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
};
pub use message::InputMessages;
#[derive(Debug)]
pub struct InputHandler {
app_data: Arc<Mutex<AppData>>,
docker_tx: Sender<DockerMessage>,
keymap: config::Keymap,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
mouse_capture: bool,
rx: Receiver<InputMessages>,
}
impl InputHandler {
pub async fn start(
app_data: Arc<Mutex<AppData>>,
docker_tx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
rx: Receiver<InputMessages>,
) {
let keymap = app_data.lock().config.keymap.clone();
let mut inner = Self {
app_data,
docker_tx,
gui_state,
is_running,
keymap,
rx,
mouse_capture: true,
};
inner.message_handler().await;
}
async fn message_handler(&mut self) {
while let Some(message) = self.rx.recv().await {
match message {
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
InputMessages::MouseEvent((mouse_event, modifider)) => {
let status = self.gui_state.lock().get_status();
let contains = |s: Status| status.contains(&s);
if contains(Status::DeleteConfirm) {
self.button_intersect(mouse_event).await;
} else if !contains(Status::Error)
&& !contains(Status::Help)
&& !contains(Status::DeleteConfirm)
&& !contains(Status::Filter)
&& !contains(Status::SearchLogs)
{
self.mouse_press(mouse_event, modifider);
}
}
}
}
}
fn sort(&self, selected_header: Header) {
self.app_data.lock().set_sort_by_header(selected_header);
}
fn quit(&self) {
let status = self.gui_state.lock().get_status();
let contains = |s: Status| status.contains(&s);
if !contains(Status::Error) | !contains(Status::Init) {
self.is_running
.store(false, std::sync::atomic::Ordering::SeqCst);
}
}
async fn confirm_delete(&self) {
let id = self.gui_state.lock().get_delete_container();
if let Some(id) = id {
self.docker_tx
.send(DockerMessage::Control((DockerCommand::Delete, id)))
.await
.ok();
}
}
fn clear_delete(&self) {
self.gui_state.lock().set_delete_container(None);
}
async fn inspect_key(&self) {
self.app_data.lock().clear_inspect_data();
let selected = self.app_data.lock().get_selected_container().cloned();
if let Some(g) = selected {
self.docker_tx.send(DockerMessage::Inspect(g.id)).await.ok();
}
}
async fn exec_key(&self) {
let is_oxker = self.app_data.lock().is_oxker();
if !is_oxker && tty_readable() {
let uuid = Uuid::new_v4();
GuiState::start_loading_animation(&self.gui_state, uuid);
let (sx, rx) = tokio::sync::oneshot::channel();
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
if let Ok(docker) = rx.await {
(ExecMode::new(&self.app_data, &docker).await).map_or_else(
|| {
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(uuid);
}
}
fn mouse_capture_key(&mut self) {
let err = || {
self.app_data.lock().set_error(
AppError::MouseCapture(!self.mouse_capture),
&self.gui_state,
Status::Error,
);
};
if self.mouse_capture {
if execute!(std::io::stdout(), DisableMouseCapture).is_ok() {
self.gui_state
.lock()
.set_info_box("✖ mouse capture disabled");
} else {
err();
}
} else if Ui::enable_mouse_capture().is_ok() {
self.gui_state
.lock()
.set_info_box("✓ mouse capture enabled");
} else {
err();
}
self.mouse_capture = !self.mouse_capture;
}
async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
let args = self.app_data.lock().config.clone();
let container = self.app_data.lock().get_selected_container_id_state_name();
if let Some((id, _, name)) = container
&& let Some(log_path) = args.dir_save
{
let (sx, rx) = tokio::sync::oneshot::channel();
self.docker_tx.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 options = Some(LogsOptions {
stderr: true,
stdout: true,
timestamps: args.show_timestamp,
since: 0,
..Default::default()
});
let mut logs = rx.await?.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)
.truncate(true)
.open(&path)?,
);
for line in &output {
stream.write_all(line.as_bytes())?;
}
stream.flush()?;
self.gui_state
.lock()
.set_info_box(&format!("saved to {}", path.display()));
}
}
Ok(())
}
async fn save_key(&self) {
let status = self.gui_state.lock().get_status();
let contains = |s: Status| status.contains(&s);
if !contains(Status::Logs) {
self.gui_state.lock().status_push(Status::Logs);
let uuid = Uuid::new_v4();
GuiState::start_loading_animation(&self.gui_state, uuid);
if self.save_logs().await.is_err() {
self.app_data.lock().set_error(
AppError::DockerLogs,
&self.gui_state,
Status::Error,
);
}
self.gui_state.lock().status_del(Status::Logs);
self.gui_state.lock().stop_loading_animation(uuid);
}
}
async fn enter_key(&self) {
let panel = self.gui_state.lock().get_selected_panel();
if panel == SelectablePanel::Commands {
let option_command = self.app_data.lock().selected_docker_controls();
if let Some(command) = option_command {
if self.app_data.lock().is_oxker_in_container() {
return;
}
let option_id = self.app_data.lock().get_selected_container_id();
if let Some(id) = option_id {
match command {
DockerCommand::Delete => self
.docker_tx
.send(DockerMessage::ConfirmDelete(id))
.await
.ok(),
_ => self
.docker_tx
.send(DockerMessage::Control((command, id)))
.await
.ok(),
};
}
}
}
}
fn get_modifier_total(&self, modifier: KeyModifiers) -> u8 {
if modifier == self.keymap.scroll_many {
10
} else {
1
}
}
fn inspect_scroll(&self, modifier: KeyModifiers, sd: &ScrollDirection) {
for _ in 0..self.get_modifier_total(modifier) {
self.gui_state.lock().set_inspect_offset(sd);
}
}
fn logs_horizontal_scroll(&self, modifier: KeyModifiers, sd: &ScrollDirection) {
let panel = self.gui_state.lock().get_selected_panel();
if panel == SelectablePanel::Logs {
for _ in 0..self.get_modifier_total(modifier) {
let width = self.gui_state.lock().get_screen_width();
self.app_data.lock().logs_horizontal_scroll(sd, width);
}
}
}
fn next_panel_key(&self) {
self.gui_state.lock().selectable_panel_next(&self.app_data);
}
fn previous_panel_key(&self) {
self.gui_state
.lock()
.selectable_panel_previous(&self.app_data);
}
fn scroll_start_key(&self) {
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => self.app_data.lock().containers_start(),
SelectablePanel::Logs => self.app_data.lock().log_start(),
SelectablePanel::Commands => self.app_data.lock().docker_controls_start(),
}
}
fn scroll_end_key(&self) {
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => self.app_data.lock().containers_end(),
SelectablePanel::Logs => self.app_data.lock().log_end(),
SelectablePanel::Commands => self.app_data.lock().docker_controls_end(),
}
}
fn handle_help(&mut self, key_code: KeyCode) {
if self.keymap.clear.0 == key_code
|| self.keymap.clear.1 == Some(key_code)
|| self.keymap.toggle_help.0 == key_code
|| self.keymap.toggle_help.1 == Some(key_code)
{
self.gui_state.lock().status_del(Status::Help);
}
if self.keymap.toggle_mouse_capture.0 == key_code
|| self.keymap.toggle_mouse_capture.1 == Some(key_code)
{
self.mouse_capture_key();
}
}
fn handle_error(&self, key_code: KeyCode) {
if self.keymap.clear.0 == key_code || self.keymap.clear.1 == Some(key_code) {
self.app_data.lock().remove_error();
self.gui_state.lock().status_del(Status::Error);
}
}
async fn handle_delete(&self, key_code: KeyCode) {
if self.keymap.delete_confirm.0 == key_code
|| self.keymap.delete_confirm.1 == Some(key_code)
{
self.confirm_delete().await;
} else if self.keymap.delete_deny.0 == key_code
|| self.keymap.delete_deny.1 == Some(key_code)
|| self.keymap.clear.0 == key_code
|| self.keymap.clear.1 == Some(key_code)
{
self.clear_delete();
}
}
fn handle_search_logs(&self, key_code: KeyCode, modifier: KeyModifiers) {
match key_code {
KeyCode::Esc => {
self.app_data.lock().logs_search_clear();
self.gui_state.lock().status_del(Status::SearchLogs);
}
_ if KeyCode::Enter == key_code
|| self.keymap.log_search_mode.0 == key_code
|| self.keymap.log_search_mode.1 == Some(key_code) =>
{
self.gui_state.lock().status_del(Status::SearchLogs);
}
_ if self.keymap.scroll_back.0 == key_code
|| self.keymap.scroll_back.1 == Some(key_code) =>
{
self.logs_horizontal_scroll(modifier, &ScrollDirection::Up);
}
_ if self.keymap.scroll_forward.0 == key_code
|| self.keymap.scroll_forward.1 == Some(key_code) =>
{
self.logs_horizontal_scroll(modifier, &ScrollDirection::Down);
}
_ if self.keymap.scroll_down.0 == key_code => {
self.app_data
.lock()
.log_search_scroll(&ScrollDirection::Down);
self.gui_state
.lock()
.set_logs_panel_selected(&self.app_data);
}
_ if self.keymap.scroll_up.0 == key_code => {
self.app_data.lock().log_search_scroll(&ScrollDirection::Up);
self.gui_state
.lock()
.set_logs_panel_selected(&self.app_data);
}
KeyCode::Backspace => {
self.app_data.lock().log_search_pop();
}
KeyCode::Char(x) => {
self.app_data.lock().log_search_push(x);
}
_ => (),
}
}
fn handle_inspect(&mut self, key_code: KeyCode, modifier: KeyModifiers) {
match key_code {
_ if self.keymap.inspect.0 == key_code
|| self.keymap.inspect.1 == Some(key_code)
|| self.keymap.clear.0 == key_code
|| self.keymap.clear.1 == Some(key_code) =>
{
self.app_data.lock().clear_inspect_data();
self.gui_state.lock().clear_inspect_offset();
self.gui_state.lock().status_del(Status::Inspect);
}
_ if self.keymap.scroll_down.0 == key_code
|| self.keymap.scroll_down.1 == Some(key_code) =>
{
self.inspect_scroll(modifier, &ScrollDirection::Down);
}
_ if self.keymap.scroll_up.0 == key_code
|| self.keymap.scroll_up.1 == Some(key_code) =>
{
self.inspect_scroll(modifier, &ScrollDirection::Up);
}
_ if self.keymap.scroll_forward.0 == key_code
|| self.keymap.scroll_forward.1 == Some(key_code) =>
{
self.inspect_scroll(modifier, &ScrollDirection::Right);
}
_ if self.keymap.scroll_back.0 == key_code
|| self.keymap.scroll_back.1 == Some(key_code) =>
{
self.inspect_scroll(modifier, &ScrollDirection::Left);
}
_ if self.keymap.toggle_mouse_capture.0 == key_code
|| self.keymap.toggle_mouse_capture.1 == Some(key_code) =>
{
self.mouse_capture_key();
}
_ if self.keymap.scroll_start.0 == key_code
|| self.keymap.scroll_start.1 == Some(key_code) =>
{
self.gui_state.lock().clear_inspect_offset();
}
_ if self.keymap.scroll_end.0 == key_code
|| self.keymap.scroll_end.1 == Some(key_code) =>
{
self.gui_state.lock().set_inspect_offset_y_to_max();
}
_ => (),
}
}
fn handle_filter(&self, key_code: KeyCode) {
match key_code {
KeyCode::Esc => {
self.app_data.lock().filter_term_clear();
self.gui_state.lock().status_del(Status::Filter);
}
_ if KeyCode::Enter == key_code
|| self.keymap.filter_mode.0 == key_code
|| self.keymap.filter_mode.1 == Some(key_code) =>
{
self.gui_state.lock().status_del(Status::Filter);
}
KeyCode::Backspace => {
self.app_data.lock().filter_term_pop();
}
KeyCode::Char(x) => {
self.app_data.lock().filter_term_push(x);
}
KeyCode::Right => {
self.app_data.lock().filter_by_next();
}
KeyCode::Left => {
self.app_data.lock().filter_by_prev();
}
_ => (),
}
}
fn handle_sort(&self, key_code: KeyCode) {
match key_code {
_ if self.keymap.force_redraw.0 == key_code
|| self.keymap.force_redraw.1 == Some(key_code) =>
{
self.gui_state.lock().set_clear();
}
_ if self.keymap.sort_reset.0 == key_code
|| self.keymap.sort_reset.1 == Some(key_code) =>
{
self.app_data.lock().reset_sorted();
}
_ if self.keymap.sort_by_name.0 == key_code
|| self.keymap.sort_by_name.1 == Some(key_code) =>
{
self.sort(Header::Name);
}
_ if self.keymap.sort_by_state.0 == key_code
|| self.keymap.sort_by_state.1 == Some(key_code) =>
{
self.sort(Header::State);
}
_ if self.keymap.sort_by_status.0 == key_code
|| self.keymap.sort_by_status.1 == Some(key_code) =>
{
self.sort(Header::Status);
}
_ if self.keymap.sort_by_cpu.0 == key_code
|| self.keymap.sort_by_cpu.1 == Some(key_code) =>
{
self.sort(Header::Cpu);
}
_ if self.keymap.sort_by_memory.0 == key_code
|| self.keymap.sort_by_memory.1 == Some(key_code) =>
{
self.sort(Header::Memory);
}
_ if self.keymap.sort_by_id.0 == key_code
|| self.keymap.sort_by_id.1 == Some(key_code) =>
{
self.sort(Header::Id);
}
_ if self.keymap.sort_by_image.0 == key_code
|| self.keymap.sort_by_image.1 == Some(key_code) =>
{
self.sort(Header::Image);
}
_ if self.keymap.sort_by_rx.0 == key_code
|| self.keymap.sort_by_rx.1 == Some(key_code) =>
{
self.sort(Header::Rx);
}
_ if self.keymap.sort_by_tx.0 == key_code
|| self.keymap.sort_by_tx.1 == Some(key_code) =>
{
self.sort(Header::Tx);
}
_ => (),
}
}
fn log_panel_height_increase(&self) {
self.gui_state.lock().log_height_increase();
}
fn log_panel_height_decrease(&self) {
self.gui_state.lock().log_height_decrease();
}
fn log_panel_toggle(&self) {
self.gui_state.lock().toggle_show_logs();
}
#[allow(clippy::cognitive_complexity)]
async fn handle_others(&mut self, key_code: KeyCode, modifier: KeyModifiers) {
self.handle_sort(key_code);
match key_code {
_ if self.keymap.exec.0 == key_code || self.keymap.exec.1 == Some(key_code) => {
self.exec_key().await;
}
_ if self.keymap.toggle_help.0 == key_code
|| self.keymap.toggle_help.1 == Some(key_code) =>
{
self.gui_state.lock().status_push(Status::Help);
}
_ if self.keymap.toggle_mouse_capture.0 == key_code
|| self.keymap.toggle_mouse_capture.1 == Some(key_code) =>
{
self.mouse_capture_key();
}
_ if self.keymap.log_section_height_decrease.0 == key_code
|| self.keymap.log_section_height_decrease.1 == Some(key_code) =>
{
self.log_panel_height_decrease();
}
_ if self.keymap.log_section_height_increase.0 == key_code
|| self.keymap.log_section_height_increase.1 == Some(key_code) =>
{
self.log_panel_height_increase();
}
_ if self.keymap.log_section_toggle.0 == key_code
|| self.keymap.log_section_toggle.1 == Some(key_code) =>
{
self.log_panel_toggle();
}
_ if self.keymap.save_logs.0 == key_code
|| self.keymap.save_logs.1 == Some(key_code) =>
{
self.save_key().await;
}
_ if self.keymap.inspect.0 == key_code || self.keymap.inspect.1 == Some(key_code) => {
self.inspect_key().await;
}
_ if self.keymap.select_next_panel.0 == key_code
|| self.keymap.select_next_panel.1 == Some(key_code) =>
{
self.next_panel_key();
}
_ if self.keymap.select_previous_panel.0 == key_code
|| self.keymap.select_previous_panel.1 == Some(key_code) =>
{
self.previous_panel_key();
}
_ if self.keymap.scroll_start.0 == key_code
|| self.keymap.scroll_start.1 == Some(key_code) =>
{
self.scroll_start_key();
}
_ if self.keymap.scroll_end.0 == key_code
|| self.keymap.scroll_end.1 == Some(key_code) =>
{
self.scroll_end_key();
}
_ if self.keymap.scroll_up.0 == key_code
|| self.keymap.scroll_up.1 == Some(key_code) =>
{
self.scroll(modifier, &ScrollDirection::Up);
}
_ if self.keymap.scroll_down.0 == key_code
|| self.keymap.scroll_down.1 == Some(key_code) =>
{
self.scroll(modifier, &ScrollDirection::Down);
}
_ if self.keymap.filter_mode.0 == key_code
|| self.keymap.filter_mode.1 == Some(key_code) =>
{
self.gui_state.lock().status_push(Status::Filter);
self.docker_tx.send(DockerMessage::Update).await.ok();
}
_ if self.keymap.log_search_mode.0 == key_code
|| self.keymap.log_search_mode.1 == Some(key_code) =>
{
if !self.gui_state.lock().get_show_logs() {
self.gui_state.lock().toggle_show_logs();
}
self.gui_state.lock().status_push(Status::SearchLogs);
}
_ if self.keymap.scroll_back.0 == key_code
|| self.keymap.scroll_back.1 == Some(key_code) =>
{
self.logs_horizontal_scroll(modifier, &ScrollDirection::Up);
}
_ if self.keymap.scroll_forward.0 == key_code
|| self.keymap.scroll_forward.1 == Some(key_code) =>
{
self.logs_horizontal_scroll(modifier, &ScrollDirection::Down);
}
KeyCode::Enter => self.enter_key().await,
_ => (),
}
}
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
let status = self.gui_state.lock().get_status();
let contains = |s: Status| status.contains(&s);
let contains_error = contains(Status::Error);
let contains_help = contains(Status::Help);
let contains_exec = contains(Status::Exec);
let contains_filter = contains(Status::Filter);
let contains_delete = contains(Status::DeleteConfirm);
let contains_search_logs = contains(Status::SearchLogs);
let contains_inspect = contains(Status::Inspect);
if !contains_exec {
let is_q = || key_code == self.keymap.quit.0 || Some(key_code) == self.keymap.quit.1;
if key_modifier == KeyModifiers::CONTROL && key_code == KeyCode::Char('c')
|| is_q() && !contains_filter && !contains_search_logs
{
self.quit();
}
if contains_error {
self.handle_error(key_code);
} else if contains_help {
self.handle_help(key_code);
} else if contains_filter {
self.handle_filter(key_code);
} else if contains_search_logs {
self.handle_search_logs(key_code, key_modifier);
} else if contains_delete {
self.handle_delete(key_code).await;
} else if contains_inspect {
self.handle_inspect(key_code, key_modifier);
} else {
self.handle_others(key_code, key_modifier).await;
}
}
}
async fn button_intersect(&self, mouse_event: MouseEvent) {
if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
let intersect = self.gui_state.lock().get_intersect_button(Rect::new(
mouse_event.column,
mouse_event.row,
1,
1,
));
if let Some(button) = intersect {
match button {
DeleteButton::Confirm => self.confirm_delete().await,
DeleteButton::Cancel => self.clear_delete(),
}
}
}
}
fn mouse_press(&self, mouse_event: MouseEvent, modifier: KeyModifiers) {
let status = self.gui_state.lock().get_status();
if status.contains(&Status::Inspect) {
match mouse_event.kind {
MouseEventKind::ScrollDown => self.inspect_scroll(modifier, &ScrollDirection::Down),
MouseEventKind::ScrollUp => self.inspect_scroll(modifier, &ScrollDirection::Up),
MouseEventKind::ScrollRight => {
self.inspect_scroll(modifier, &ScrollDirection::Right)
}
MouseEventKind::ScrollLeft => self.inspect_scroll(modifier, &ScrollDirection::Left),
_ => (),
}
} else if status.contains(&Status::Help) {
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
let help_intersect = self.gui_state.lock().get_intersect_help(mouse_point);
if help_intersect {
self.gui_state.lock().status_del(Status::Help);
}
} else {
match mouse_event.kind {
MouseEventKind::ScrollUp => self.scroll(modifier, &ScrollDirection::Up),
MouseEventKind::ScrollDown => self.scroll(modifier, &ScrollDirection::Down),
MouseEventKind::Down(MouseButton::Left) => {
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
let header = self.gui_state.lock().get_intersect_header(mouse_point);
if let Some(header) = header {
self.sort(header);
}
let help_intersect = self.gui_state.lock().get_intersect_help(mouse_point);
if help_intersect {
self.gui_state.lock().status_push(Status::Help);
}
self.gui_state.lock().check_panel_intersect(mouse_point);
}
_ => (),
}
}
}
fn scroll(&self, modifier: KeyModifiers, scroll: &ScrollDirection) {
let status = self.gui_state.lock().get_status();
if status.contains(&Status::SearchLogs) {
self.app_data.lock().log_search_scroll(scroll);
} else {
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => {
for _ in 0..self.get_modifier_total(modifier) {
self.app_data.lock().containers_scroll(scroll);
}
}
SelectablePanel::Logs => {
for _ in 0..self.get_modifier_total(modifier) {
self.app_data.lock().log_scroll(scroll);
}
}
SelectablePanel::Commands => self.app_data.lock().docker_controls_scroll(scroll),
}
}
}
}