use std::time::{Duration, Instant};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::widgets::ListState;
use tokio::sync::mpsc;
use crate::client::client::{ChildProcessInfo, WhyBlocked};
use super::actions::{Action, ActionResult, PendingOperation, ServiceDisplayInfo};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StatusLevel {
Info,
Success,
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Focus {
Services,
Logs,
Deps,
}
pub struct App {
pub services: Vec<ServiceDisplayInfo>,
pub selected: ListState,
pub logs: Vec<String>,
pub children: Vec<ChildProcessInfo>,
pub focus: Focus,
pub log_scroll: usize,
pub following_logs: bool,
pub show_help: bool,
pub show_detail: bool,
pub show_why: bool,
pub why_info: Option<WhyBlocked>,
pub status_message: Option<(String, Instant, StatusLevel)>,
pub pending_operations: Vec<PendingOperation>,
pub connected: bool,
pub last_fetch: Option<Instant>,
action_tx: mpsc::UnboundedSender<Action>,
}
impl App {
pub fn new(action_tx: mpsc::UnboundedSender<Action>) -> Self {
let mut selected = ListState::default();
selected.select(Some(0));
Self {
services: Vec::new(),
selected,
logs: Vec::new(),
children: Vec::new(),
focus: Focus::Services,
log_scroll: 0,
following_logs: true,
show_help: false,
show_detail: false,
show_why: false,
why_info: None,
status_message: None,
pending_operations: Vec::new(),
connected: false,
last_fetch: None,
action_tx,
}
}
pub fn set_status(&mut self, msg: impl Into<String>, level: StatusLevel) {
self.status_message = Some((msg.into(), Instant::now(), level));
}
pub fn selected_service(&self) -> Option<&ServiceDisplayInfo> {
self.selected.selected().and_then(|i| self.services.get(i))
}
pub fn selected_service_name(&self) -> Option<String> {
self.selected_service().map(|s| s.name.clone())
}
pub fn should_refresh(&self) -> bool {
match self.last_fetch {
Some(time) => time.elapsed() >= Duration::from_secs(2),
None => true,
}
}
pub fn add_pending(&mut self, action: &str, service: &str) {
self.pending_operations.push(PendingOperation {
action: action.to_string(),
service: service.to_string(),
started: Instant::now(),
});
}
pub fn remove_pending(&mut self, action: &str, service: &str) {
self.pending_operations
.retain(|op| !(op.action == action && op.service == service));
}
pub fn update_pending_operations(&mut self) {
self.pending_operations
.retain(|op| op.started.elapsed() < Duration::from_secs(120));
}
#[allow(dead_code)]
pub fn close_popups(&mut self) {
self.show_help = false;
self.show_detail = false;
self.show_why = false;
}
pub fn previous_service(&mut self) {
if self.services.is_empty() {
return;
}
let i = match self.selected.selected() {
Some(i) => {
if i == 0 {
self.services.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.selected.select(Some(i));
self.following_logs = true;
self.log_scroll = 0;
}
pub fn next_service(&mut self) {
if self.services.is_empty() {
return;
}
let i = match self.selected.selected() {
Some(i) => {
if i >= self.services.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.selected.select(Some(i));
self.following_logs = true;
self.log_scroll = 0;
}
pub fn cycle_focus(&mut self) {
self.focus = match self.focus {
Focus::Services => Focus::Deps,
Focus::Deps => Focus::Logs,
Focus::Logs => Focus::Services,
};
}
pub fn scroll_logs_up(&mut self) {
if self.log_scroll > 0 {
self.log_scroll -= 1;
self.following_logs = false;
}
}
pub fn scroll_logs_down(&mut self, visible_height: usize) {
let max_scroll = self.logs.len().saturating_sub(visible_height);
if self.log_scroll < max_scroll {
self.log_scroll += 1;
}
if self.log_scroll >= max_scroll {
self.following_logs = true;
}
}
pub fn scroll_logs_to_end(&mut self) {
self.following_logs = true;
}
pub fn handle_key(&mut self, key: KeyEvent) -> Option<Action> {
if self.show_help {
self.show_help = false;
return None;
}
if self.show_why {
self.show_why = false;
return None;
}
if self.show_detail {
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
self.show_detail = false;
}
return None;
}
match (key.code, key.modifiers) {
(KeyCode::Char('Q'), _) | (KeyCode::Esc, _) => Some(Action::Quit),
(KeyCode::Char('c'), KeyModifiers::CONTROL) => Some(Action::Quit),
(KeyCode::Char('?'), _) => {
self.show_help = true;
None
}
(KeyCode::Char('w'), _) => self.selected_service_name().map(Action::FetchWhy),
(KeyCode::Enter, _) => {
if self.focus == Focus::Services {
self.show_detail = true;
}
None
}
(KeyCode::Up, _) | (KeyCode::Char('k'), KeyModifiers::NONE) => {
if self.focus == Focus::Services {
self.previous_service();
self.request_service_data();
} else if self.focus == Focus::Logs {
self.scroll_logs_up();
}
None
}
(KeyCode::Down, _) | (KeyCode::Char('j'), KeyModifiers::NONE) => {
if self.focus == Focus::Services {
self.next_service();
self.request_service_data();
} else if self.focus == Focus::Logs {
self.scroll_logs_down(30); }
None
}
(KeyCode::Tab, _) => {
self.cycle_focus();
None
}
(KeyCode::PageUp, _) => {
for _ in 0..10 {
self.scroll_logs_up();
}
None
}
(KeyCode::PageDown, _) => {
for _ in 0..10 {
self.scroll_logs_down(30);
}
None
}
(KeyCode::Char('g'), KeyModifiers::NONE) => {
self.log_scroll = 0;
self.following_logs = false;
None
}
(KeyCode::Char('G'), KeyModifiers::SHIFT) => {
self.scroll_logs_to_end();
None
}
(KeyCode::Char('s'), KeyModifiers::NONE) => self.selected_service_name().map(|name| {
self.add_pending("start", &name);
Action::StartService(name)
}),
(KeyCode::Char('S'), KeyModifiers::SHIFT) => self.selected_service_name().map(|name| {
self.add_pending("stop", &name);
Action::StopService(name)
}),
(KeyCode::Char('r'), KeyModifiers::NONE) => self.selected_service_name().map(|name| {
self.add_pending("restart", &name);
Action::RestartService(name)
}),
(KeyCode::Char('d'), KeyModifiers::NONE) => self.selected_service_name().map(|name| {
self.add_pending("remove", &name);
Action::RemoveService(name)
}),
(KeyCode::Char('D'), KeyModifiers::SHIFT) => self.selected_service_name().map(|name| {
self.add_pending("halt+remove", &name);
Action::HaltAndRemove(name)
}),
(KeyCode::Char('K'), KeyModifiers::SHIFT) => self.selected_service_name().map(|name| {
self.add_pending("kill", &name);
Action::KillService(name, "SIGKILL".to_string())
}),
(KeyCode::Char('R'), KeyModifiers::SHIFT) => {
self.set_status("Refreshing...", StatusLevel::Info);
Some(Action::FetchServices)
}
_ => None,
}
}
fn request_service_data(&self) {
if let Some(name) = self.selected_service_name() {
let _ = self.action_tx.send(Action::FetchLogs(name.clone()));
let _ = self.action_tx.send(Action::FetchChildren(name));
}
}
pub fn handle_action_result(&mut self, result: ActionResult) {
match result {
ActionResult::Services(Ok(services)) => {
let selected_name = self.selected_service_name();
self.services = services;
self.connected = true;
self.last_fetch = Some(Instant::now());
if let Some(name) = selected_name {
if let Some(idx) = self.services.iter().position(|s| s.name == name) {
self.selected.select(Some(idx));
}
}
if self.selected.selected().is_none() && !self.services.is_empty() {
self.selected.select(Some(0));
}
if let Some(idx) = self.selected.selected() {
if idx >= self.services.len() && !self.services.is_empty() {
self.selected.select(Some(self.services.len() - 1));
}
}
}
ActionResult::Services(Err(e)) => {
self.set_status(
format!("Failed to fetch services: {}", e),
StatusLevel::Error,
);
}
ActionResult::Logs(name, Ok(logs)) => {
if self.selected_service_name().as_ref() == Some(&name) {
self.logs = logs;
}
}
ActionResult::Logs(name, Err(e)) => {
if self.selected_service_name().as_ref() == Some(&name) {
self.set_status(
format!("Failed to fetch logs for {}: {}", name, e),
StatusLevel::Warning,
);
}
}
ActionResult::Children(name, Ok(children)) => {
if self.selected_service_name().as_ref() == Some(&name) {
self.children = children;
}
}
ActionResult::Children(name, Err(_)) => {
if self.selected_service_name().as_ref() == Some(&name) {
self.children.clear();
}
}
ActionResult::Why(_name, Ok(why)) => {
self.why_info = Some(why);
self.show_why = true;
}
ActionResult::Why(name, Err(e)) => {
self.set_status(
format!("Failed to get why for {}: {}", name, e),
StatusLevel::Error,
);
}
ActionResult::OperationComplete {
action,
service,
result,
} => {
self.remove_pending(&action, &service);
match result {
Ok(()) => {
self.set_status(
format!("{} {}: success", action, service),
StatusLevel::Success,
);
let _ = self.action_tx.send(Action::FetchServices);
}
Err(e) => {
self.set_status(
format!("{} {} failed: {}", action, service, e),
StatusLevel::Error,
);
}
}
}
ActionResult::Disconnected(e) => {
self.connected = false;
self.set_status(format!("Disconnected: {}", e), StatusLevel::Error);
}
ActionResult::Reconnected => {
self.connected = true;
self.set_status("Reconnected to zinit", StatusLevel::Success);
}
}
}
}