use parking_lot::Mutex;
use ratatui::layout::{Constraint, Rect};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::Instant,
};
use tokio::task::JoinHandle;
use uuid::Uuid;
use crate::{
app_data::{AppData, ContainerId, Header, ScrollDirection},
exec::ExecMode,
};
use super::Rerender;
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
pub enum SelectablePanel {
#[default]
Containers,
Commands,
Logs,
}
impl SelectablePanel {
pub const fn title(self) -> &'static str {
match self {
Self::Containers => "Containers",
Self::Logs => "Logs",
Self::Commands => "",
}
}
pub const fn next(self) -> Self {
match self {
Self::Containers => Self::Commands,
Self::Commands => Self::Logs,
Self::Logs => Self::Containers,
}
}
pub const fn prev(self) -> Self {
match self {
Self::Containers => Self::Logs,
Self::Commands => Self::Containers,
Self::Logs => Self::Commands,
}
}
}
#[derive(Debug, Copy, Clone)]
pub enum Region {
Panel(SelectablePanel),
Header(Header),
HelpPanel,
Delete(DeleteButton),
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum DeleteButton {
Confirm,
Cancel,
}
#[allow(unused)]
#[derive(Debug, Clone, Copy)]
pub enum BoxLocation {
TopLeft,
TopCentre,
TopRight,
MiddleLeft,
MiddleCentre,
MiddleRight,
BottomLeft,
BottomCentre,
BottomRight,
}
impl BoxLocation {
pub const fn get_indexes(self) -> (usize, usize) {
match self {
Self::TopLeft => (0, 0),
Self::TopCentre => (0, 1),
Self::TopRight => (0, 2),
Self::MiddleLeft => (1, 0),
Self::MiddleCentre => (1, 1),
Self::MiddleRight => (1, 2),
Self::BottomLeft => (2, 0),
Self::BottomCentre => (2, 1),
Self::BottomRight => (2, 2),
}
}
pub const fn get_constraints(
self,
blank_horizontal: u16,
blank_vertical: u16,
text_lines: u16,
text_width: u16,
) -> ([Constraint; 3], [Constraint; 3]) {
(
Self::get_horizontal_constraints(self, blank_horizontal, text_width),
Self::get_vertical_constraints(self, blank_vertical, text_lines),
)
}
const fn get_horizontal_constraints(
self,
blank_horizontal: u16,
text_width: u16,
) -> [Constraint; 3] {
match self {
Self::TopLeft | Self::MiddleLeft | Self::BottomLeft => [
Constraint::Min(text_width),
Constraint::Max(blank_horizontal),
Constraint::Max(blank_horizontal),
],
Self::TopCentre | Self::MiddleCentre | Self::BottomCentre => [
Constraint::Max(blank_horizontal),
Constraint::Min(text_width),
Constraint::Max(blank_horizontal),
],
Self::TopRight | Self::MiddleRight | Self::BottomRight => [
Constraint::Max(blank_horizontal),
Constraint::Max(blank_horizontal),
Constraint::Min(text_width),
],
}
}
const fn get_vertical_constraints(
self,
blank_vertical: u16,
number_lines: u16,
) -> [Constraint; 3] {
match self {
Self::TopLeft | Self::TopCentre | Self::TopRight => [
Constraint::Min(number_lines),
Constraint::Max(blank_vertical),
Constraint::Max(blank_vertical),
],
Self::MiddleLeft | Self::MiddleCentre | Self::MiddleRight => [
Constraint::Max(blank_vertical),
Constraint::Min(number_lines),
Constraint::Max(blank_vertical),
],
Self::BottomLeft | Self::BottomCentre | Self::BottomRight => [
Constraint::Max(blank_vertical),
Constraint::Max(blank_vertical),
Constraint::Min(number_lines),
],
}
}
}
const FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const FRAMES_LEN: u8 = 9;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Status {
DeleteConfirm,
DockerConnect(Option<String>),
Error,
Exec,
Filter,
Help,
Init,
Inspect,
Logs,
SearchLogs,
}
#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
pub struct ScrollOffset {
pub x: usize,
pub y: usize,
}
#[derive(Debug)]
pub struct GuiState {
delete_container_id: Option<ContainerId>,
exec_mode: Option<ExecMode>,
intersect_delete: HashMap<DeleteButton, Rect>,
intersect_heading: HashMap<Header, Rect>,
intersect_help: Option<Rect>,
intersect_panel: HashMap<SelectablePanel, Rect>,
loading_handle: Option<JoinHandle<()>>,
loading_index: u8,
loading_set: HashSet<Uuid>,
log_height: u16,
rerender: Arc<Rerender>,
selected_panel: SelectablePanel,
screen_width: u16,
show_logs: bool,
inspect_offset: ScrollOffset,
inspect_offset_max: ScrollOffset,
status: HashSet<Status>,
pub info_box_text: Option<(String, Instant)>,
}
impl GuiState {
pub fn new(redraw: &Arc<Rerender>, show_logs: bool) -> Self {
Self {
delete_container_id: None,
exec_mode: None,
info_box_text: None,
intersect_delete: HashMap::new(),
intersect_heading: HashMap::new(),
intersect_help: None,
intersect_panel: HashMap::new(),
inspect_offset: ScrollOffset::default(),
inspect_offset_max: ScrollOffset::default(),
loading_handle: None,
loading_index: 0,
loading_set: HashSet::new(),
log_height: 75,
screen_width: 0,
rerender: Arc::clone(redraw),
selected_panel: SelectablePanel::default(),
show_logs,
status: HashSet::new(),
}
}
pub fn log_height_increase(&mut self) {
if self.show_logs && self.log_height <= 75 {
self.log_height = self.log_height.saturating_add(5);
self.rerender.update_draw();
}
}
pub fn log_height_decrease(&mut self) {
if self.show_logs {
self.log_height = self.log_height.saturating_sub(5);
if self.log_height == 0 && self.selected_panel == SelectablePanel::Logs {
self.show_logs = false;
self.selected_panel = SelectablePanel::Containers;
}
self.rerender.update_draw();
}
}
pub fn set_inspect_offset(&mut self, sd: &ScrollDirection) {
match sd {
ScrollDirection::Up => self.inspect_offset.y = self.inspect_offset.y.saturating_sub(1),
ScrollDirection::Down => {
self.inspect_offset.y = self
.inspect_offset
.y
.saturating_add(1)
.min(self.inspect_offset_max.y)
}
ScrollDirection::Left => {
self.inspect_offset.x = self.inspect_offset.x.saturating_sub(1)
}
ScrollDirection::Right => {
self.inspect_offset.x = self
.inspect_offset
.x
.saturating_add(1)
.min(self.inspect_offset_max.x)
}
}
self.rerender.update_draw();
}
pub fn get_inspect_offset(&self) -> ScrollOffset {
self.inspect_offset
}
pub fn set_inspect_offset_max(&mut self, offset: ScrollOffset) {
self.inspect_offset_max = offset
}
pub fn set_inspect_offset_y_to_max(&mut self) {
self.inspect_offset.y = self.inspect_offset_max.y;
self.rerender.update_draw();
}
pub fn clear_inspect_offset(&mut self) {
self.inspect_offset.x = 0;
self.inspect_offset.y = 0;
self.inspect_offset_max = ScrollOffset::default();
self.rerender.update_draw();
}
pub const fn set_screen_width(&mut self, width: u16) {
self.screen_width = width;
}
pub const fn get_screen_width(&self) -> u16 {
self.screen_width
}
pub const fn get_show_logs(&self) -> bool {
self.show_logs
}
pub fn toggle_show_logs(&mut self) {
self.show_logs = !self.show_logs;
if !self.show_logs && self.selected_panel == SelectablePanel::Logs {
self.selected_panel = SelectablePanel::Containers;
}
self.rerender.update_draw();
}
#[cfg(test)]
pub const fn log_height_zero(&mut self) {
self.log_height = 0;
}
pub const fn get_log_height(&self) -> u16 {
self.log_height
}
pub fn clear_area_map(&mut self) {
self.intersect_panel.clear();
}
pub fn set_clear(&self) {
self.rerender.set_clear();
}
pub const fn get_selected_panel(&self) -> SelectablePanel {
self.selected_panel
}
pub fn check_panel_intersect(&mut self, rect: Rect) {
if let Some(data) = self
.intersect_panel
.iter()
.filter(|i| i.1.intersects(rect))
.collect::<Vec<_>>()
.first()
{
self.selected_panel = *data.0;
self.rerender.update_draw();
}
}
pub fn get_intersect_button(&self, rect: Rect) -> Option<DeleteButton> {
self.intersect_delete
.iter()
.filter(|i| i.1.intersects(rect))
.collect::<Vec<_>>()
.first()
.map(|data| *data.0)
}
pub fn get_intersect_header(&self, rect: Rect) -> Option<Header> {
self.intersect_heading
.iter()
.filter(|i| i.1.intersects(rect))
.collect::<Vec<_>>()
.first()
.map(|data| *data.0)
}
pub fn get_intersect_help(&self, rect: Rect) -> bool {
self.intersect_help
.as_ref()
.is_some_and(|i| i.intersects(rect))
}
pub fn update_region_map(&mut self, region: Region, area: Rect) {
match region {
Region::Header(header) => {
self.intersect_heading
.entry(header)
.and_modify(|w| *w = area)
.or_insert(area);
}
Region::Panel(panel) => {
self.intersect_panel
.entry(panel)
.and_modify(|w| *w = area)
.or_insert(area);
}
Region::Delete(button) => {
self.intersect_delete
.entry(button)
.and_modify(|w| *w = area)
.or_insert(area);
}
Region::HelpPanel => {
self.intersect_help = Some(area);
}
}
}
pub fn get_delete_container(&self) -> Option<ContainerId> {
self.delete_container_id.clone()
}
pub fn set_delete_container(&mut self, id: Option<ContainerId>) {
if id.is_some() {
self.status.insert(Status::DeleteConfirm);
} else {
self.intersect_delete.clear();
self.status_del(Status::DeleteConfirm);
}
self.delete_container_id = id;
self.rerender.update_draw();
}
pub fn get_status(&self) -> HashSet<Status> {
self.status.clone()
}
pub fn status_del(&mut self, status: Status) {
self.status.remove(&status);
match status {
Status::DeleteConfirm => {
self.status.remove(&Status::DeleteConfirm);
}
Status::Exec => {
self.exec_mode = None;
}
_ => (),
}
self.rerender.update_draw();
}
pub fn set_exec_mode(&mut self, mode: ExecMode) {
self.exec_mode = Some(mode);
self.status.insert(Status::Exec);
self.rerender.update_draw();
}
pub fn get_exec_mode(&self) -> Option<ExecMode> {
self.exec_mode.clone()
}
pub fn status_push(&mut self, status: Status) {
if status != Status::Exec {
self.status.insert(status);
self.rerender.update_draw();
}
}
pub fn set_logs_panel_selected(&mut self, app_data: &Arc<Mutex<AppData>>) {
self.selected_panel = SelectablePanel::Logs;
if (app_data.lock().get_container_len() == 0
&& self.get_selected_panel() == SelectablePanel::Commands)
|| (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs)
{
self.selected_panel = self.selected_panel.next();
}
self.rerender.update_draw();
}
pub fn selectable_panel_next(&mut self, app_data: &Arc<Mutex<AppData>>) {
self.selected_panel = self.selected_panel.next();
if (app_data.lock().get_container_len() == 0
&& self.get_selected_panel() == SelectablePanel::Commands)
|| (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs)
{
self.selected_panel = self.selected_panel.next();
}
self.rerender.update_draw();
}
pub fn selectable_panel_previous(&mut self, app_data: &Arc<Mutex<AppData>>) {
self.selected_panel = self.selected_panel.prev();
if (app_data.lock().get_container_len() == 0
&& self.get_selected_panel() == SelectablePanel::Commands)
|| (self.log_height == 0 && self.get_selected_panel() == SelectablePanel::Logs)
{
self.selected_panel = self.selected_panel.prev();
}
self.rerender.update_draw();
}
pub fn next_loading(&mut self, uuid: Uuid) {
if self.loading_index == FRAMES_LEN {
self.loading_index = 0;
} else {
self.loading_index += 1;
}
self.loading_set.insert(uuid);
self.rerender.update_draw();
}
pub fn is_loading(&self) -> bool {
!self.loading_set.is_empty()
}
pub fn get_loading(&self) -> char {
if self.is_loading() {
FRAMES[usize::from(self.loading_index)]
} else {
' '
}
}
pub fn start_loading_animation(gui_state: &Arc<Mutex<Self>>, loading_uuid: Uuid) {
if !gui_state.lock().is_loading() {
let inner_state = Arc::clone(gui_state);
gui_state.lock().loading_handle = Some(tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
inner_state.lock().next_loading(loading_uuid);
}
}));
}
gui_state.lock().next_loading(loading_uuid);
}
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
self.loading_set.remove(&loading_uuid);
self.rerender.update_draw();
if self.loading_set.is_empty() {
self.loading_index = 0;
if let Some(h) = &self.loading_handle {
h.abort();
}
self.loading_handle = None;
}
}
pub fn set_info_box(&mut self, text: &str) {
self.info_box_text = Some((text.to_owned(), std::time::Instant::now()));
self.rerender.update_draw();
}
pub fn reset_info_box(&mut self) {
self.info_box_text = None;
self.rerender.update_draw();
}
}