520 lines
18 KiB
Rust
520 lines
18 KiB
Rust
use bollard::models::ContainerSummary;
|
|
use core::fmt;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
use tui::widgets::ListItem;
|
|
|
|
mod container_state;
|
|
|
|
use crate::{app_error::AppError, parse_args::CliArgs, ui::log_sanitizer};
|
|
pub use container_state::*;
|
|
|
|
/// Global app_state, stored in an Arc<Mutex>
|
|
#[derive(Debug)]
|
|
pub struct AppData {
|
|
args: CliArgs,
|
|
error: Option<AppError>,
|
|
logs_parsed: bool,
|
|
pub containers: StatefulList<ContainerItem>,
|
|
pub init: bool,
|
|
pub show_error: bool,
|
|
sorted_by: Option<(Header, SortedOrder)>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum SortedOrder {
|
|
Asc,
|
|
Desc,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
|
|
pub enum Header {
|
|
State,
|
|
Status,
|
|
Cpu,
|
|
Memory,
|
|
Id,
|
|
Name,
|
|
Image,
|
|
Rx,
|
|
Tx,
|
|
}
|
|
|
|
/// Convert errors into strings to display
|
|
impl fmt::Display for Header {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let disp = match self {
|
|
Self::State => "state",
|
|
Self::Status => "status",
|
|
Self::Cpu => "cpu",
|
|
Self::Memory => "memory/limit",
|
|
Self::Id => "id",
|
|
Self::Name => "name",
|
|
Self::Image => "image",
|
|
Self::Rx => "↓ rx",
|
|
Self::Tx => "↑ tx",
|
|
};
|
|
write!(f, "{:>x$}", disp, x = f.width().unwrap_or(1))
|
|
}
|
|
}
|
|
|
|
impl AppData {
|
|
pub fn get_sorted(&self) -> Option<(Header, SortedOrder)> {
|
|
self.sorted_by.clone()
|
|
}
|
|
|
|
/// Change the sorted order, also set the selected container state to match new order
|
|
pub fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
|
|
self.sorted_by = x;
|
|
let id = self.get_selected_container_id();
|
|
self.sort_containers();
|
|
self.containers.state.select(
|
|
self.containers
|
|
.items
|
|
.iter()
|
|
.position(|i| Some(i.id.clone()) == id),
|
|
);
|
|
}
|
|
/// Generate a default app_state
|
|
pub fn default(args: CliArgs) -> Self {
|
|
Self {
|
|
args,
|
|
containers: StatefulList::new(vec![]),
|
|
error: None,
|
|
init: false,
|
|
logs_parsed: false,
|
|
show_error: false,
|
|
sorted_by: None,
|
|
}
|
|
}
|
|
|
|
/// Current time as unix timestamp
|
|
#[allow(clippy::expect_used)]
|
|
fn get_systemtime() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("In our known reality, this error should never occur")
|
|
.as_secs()
|
|
}
|
|
|
|
/// Get the current select docker command
|
|
/// So know which command to execute
|
|
pub fn get_docker_command(&self) -> Option<DockerControls> {
|
|
let mut output = None;
|
|
if let Some(index) = self.containers.state.selected() {
|
|
if let Some(control_index) = self.containers.items[index]
|
|
.docker_controls
|
|
.state
|
|
.selected()
|
|
{
|
|
output =
|
|
Some(self.containers.items[index].docker_controls.items[control_index].clone());
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
/// Change selected choice of docker commands of selected container
|
|
pub fn docker_command_next(&mut self) {
|
|
if let Some(index) = self.containers.state.selected() {
|
|
self.containers.items[index].docker_controls.next();
|
|
}
|
|
}
|
|
|
|
/// Change selected choice of docker commands of selected container
|
|
pub fn docker_command_previous(&mut self) {
|
|
if let Some(index) = self.containers.state.selected() {
|
|
self.containers.items[index].docker_controls.previous();
|
|
}
|
|
}
|
|
|
|
/// Change selected choice of docker commands of selected container
|
|
pub fn docker_command_start(&mut self) {
|
|
if let Some(index) = self.containers.state.selected() {
|
|
self.containers.items[index].docker_controls.start();
|
|
}
|
|
}
|
|
|
|
/// Change selected choice of docker commands of selected container
|
|
pub fn docker_command_end(&mut self) {
|
|
if let Some(index) = self.containers.state.selected() {
|
|
self.containers.items[index].docker_controls.end();
|
|
}
|
|
}
|
|
|
|
/// return single app_state error
|
|
pub fn get_error(&self) -> Option<AppError> {
|
|
self.error.clone()
|
|
}
|
|
|
|
/// remove single app_state error
|
|
pub fn remove_error(&mut self) {
|
|
self.error = None;
|
|
}
|
|
|
|
/// insert single app_state error
|
|
pub fn set_error(&mut self, error: AppError) {
|
|
self.error = Some(error);
|
|
}
|
|
|
|
/// Find the id of the currently selected container.
|
|
/// If any containers on system, will always return a string.
|
|
/// Only returns None when no containers found.
|
|
pub fn get_selected_container_id(&self) -> Option<String> {
|
|
let mut output = None;
|
|
if let Some(index) = self.containers.state.selected() {
|
|
let id = self
|
|
.containers
|
|
.items
|
|
.iter()
|
|
.skip(index)
|
|
.take(1)
|
|
.map(|i| i.id.clone())
|
|
.collect::<String>();
|
|
output = Some(id);
|
|
}
|
|
output
|
|
}
|
|
|
|
/// Sort the containers vec, based on a heading, either ascending or descending
|
|
pub fn sort_containers(&mut self) {
|
|
if let Some((head, so)) = self.sorted_by.as_ref() {
|
|
match head {
|
|
Header::State => match so {
|
|
SortedOrder::Desc => self
|
|
.containers
|
|
.items
|
|
.sort_by(|a, b| a.state.order().cmp(b.state.order())),
|
|
SortedOrder::Asc => self
|
|
.containers
|
|
.items
|
|
.sort_by(|a, b| b.state.order().cmp(a.state.order())),
|
|
},
|
|
Header::Status => match so {
|
|
SortedOrder::Asc => self
|
|
.containers
|
|
.items
|
|
.sort_by(|a, b| a.status.cmp(&b.status)),
|
|
SortedOrder::Desc => self
|
|
.containers
|
|
.items
|
|
.sort_by(|a, b| b.status.cmp(&a.status)),
|
|
},
|
|
Header::Cpu => match so {
|
|
SortedOrder::Asc => self
|
|
.containers
|
|
.items
|
|
.sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())),
|
|
SortedOrder::Desc => self
|
|
.containers
|
|
.items
|
|
.sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())),
|
|
},
|
|
Header::Memory => match so {
|
|
SortedOrder::Asc => self
|
|
.containers
|
|
.items
|
|
.sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())),
|
|
SortedOrder::Desc => self
|
|
.containers
|
|
.items
|
|
.sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())),
|
|
},
|
|
Header::Id => match so {
|
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)),
|
|
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)),
|
|
},
|
|
Header::Image => match so {
|
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)),
|
|
SortedOrder::Desc => {
|
|
self.containers.items.sort_by(|a, b| b.image.cmp(&a.image));
|
|
}
|
|
},
|
|
Header::Name => match so {
|
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)),
|
|
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)),
|
|
},
|
|
Header::Rx => match so {
|
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.rx.cmp(&b.rx)),
|
|
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.rx.cmp(&a.rx)),
|
|
},
|
|
Header::Tx => match so {
|
|
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.tx.cmp(&b.tx)),
|
|
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.tx.cmp(&a.tx)),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Find the index of the currently selected single log line
|
|
pub fn get_selected_log_index(&self) -> Option<usize> {
|
|
let mut output = None;
|
|
if let Some(id) = self.get_selected_container_id() {
|
|
if let Some(index) = self.containers.items.iter().position(|i| i.id == id) {
|
|
output = Some(index);
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
/// Get the title for log panel for selected container
|
|
/// will be "logs x/x"
|
|
pub fn get_log_title(&self) -> String {
|
|
self.get_selected_log_index().map_or_else(
|
|
|| String::from(""),
|
|
|index| self.containers.items[index].logs.get_state_title(),
|
|
)
|
|
}
|
|
|
|
/// select next selected log line
|
|
pub fn log_next(&mut self) {
|
|
if let Some(index) = self.get_selected_log_index() {
|
|
self.containers.items[index].logs.next();
|
|
}
|
|
}
|
|
|
|
/// select previous selected log line
|
|
pub fn log_previous(&mut self) {
|
|
if let Some(index) = self.get_selected_log_index() {
|
|
self.containers.items[index].logs.previous();
|
|
}
|
|
}
|
|
|
|
/// select last selected log line
|
|
pub fn log_end(&mut self) {
|
|
if let Some(index) = self.get_selected_log_index() {
|
|
self.containers.items[index].logs.end();
|
|
}
|
|
}
|
|
|
|
/// select first selected log line
|
|
pub fn log_start(&mut self) {
|
|
if let Some(index) = self.get_selected_log_index() {
|
|
self.containers.items[index].logs.start();
|
|
}
|
|
}
|
|
|
|
pub fn initialised(&mut self, all_ids: &[(bool, String)]) -> bool {
|
|
let count_is_running = all_ids.iter().filter(|i| i.0).count();
|
|
let number_with_cpu_status = self
|
|
.containers
|
|
.items
|
|
.iter()
|
|
.filter(|i| !i.cpu_stats.is_empty())
|
|
.count();
|
|
self.logs_parsed && count_is_running == number_with_cpu_status
|
|
}
|
|
|
|
/// Just get the total number of containers
|
|
pub fn get_container_len(&self) -> usize {
|
|
self.containers.items.len()
|
|
}
|
|
|
|
/// Find the widths for the strings in the containers panel.
|
|
/// So can display nicely and evenly
|
|
pub fn get_width(&self) -> Columns {
|
|
let mut output = Columns::new();
|
|
// Think this is causing issues
|
|
let count = |x: &String| x.chars().count();
|
|
// let count = |_:&String|10;
|
|
|
|
for container in &self.containers.items {
|
|
let cpu_count = count(
|
|
&container
|
|
.cpu_stats
|
|
.back()
|
|
.unwrap_or(&CpuStats::new(0.0))
|
|
.to_string(),
|
|
);
|
|
let mem_count = count(&format!(
|
|
"{} / {}",
|
|
container.mem_stats.back().unwrap_or(&ByteStats::new(0)),
|
|
container.mem_limit
|
|
));
|
|
|
|
let rx_count = count(&container.rx.to_string());
|
|
let tx_count = count(&container.tx.to_string());
|
|
let image_count = count(&container.image);
|
|
let name_count = count(&container.name);
|
|
let state_count = count(&container.state.to_string());
|
|
let status_count = count(&container.status);
|
|
|
|
if cpu_count > output.cpu.1 {
|
|
output.cpu.1 = cpu_count;
|
|
};
|
|
if image_count > output.image.1 {
|
|
output.image.1 = image_count;
|
|
};
|
|
if mem_count > output.mem.1 {
|
|
output.mem.1 = mem_count;
|
|
};
|
|
if name_count > output.name.1 {
|
|
output.name.1 = name_count;
|
|
};
|
|
if state_count > output.state.1 {
|
|
output.state.1 = state_count;
|
|
};
|
|
if status_count > output.status.1 {
|
|
output.status.1 = status_count;
|
|
};
|
|
if rx_count > output.net_rx.1 {
|
|
output.net_rx.1 = rx_count;
|
|
};
|
|
if tx_count > output.net_tx.1 {
|
|
output.net_tx.1 = tx_count;
|
|
};
|
|
}
|
|
output
|
|
}
|
|
|
|
/// Get all containers ids
|
|
pub fn get_all_ids(&self) -> Vec<String> {
|
|
self.containers
|
|
.items
|
|
.iter()
|
|
.map(|i| i.id.clone())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
/// find container given id
|
|
fn get_container_by_id(&mut self, id: &str) -> Option<&mut ContainerItem> {
|
|
self.containers.items.iter_mut().find(|i| i.id == id)
|
|
}
|
|
|
|
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
|
|
pub fn update_stats(
|
|
&mut self,
|
|
id: &str,
|
|
cpu_stat: Option<f64>,
|
|
mem_stat: Option<u64>,
|
|
mem_limit: u64,
|
|
rx: u64,
|
|
tx: u64,
|
|
) {
|
|
if let Some(container) = self.get_container_by_id(id) {
|
|
if container.cpu_stats.len() >= 60 {
|
|
container.cpu_stats.pop_front();
|
|
}
|
|
if container.mem_stats.len() >= 60 {
|
|
container.mem_stats.pop_front();
|
|
}
|
|
|
|
if let Some(cpu) = cpu_stat {
|
|
container.cpu_stats.push_back(CpuStats::new(cpu));
|
|
}
|
|
if let Some(mem) = mem_stat {
|
|
container.mem_stats.push_back(ByteStats::new(mem));
|
|
}
|
|
|
|
container.rx.update(rx);
|
|
container.tx.update(tx);
|
|
container.mem_limit.update(mem_limit);
|
|
}
|
|
}
|
|
|
|
/// Update, or insert, containers
|
|
pub fn update_containers(&mut self, containers: &[ContainerSummary]) {
|
|
let all_ids = self.get_all_ids();
|
|
|
|
if !containers.is_empty() && self.containers.state.selected().is_none() {
|
|
self.containers.start();
|
|
}
|
|
|
|
for (index, id) in all_ids.iter().enumerate() {
|
|
if !containers
|
|
.iter()
|
|
.filter_map(|i| i.id.as_ref())
|
|
.any(|x| x == id)
|
|
{
|
|
// If removed container is currently selected, then change selected to previous
|
|
// This will default to 0 in any edge cases
|
|
if self.containers.state.selected().is_some() {
|
|
self.containers.previous();
|
|
}
|
|
// Check is some, else can cause out of bounds error, if containers get removed before a docker update
|
|
if self.containers.items.get(index).is_some() {
|
|
self.containers.items.remove(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
for i in containers.iter() {
|
|
if let Some(id) = i.id.as_ref() {
|
|
// maybe if no name then continue?
|
|
let name = i.names.as_ref().map_or("".to_owned(), |f|f.get(0).map_or("".to_owned(), |f|f.clone()));
|
|
// if let Some(c) = name.chars().next() {
|
|
// if c == '/' {
|
|
// name.remove(0);
|
|
// }
|
|
// }
|
|
|
|
let state = State::from(i.state.as_ref().map_or("dead".to_owned(), |f|f.trim().to_owned()));
|
|
|
|
|
|
|
|
let status = i.status.as_ref().map_or("".to_owned(), |f| f.trim().to_owned());
|
|
let image = i.image.as_ref().map_or("".to_owned(), |f|f.clone());
|
|
|
|
if let Some(current_container) = self.get_container_by_id(id) {
|
|
if current_container.name != name {
|
|
current_container.name = name;
|
|
};
|
|
if current_container.status != status {
|
|
current_container.status = status;
|
|
};
|
|
if current_container.state != state {
|
|
current_container.docker_controls.items = DockerControls::gen_vec(&state);
|
|
|
|
// Update the list state, needs to be None if the gen_vec returns an empty vec
|
|
match state {
|
|
State::Removing | State::Restarting | State::Unknown => {
|
|
current_container.docker_controls.state.select(None);
|
|
}
|
|
_ => current_container.docker_controls.start(),
|
|
};
|
|
current_container.state = state;
|
|
};
|
|
if current_container.image != image {
|
|
// current_container.image = image.chars().into_iter().take(64).collect();
|
|
current_container.image = image;
|
|
};
|
|
} else {
|
|
// let mut container = ContainerItem::new(id.clone(), status, image.chars().into_iter().take(64).collect(), state, name);
|
|
let mut container = ContainerItem::new(id.clone(), status, image, state, name);
|
|
container.logs.end();
|
|
self.containers.items.push(container);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// update logs of a given container, based on id
|
|
pub fn update_log_by_id(&mut self, output: &[String], id: &str) {
|
|
let tz = Self::get_systemtime();
|
|
let color = self.args.color;
|
|
let raw = self.args.raw;
|
|
|
|
if let Some(container) = self.get_container_by_id(id) {
|
|
container.last_updated = tz;
|
|
let current_len = container.logs.items.len();
|
|
|
|
for i in output.iter() {
|
|
let lines = if color {
|
|
log_sanitizer::colorize_logs(i)
|
|
} else if raw {
|
|
log_sanitizer::raw(i.clone())
|
|
} else {
|
|
log_sanitizer::remove_ansi(i)
|
|
};
|
|
container.logs.items.push(ListItem::new(lines));
|
|
}
|
|
|
|
if container.logs.state.selected().is_none()
|
|
|| container.logs.state.selected().unwrap_or_default() + 1 == current_len
|
|
{
|
|
container.logs.end();
|
|
}
|
|
}
|
|
self.logs_parsed = true;
|
|
}
|
|
}
|