Files
oxkerclone/src/ui/gui_state.rs
T
Jack Wills d5d8a0dbc5 feat: filter containers, closes #37
Enable filtering of containers, toggled by pressing `F1` or `/`, build on PR #38 from MohammadShabaniSBU
2024-07-12 15:43:42 +00:00

382 lines
12 KiB
Rust

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::{ContainerId, Header},
exec::ExecMode,
};
#[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),
Delete(DeleteButton),
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum DeleteButton {
Yes,
No,
}
#[allow(unused)]
#[derive(Debug, Clone, Copy)]
pub enum BoxLocation {
TopLeft,
TopCentre,
TopRight,
MiddleLeft,
MiddleCentre,
MiddleRight,
BottomLeft,
BottomCentre,
BottomRight,
}
impl BoxLocation {
/// Screen is divided into 3x3 sections
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),
}
}
/// Get both the vertical and hoziztonal constrains
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),
],
}
}
}
// loading animation frames
const FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const FRAMES_LEN: u8 = 9;
/// The application gui state can be in multiple of these four states at the same time
/// Various functions (e.g input handler), operate differently depending upon current Status
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Status {
DeleteConfirm,
DockerConnect,
Error,
Exec,
Filter,
Help,
Init,
Logs,
}
/// Global gui_state, stored in an Arc<Mutex>
#[derive(Debug, Default)]
pub struct GuiState {
delete_container: Option<ContainerId>,
delete_map: HashMap<DeleteButton, Rect>,
heading_map: HashMap<Header, Rect>,
loading_handle: Option<JoinHandle<()>>,
loading_index: u8,
loading_set: HashSet<Uuid>,
panel_map: HashMap<SelectablePanel, Rect>,
selected_panel: SelectablePanel,
status: HashSet<Status>,
exec_mode: Option<ExecMode>,
pub info_box_text: Option<(String, Instant)>,
}
impl GuiState {
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
pub fn clear_area_map(&mut self) {
self.panel_map.clear();
}
/// Get the currently selected panel
pub const fn get_selected_panel(&self) -> SelectablePanel {
self.selected_panel
}
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
pub fn panel_intersect(&mut self, rect: Rect) {
if let Some(data) = self
.panel_map
.iter()
.filter(|i| i.1.intersects(rect))
.collect::<Vec<_>>()
.first()
{
self.selected_panel = *data.0;
}
}
/// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button
pub fn button_intersect(&mut self, rect: Rect) -> Option<DeleteButton> {
self.delete_map
.iter()
.filter(|i| i.1.intersects(rect))
.collect::<Vec<_>>()
.first()
.map(|data| *data.0)
}
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
pub fn header_intersect(&mut self, rect: Rect) -> Option<Header> {
self.heading_map
.iter()
.filter(|i| i.1.intersects(rect))
.collect::<Vec<_>>()
.first()
.map(|data| *data.0)
}
/// Insert, or updates header area panel into heading_map
pub fn update_region_map(&mut self, region: Region, area: Rect) {
match region {
Region::Header(header) => self
.heading_map
.entry(header)
.and_modify(|w| *w = area)
.or_insert(area),
Region::Panel(panel) => self
.panel_map
.entry(panel)
.and_modify(|w| *w = area)
.or_insert(area),
Region::Delete(button) => self
.delete_map
.entry(button)
.and_modify(|w| *w = area)
.or_insert(area),
};
}
/// Check if an ContainerId is set in the delete_container field
pub fn get_delete_container(&self) -> Option<ContainerId> {
self.delete_container.clone()
}
/// Set either a ContainerId, or None, to the delete_container field
/// If Some, will also insert the DeleteConfirm status into self.status
pub fn set_delete_container(&mut self, id: Option<ContainerId>) {
if id.is_some() {
self.status.insert(Status::DeleteConfirm);
} else {
self.delete_map.clear();
self.status.remove(&Status::DeleteConfirm);
}
self.delete_container = id;
}
/// Check if the current gui_status contains any of the given status'
/// Don't really like this methodology for gui state, needs a re-think
pub fn status_contains(&self, status: &[Status]) -> bool {
status.iter().any(|i| self.status.contains(i))
}
/// Remove a gui_status into the current gui_status HashSet
/// Remove exec mode & deleteConfirm is required
pub fn status_del(&mut self, status: Status) {
self.status.remove(&status);
match status {
Status::DeleteConfirm => {
self.status.remove(&Status::DeleteConfirm);
}
Status::Exec => {
self.exec_mode = None;
}
_ => (),
}
}
/// Inset the ExecMode into self, and set the Status as exec
/// Using StatusPush with Status::Exec won't insert into the hash map
/// To force self.exec_mode to be set
pub fn set_exec_mode(&mut self, mode: ExecMode) {
self.exec_mode = Some(mode);
self.status.insert(Status::Exec);
}
pub fn get_exec_mode(&mut self) -> Option<ExecMode> {
self.exec_mode.clone()
}
/// Insert a gui_status into the current gui_status HashSet
/// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead
pub fn status_push(&mut self, status: Status) {
match status {
Status::Exec => (),
_ => {
self.status.insert(status);
}
}
}
/// Change to next selectable panel
pub fn next_panel(&mut self) {
self.selected_panel = self.selected_panel.next();
}
/// Change to previous selectable panel
pub fn previous_panel(&mut self) {
self.selected_panel = self.selected_panel.prev();
}
/// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array
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);
}
pub fn is_loading(&self) -> bool {
!self.loading_set.is_empty()
}
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
pub fn get_loading(&self) -> char {
if self.is_loading() {
FRAMES[usize::from(self.loading_index)]
} else {
' '
}
}
/// Animate the loading icon in its own Tokio thread
/// This should only be able to executed once, rather than multiple spawns
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);
}
/// Stop the loading_spin function, and reset gui loading status
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
self.loading_set.remove(&loading_uuid);
if self.loading_set.is_empty() {
self.loading_index = 0;
if let Some(h) = &self.loading_handle {
h.abort();
}
self.loading_handle = None;
}
}
/// Set info box content
pub fn set_info_box(&mut self, text: &str) {
self.info_box_text = Some((text.to_owned(), std::time::Instant::now()));
}
/// Remove info box content
pub fn reset_info_box(&mut self) {
self.info_box_text = None;
}
}