use super::Window; use crate::tui::{impls::CRUD, Frame}; use anyhow::Result as AResult; use crossterm::event::KeyCode; use std::{fmt::Display, str::FromStr}; use strum::VariantNames; use tokio::join; use tui::widgets::ListState; use u_lib::models::{Agent, AssignedJob, JobMeta}; use uuid::Uuid; use tui::layout::{Constraint, Direction, Layout}; use tui::style::{Color, Modifier, Style}; use tui::text::Spans; use tui::widgets::{Block, Borders, List, ListItem, Tabs}; #[derive(strum::Display, strum::EnumVariantNames, strum::EnumString)] pub enum UiTabs { Agents, Jobs, Map, } impl UiTabs { pub fn variants() -> &'static [&'static str] { Self::VARIANTS } pub fn index(&self) -> usize { let ss = self.to_string(); Self::VARIANTS.iter().position(|el| **el == ss).unwrap() } pub fn next(&self) -> Self { let next_idx = (self.index() + 1) % Self::VARIANTS.len(); Self::from_str(Self::VARIANTS[next_idx]).unwrap() } pub fn prev(&self) -> Self { let vlen = Self::VARIANTS.len(); let next_idx = (self.index() + vlen - 1) % vlen; Self::from_str(Self::VARIANTS[next_idx]).unwrap() } } pub struct StatefulList { pub updated: bool, pub inner: Vec, pub state: ListState, } impl StatefulList { pub async fn update(&mut self) -> AResult<()> { if !self.updated { let new_values = ::read().await?; self.inner = new_values; self.updated = true; } Ok(()) } pub async fn delete(&mut self) -> AResult<()> { if let Some(s) = self.state.selected() { let uid = self.inner[s].id(); ::delete(uid).await?; } Ok(()) } pub fn get(&self, id: Uuid) -> Option<&T> { for item in self.inner.iter() { if item.id() == id { return Some(item); } } None } } impl Default for StatefulList { fn default() -> Self { let mut state = ListState::default(); state.select(Some(0)); StatefulList { updated: false, inner: vec![], state, } } } pub struct MainWnd { pub active_tab: UiTabs, pub last_error: Option, pub agents: StatefulList, pub jobs: StatefulList, pub map: StatefulList, } impl Default for MainWnd { fn default() -> Self { MainWnd { active_tab: UiTabs::Agents, last_error: None, agents: Default::default(), jobs: Default::default(), map: Default::default(), } } } impl MainWnd { pub fn next_tab(&mut self) { self.active_tab = self.active_tab.next() } pub fn prev_tab(&mut self) { self.active_tab = self.active_tab.prev() } fn check_err(&mut self, res: AResult<()>) -> bool { if let Err(e) = res { self.last_error = Some(e.to_string()); true } else { false } } pub async fn check_updates(&mut self) { if !self.agents.updated || !self.jobs.updated || !self.map.updated { let state = self.tab_list_state(); if let None = state.selected() { state.select(Some(0)) } let (a, j, m) = join! { self.agents.update(), self.jobs.update(), self.map.update() }; for res in [a, j, m] { self.check_err(res); } } } pub fn tab_data(&self) -> Vec { match self.active_tab { UiTabs::Agents => self .agents .inner .iter() .map(|i| format!("{}: {}-{}", crop(i.id, 6), i.username, i.hostname)) .collect(), UiTabs::Jobs => self .jobs .inner .iter() .map(|i| format!("{}: {}", crop(i.id, 6), i.alias.clone().unwrap_or_default())) .collect(), UiTabs::Map => self .map .inner .iter() .map(|i| { let job = self.jobs.get(i.job_id).unwrap(); let job_id = crop(i.job_id, 6); let job_ident = if let Some(alias) = job.alias.as_ref() { format!("{} ({})", alias, job_id) } else { format!("{}", job_id) }; let agent = self.agents.get(i.agent_id).unwrap(); let agent_id = crop(i.agent_id, 6); let agent_ident = if let Some(alias) = agent.alias.as_ref() { format!("{} ({})", alias, agent_id) } else { format!("{}-{} ({})", agent.username, agent.hostname, agent_id) }; format!("{}: {} for {}", crop(i.id, 6), job_ident, agent_ident) }) .collect(), } } pub fn tab_list_state(&mut self) -> &mut ListState { match self.active_tab { UiTabs::Agents => &mut self.agents.state, UiTabs::Jobs => &mut self.jobs.state, UiTabs::Map => &mut self.map.state, } } pub fn update_tab(&mut self) { match self.active_tab { UiTabs::Agents => self.agents.updated = false, UiTabs::Jobs => self.jobs.updated = false, UiTabs::Map => self.map.updated = false, } } pub fn on_down(&mut self) { let (list_len, list_state) = match self.active_tab { UiTabs::Agents => (self.agents.inner.len(), &mut self.agents.state), UiTabs::Jobs => (self.jobs.inner.len(), &mut self.jobs.state), UiTabs::Map => (self.map.inner.len(), &mut self.map.state), }; if list_len == 0 { list_state.select(None); } else { let selected = list_state.selected().unwrap_or(0); list_state.select(Some((selected + 1) % list_len)); } } pub fn on_up(&mut self) { let (list_len, list_state) = match self.active_tab { UiTabs::Agents => (self.agents.inner.len(), &mut self.agents.state), UiTabs::Jobs => (self.jobs.inner.len(), &mut self.jobs.state), UiTabs::Map => (self.map.inner.len(), &mut self.map.state), }; if list_len == 0 { list_state.select(None); } else { let selected = list_state.selected().unwrap_or(1); list_state.select(Some((selected + list_len - 1) % list_len)); } } pub async fn delete(&mut self) { let res = match self.active_tab { UiTabs::Agents => self.agents.delete().await, UiTabs::Jobs => self.jobs.delete().await, UiTabs::Map => self.map.delete().await, }; if !self.check_err(res) { self.on_up(); } } } #[async_trait] impl Window for MainWnd { async fn handle_kbd(&mut self, k: KeyCode) -> AResult<()> { match k { KeyCode::Esc => { /*teardown()?; wh.show_cursor()?; break;*/ } KeyCode::Left => self.prev_tab(), KeyCode::Right => self.next_tab(), KeyCode::Up => self.on_up(), KeyCode::Down => self.on_down(), KeyCode::Delete => { self.delete().await; self.update_tab(); } KeyCode::F(5) => self.update_tab(), _ => (), }; Ok(()) } async fn update(&mut self) -> AResult<()> { self.check_updates().await; Ok(()) } async fn close(&mut self) { //self.show_cursor() } fn draw(&mut self, f: &mut Frame) { let size = f.size(); let chunks = Layout::default() .direction(Direction::Vertical) .margin(1) .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .split(size); let titles = UiTabs::variants() .iter() .cloned() .map(Spans::from) .collect(); let tabs = Tabs::new(titles) .block( Block::default() .title("The whole that you need to know") .borders(Borders::ALL), ) .style(Style::default().fg(Color::White)) .highlight_style(Style::default().fg(Color::Yellow)) .divider("-") .select(self.active_tab.index()); f.render_widget(tabs, chunks[0]); let tab_data = self .tab_data() .into_iter() .map(ListItem::new) .collect::>(); let list = List::new(tab_data) .block(Block::default().borders(Borders::ALL)) .highlight_style(Style::default().add_modifier(Modifier::BOLD)); f.render_stateful_widget(list, chunks[1], self.tab_list_state()); } } fn crop(data: T, retain: usize) -> String { data.to_string()[..retain].to_string() }