init commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
pub mod log_sanitizer {
|
||||
|
||||
use cansi::{categorise_text, Color as CansiColor, Intensity};
|
||||
use tui::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
};
|
||||
|
||||
/// Attempt to colorize the given string to tui-rs standars
|
||||
pub fn colorize_logs(input: String) -> Vec<Spans<'static>> {
|
||||
vec![Spans::from(
|
||||
categorise_text(&input)
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let fg_color = color_ansi_to_tui(i.fg_colour);
|
||||
let bg_color = color_ansi_to_tui(i.bg_colour);
|
||||
let style = Style::default().bg(bg_color).fg(fg_color);
|
||||
if i.blink {
|
||||
style.add_modifier(Modifier::SLOW_BLINK);
|
||||
}
|
||||
if i.underline {
|
||||
style.add_modifier(Modifier::UNDERLINED);
|
||||
}
|
||||
if i.reversed {
|
||||
style.add_modifier(Modifier::REVERSED);
|
||||
}
|
||||
if i.intensity == Intensity::Bold {
|
||||
style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if i.hidden {
|
||||
style.add_modifier(Modifier::HIDDEN);
|
||||
}
|
||||
if i.strikethrough {
|
||||
style.add_modifier(Modifier::CROSSED_OUT);
|
||||
}
|
||||
Span::styled(i.text.to_owned(), style)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)]
|
||||
}
|
||||
|
||||
/// Remove all ansi formatting from a given string and create tui-rs spans
|
||||
pub fn remove_ansi(input: String) -> Vec<Spans<'static>> {
|
||||
let mut output = String::from("");
|
||||
for i in categorise_text(&input) {
|
||||
output.push_str(i.text)
|
||||
}
|
||||
raw(output)
|
||||
}
|
||||
|
||||
/// create tui-rs spans that exactly match the given strings
|
||||
pub fn raw(input: String) -> Vec<Spans<'static>> {
|
||||
vec![Spans::from(Span::raw(input))]
|
||||
}
|
||||
|
||||
/// Change from ansi to tui colors
|
||||
fn color_ansi_to_tui(color: CansiColor) -> Color {
|
||||
match color {
|
||||
CansiColor::Black => Color::Black,
|
||||
CansiColor::Red => Color::Red,
|
||||
CansiColor::Green => Color::Green,
|
||||
CansiColor::Yellow => Color::Yellow,
|
||||
CansiColor::Blue => Color::Blue,
|
||||
CansiColor::Magenta => Color::Magenta,
|
||||
CansiColor::Cyan => Color::Cyan,
|
||||
CansiColor::White => Color::White,
|
||||
CansiColor::BrightBlack => Color::Black,
|
||||
CansiColor::BrightRed => Color::LightRed,
|
||||
CansiColor::BrightGreen => Color::LightGreen,
|
||||
CansiColor::BrightYellow => Color::LightYellow,
|
||||
CansiColor::BrightBlue => Color::LightBlue,
|
||||
CansiColor::BrightMagenta => Color::LightMagenta,
|
||||
CansiColor::BrightCyan => Color::LightCyan,
|
||||
CansiColor::BrightWhite => Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
use parking_lot::Mutex;
|
||||
use std::default::Default;
|
||||
use std::{fmt::Display, sync::Arc};
|
||||
use tui::{
|
||||
backend::Backend,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
symbols,
|
||||
text::{Span, Spans},
|
||||
widgets::{
|
||||
Axis, Block, BorderType, Borders, Chart, Clear, Dataset, GraphType, List, ListItem,
|
||||
Paragraph,
|
||||
},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
||||
app_error::AppError,
|
||||
};
|
||||
|
||||
use super::{GuiState, SelectablePanel};
|
||||
|
||||
const NAME_TEXT: &str = r#"
|
||||
88
|
||||
88
|
||||
88
|
||||
,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba,
|
||||
a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8
|
||||
8b d8 )888( 8888[ 8PP""""""" 88
|
||||
"8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88
|
||||
`"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 "#;
|
||||
|
||||
const NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||
const MARGIN: &str = " ";
|
||||
|
||||
/// Generate block, add a bored if is the selected panel,
|
||||
/// add custom title based on state of each panel
|
||||
fn generate_block<'a>(
|
||||
selectable_panel: Option<SelectablePanel>,
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
selected_panel: &SelectablePanel,
|
||||
) -> Block<'a> {
|
||||
let mut block = Block::default().borders(Borders::ALL);
|
||||
|
||||
if let Some(panel) = selectable_panel {
|
||||
let title = match panel {
|
||||
SelectablePanel::Containers => {
|
||||
format!(
|
||||
" {} {} ",
|
||||
panel.title(),
|
||||
app_data.lock().containers.get_state_title()
|
||||
)
|
||||
}
|
||||
SelectablePanel::Logs => {
|
||||
format!(" {} {} ", panel.title(), app_data.lock().get_log_title())
|
||||
}
|
||||
_ => String::from(""),
|
||||
};
|
||||
block = block.title(title);
|
||||
if selected_panel == &panel {
|
||||
let selected_style = Style::default().fg(Color::LightCyan);
|
||||
let selected_border = BorderType::Plain;
|
||||
block = block
|
||||
.border_style(selected_style)
|
||||
.border_type(selected_border);
|
||||
}
|
||||
}
|
||||
block
|
||||
}
|
||||
|
||||
/// Draw the selectable panels
|
||||
pub fn draw_commands<B: Backend>(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
f: &mut Frame<'_, B>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
index: Option<usize>,
|
||||
selected_panel: &SelectablePanel,
|
||||
) {
|
||||
let panel = SelectablePanel::Commands;
|
||||
let block = generate_block(Some(panel), app_data, selected_panel);
|
||||
|
||||
gui_state.lock().insert_into_area_map(panel, area);
|
||||
|
||||
if let Some(i) = index {
|
||||
let items = app_data.lock().containers.items[i]
|
||||
.docker_controls
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let lines = Spans::from(vec![Span::styled(
|
||||
i.to_string(),
|
||||
Style::default().fg(i.get_color()),
|
||||
)]);
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let items = List::new(items)
|
||||
.block(block)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("▶ ");
|
||||
|
||||
f.render_stateful_widget(
|
||||
items,
|
||||
area,
|
||||
&mut app_data.lock().containers.items[i].docker_controls.state,
|
||||
);
|
||||
} else {
|
||||
let debug_text = String::from("");
|
||||
let paragraph = Paragraph::new(debug_text)
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(paragraph, area)
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the selectable panels
|
||||
pub fn draw_containers<B: Backend>(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
f: &mut Frame<'_, B>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
selected_panel: &SelectablePanel,
|
||||
widths: &Columns,
|
||||
) {
|
||||
let panel = SelectablePanel::Containers;
|
||||
let block = generate_block(Some(panel), app_data, selected_panel);
|
||||
|
||||
gui_state.lock().insert_into_area_map(panel, area);
|
||||
|
||||
let items = app_data
|
||||
.lock()
|
||||
.containers
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let state_style = Style::default().fg(i.state.get_color());
|
||||
let blue = Style::default().fg(Color::Blue);
|
||||
|
||||
let mems = format!(
|
||||
"{:>1} / {:>1}",
|
||||
i.mem_stats.back().unwrap_or(&ByteStats::new(0)),
|
||||
i.mem_limit
|
||||
);
|
||||
|
||||
let lines = Spans::from(vec![
|
||||
Span::styled(
|
||||
format!("{:<width$}", i.state.to_string(), width = widths.state.1),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{}{:>width$}", MARGIN, i.status, width = widths.status.1),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
i.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)),
|
||||
width = widths.cpu.1
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{}{:>width$}", MARGIN, mems, width = widths.mem.1),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{}{:>width$}", MARGIN, i.name, width = widths.name.1),
|
||||
blue,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{}{:>width$}", MARGIN, i.image, width = widths.image.1),
|
||||
blue,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{}{:>width$}", MARGIN, i.net_rx, width = widths.net_rx.1),
|
||||
Style::default().fg(Color::Rgb(255, 233, 193)),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{}{:>width$}", MARGIN, i.net_tx, width = widths.net_tx.1),
|
||||
Style::default().fg(Color::Rgb(205, 140, 140)),
|
||||
),
|
||||
]);
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if items.is_empty() {
|
||||
let debug_text = String::from("no containers running");
|
||||
let paragraph = Paragraph::new(debug_text)
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(paragraph, area)
|
||||
} else {
|
||||
let items = List::new(items)
|
||||
.block(block)
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("⚪ ");
|
||||
|
||||
f.render_stateful_widget(items, area, &mut app_data.lock().containers.state);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the selectable panels
|
||||
pub fn draw_logs<B: Backend>(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
f: &mut Frame<'_, B>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
index: Option<usize>,
|
||||
selected_panel: &SelectablePanel,
|
||||
) {
|
||||
let panel = SelectablePanel::Logs;
|
||||
|
||||
gui_state.lock().insert_into_area_map(panel, area);
|
||||
|
||||
let block = generate_block(Some(panel), app_data, selected_panel);
|
||||
|
||||
let init = app_data.lock().init;
|
||||
if !init {
|
||||
let icon = gui_state.lock().get_loading();
|
||||
let parsing_logs = format!("parsing logs {}", icon);
|
||||
let paragraph = Paragraph::new(parsing_logs)
|
||||
.style(Style::default())
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(paragraph, area)
|
||||
|
||||
} else if let Some(index) = index {
|
||||
let items = app_data.lock().containers.items[index]
|
||||
.logs
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|i| i.1.to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let items = List::new(items)
|
||||
.block(block)
|
||||
.highlight_symbol("▶ ")
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||
f.render_stateful_widget(
|
||||
items,
|
||||
area,
|
||||
&mut app_data.lock().containers.items[index].logs.state,
|
||||
);
|
||||
} else {
|
||||
let debug_text = String::from("no logs found");
|
||||
let paragraph = Paragraph::new(debug_text)
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(paragraph, area)
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the cpu + mem charts
|
||||
pub fn draw_chart<B: Backend>(
|
||||
f: &mut Frame<'_, B>,
|
||||
area: Rect,
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
index: Option<usize>,
|
||||
) {
|
||||
if let Some(index) = index {
|
||||
let area = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(area);
|
||||
let (cpu, mem) = app_data.lock().containers.items[index].get_chart_data();
|
||||
|
||||
let cpu_dataset = vec![Dataset::default()
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Magenta))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&cpu.0)];
|
||||
|
||||
let mem_dataset = vec![Dataset::default()
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&mem.0)];
|
||||
let cpu_chart = make_chart(
|
||||
cpu.2,
|
||||
String::from("cpu"),
|
||||
cpu_dataset,
|
||||
CpuStats::new(cpu.0.last().unwrap_or(&(0.00, 0.00)).1),
|
||||
cpu.1,
|
||||
);
|
||||
let mem_chart = make_chart(
|
||||
mem.2,
|
||||
String::from("memory"),
|
||||
mem_dataset,
|
||||
ByteStats::new(mem.0.last().unwrap_or(&(0.0, 0.0)).1 as u64),
|
||||
mem.1,
|
||||
);
|
||||
|
||||
f.render_widget(cpu_chart, area[0]);
|
||||
f.render_widget(mem_chart, area[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create charts
|
||||
fn make_chart<T: Stats + Display>(
|
||||
state: State,
|
||||
name: String,
|
||||
dataset: Vec<Dataset>,
|
||||
current: T,
|
||||
max: T,
|
||||
) -> Chart {
|
||||
let title_color = match state {
|
||||
State::Running => Color::Green,
|
||||
_ => state.get_color(),
|
||||
};
|
||||
let label_color = match state {
|
||||
State::Running => ORANGE,
|
||||
_ => state.get_color(),
|
||||
};
|
||||
Chart::new(dataset)
|
||||
.block(
|
||||
Block::default()
|
||||
.title_alignment(Alignment::Center)
|
||||
.title(Span::styled(
|
||||
format!(" {} {} ", name, current),
|
||||
Style::default()
|
||||
.fg(title_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Plain),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.style(Style::default().fg(title_color))
|
||||
.bounds([0.00, 60.0]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.labels(vec![
|
||||
Span::styled("", Style::default().fg(label_color)),
|
||||
Span::styled(
|
||||
format!("{}", max),
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(label_color),
|
||||
),
|
||||
])
|
||||
// add 0.01, for cases when the value is 0
|
||||
.bounds([0.0, max.get_value() +0.01]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Show error popup over whole screen
|
||||
pub fn draw_info_bar<B: Backend>(
|
||||
area: Rect,
|
||||
columns: &Columns,
|
||||
f: &mut Frame<'_, B>,
|
||||
has_containers: bool,
|
||||
info_visible: bool,
|
||||
) {
|
||||
let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black));
|
||||
|
||||
f.render_widget(block(), area);
|
||||
|
||||
let mut column_headings = format!(" {:>width$}", columns.state.0, width = columns.state.1);
|
||||
column_headings.push_str(
|
||||
format!(
|
||||
"{} {:>width$}",
|
||||
MARGIN,
|
||||
columns.status.0,
|
||||
width = columns.status.1
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
column_headings
|
||||
.push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str());
|
||||
column_headings
|
||||
.push_str(format!("{}{:>width$}", MARGIN, columns.mem.0, width = columns.mem.1).as_str());
|
||||
column_headings.push_str(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
columns.name.0,
|
||||
width = columns.name.1
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
column_headings.push_str(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
columns.image.0,
|
||||
width = columns.image.1
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
column_headings.push_str(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
columns.net_rx.0,
|
||||
width = columns.net_rx.1
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
column_headings.push_str(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
columns.net_tx.0,
|
||||
width = columns.net_tx.1
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let suffix = if info_visible { "exit" } else { "show" };
|
||||
let info_text = format!("( h ) to {} help {}", suffix, MARGIN);
|
||||
let info_width = info_text.chars().count();
|
||||
|
||||
let column_width = column_headings.chars().count();
|
||||
|
||||
let splits = if has_containers {
|
||||
vec![
|
||||
Constraint::Min(column_width as u16),
|
||||
Constraint::Min(info_width as u16),
|
||||
]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
|
||||
let split_bar = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(splits.as_ref())
|
||||
.split(area);
|
||||
|
||||
if has_containers {
|
||||
let paragraph = Paragraph::new(column_headings)
|
||||
.block(block())
|
||||
.alignment(Alignment::Left);
|
||||
f.render_widget(paragraph, split_bar[0]);
|
||||
}
|
||||
|
||||
let paragraph = Paragraph::new(info_text)
|
||||
.block(block())
|
||||
.alignment(Alignment::Right);
|
||||
|
||||
let index = if has_containers { 1 } else { 0 };
|
||||
f.render_widget(paragraph, split_bar[index]);
|
||||
}
|
||||
|
||||
/// Show error popup over whole screen
|
||||
pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
||||
let title = format!(" {} ", VERSION);
|
||||
|
||||
let mut description_text =
|
||||
String::from("\n A basic docker container information viewer and controller.");
|
||||
description_text.push_str("\n Tab or Alt+Tab to change panels, arrows to change lines, enter to send docker container commands.");
|
||||
description_text.push_str("\n Mouse input also available.");
|
||||
description_text.push_str("\n ( q ) to quit at any time.");
|
||||
description_text
|
||||
.push_str("\n\n currenty an early work in progress, all and any input appreciated");
|
||||
description_text.push_str(format!("\n {}", REPO.trim()).as_str());
|
||||
|
||||
let mut max_line_width = 0;
|
||||
|
||||
let all_text = format!("{}{}", NAME_TEXT, description_text);
|
||||
|
||||
all_text.lines().into_iter().for_each(|line| {
|
||||
let width = line.chars().count();
|
||||
if width > max_line_width {
|
||||
max_line_width = width;
|
||||
}
|
||||
});
|
||||
|
||||
let mut lines = all_text.lines().count();
|
||||
|
||||
// Add some vertical and horizontal padding to the info box
|
||||
lines += 3;
|
||||
max_line_width += 4;
|
||||
|
||||
let name_paragraph = Paragraph::new(NAME_TEXT)
|
||||
.style(Style::default().bg(Color::Magenta).fg(Color::White))
|
||||
.block(Block::default())
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let description_paragraph = Paragraph::new(description_text.as_str())
|
||||
.style(Style::default().bg(Color::Magenta).fg(Color::Black))
|
||||
.block(Block::default())
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
let block = Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(Color::Black));
|
||||
|
||||
let area = centered_info(lines as u16, max_line_width as u16, f.size());
|
||||
|
||||
let split_popup = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Max(NAME_TEXT.lines().count() as u16),
|
||||
Constraint::Max(description_text.lines().count() as u16),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
|
||||
// Order is important here
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(name_paragraph, split_popup[0]);
|
||||
f.render_widget(description_paragraph, split_popup[1]);
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
|
||||
/// Show error popup over whole screen
|
||||
pub fn draw_error<B: Backend>(f: &mut Frame<'_, B>, error: AppError, seconds: Option<u8>) {
|
||||
let block = Block::default()
|
||||
.title(" Error ")
|
||||
.border_type(BorderType::Rounded)
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let to_push = match error {
|
||||
AppError::DockerConnect => {
|
||||
format!(
|
||||
"\n\n {}::v{} closing in {:02} seconds",
|
||||
NAME,
|
||||
VERSION,
|
||||
seconds.unwrap_or(5)
|
||||
)
|
||||
}
|
||||
_ => String::from("\n\n ( c ) to clear error\n ( q ) to quit oxker"),
|
||||
};
|
||||
|
||||
let mut text = format!("\n{}", error);
|
||||
|
||||
text.push_str(to_push.as_str());
|
||||
|
||||
let mut max_line_width = 0;
|
||||
text.lines().into_iter().for_each(|line| {
|
||||
let width = line.chars().count();
|
||||
if width > max_line_width {
|
||||
max_line_width = width;
|
||||
}
|
||||
});
|
||||
|
||||
let mut lines = text.lines().count();
|
||||
|
||||
// Add some horizontal & vertical margins
|
||||
max_line_width += 8;
|
||||
lines += 3;
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().bg(Color::Red).fg(Color::White))
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let area = centered_info(lines as u16, max_line_width as u16, f.size());
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
/// draw a box in the center of the screen, based on max line width + number of lines
|
||||
fn centered_info(number_lines: u16, max_line_width: u16, r: Rect) -> Rect {
|
||||
// This can panic if number_lines or max_line_width is larger than r.height or r.width
|
||||
let blank_vertical = (r.height - number_lines) / 2;
|
||||
let blank_horizontal = (r.width - max_line_width) / 2;
|
||||
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Max(blank_vertical),
|
||||
Constraint::Max(number_lines),
|
||||
Constraint::Max(blank_vertical),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(r);
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Max(blank_horizontal),
|
||||
Constraint::Max(max_line_width),
|
||||
Constraint::Max(blank_horizontal),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
use std::{collections::HashMap, fmt};
|
||||
use tui::layout::Rect;
|
||||
|
||||
#[derive(Debug, PartialEq, std::hash::Hash, std::cmp::Eq, Clone, Copy)]
|
||||
pub enum SelectablePanel {
|
||||
Containers,
|
||||
Commands,
|
||||
Logs,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum Loading {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
Five,
|
||||
Six,
|
||||
Seven,
|
||||
Eight,
|
||||
Nine,
|
||||
Ten,
|
||||
}
|
||||
|
||||
impl Loading {
|
||||
pub fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::One => Self::Two,
|
||||
Self::Two => Self::Three,
|
||||
Self::Three => Self::Four,
|
||||
Self::Four => Self::Five,
|
||||
Self::Five => Self::Six,
|
||||
Self::Six => Self::Seven,
|
||||
Self::Seven => Self::Eight,
|
||||
Self::Eight => Self::Nine,
|
||||
Self::Nine => Self::Ten,
|
||||
Self::Ten => Self::One,
|
||||
// Self::Five => Self::One
|
||||
}
|
||||
}
|
||||
}
|
||||
// "⠋",
|
||||
// "⠙",
|
||||
// "⠹",
|
||||
// "⠸",
|
||||
// "⠼",
|
||||
// "⠴",
|
||||
// "⠦",
|
||||
// "⠧",
|
||||
// "⠇",
|
||||
// "⠏"
|
||||
|
||||
impl fmt::Display for Loading {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let disp = match self {
|
||||
Self::One => "⠋",
|
||||
Self::Two => "⠙",
|
||||
Self::Three => "⠹",
|
||||
Self::Four => "⠸",
|
||||
Self::Five => "⠼",
|
||||
Self::Six => "⠴",
|
||||
Self::Seven => "⠦",
|
||||
Self::Eight => "⠧",
|
||||
Self::Nine => "⠇",
|
||||
Self::Ten => "⠏",
|
||||
};
|
||||
write!(f, "{}", disp)
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectablePanel {
|
||||
pub fn title(self) -> &'static str {
|
||||
match self {
|
||||
Self::Containers => "Containers",
|
||||
Self::Logs => "Logs",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
pub fn next(self) -> Self {
|
||||
match self {
|
||||
Self::Containers => Self::Commands,
|
||||
Self::Commands => Self::Logs,
|
||||
Self::Logs => Self::Containers,
|
||||
}
|
||||
}
|
||||
pub fn prev(self) -> Self {
|
||||
match self {
|
||||
Self::Containers => Self::Logs,
|
||||
Self::Commands => Self::Containers,
|
||||
Self::Logs => Self::Commands,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global gui_state, stored in an Arc<Mutex>
|
||||
#[derive(Debug)]
|
||||
pub struct GuiState {
|
||||
// Think this should be a BMapTree, so can define order when iterating over potential intersects
|
||||
// Is an issue if two panels are in the same space, sush as a smaller panel embedded, yet infront of, a larger panel
|
||||
// If a BMapTree think it would mean have to implement ordering for SelectablePanel
|
||||
area_map: HashMap<SelectablePanel, Rect>,
|
||||
loading: Loading,
|
||||
pub selected_panel: SelectablePanel,
|
||||
pub show_help: bool,
|
||||
}
|
||||
|
||||
impl GuiState {
|
||||
/// Generate a default gui_state
|
||||
pub fn default() -> Self {
|
||||
Self {
|
||||
area_map: HashMap::new(),
|
||||
loading: Loading::One,
|
||||
selected_panel: SelectablePanel::Containers,
|
||||
show_help: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||
pub fn clear_area_map(&mut self) {
|
||||
self.area_map.clear();
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||
pub fn rect_insersects(&mut self, rect: Rect) {
|
||||
if let Some(data) = self
|
||||
.area_map
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
.get(0)
|
||||
{
|
||||
self.selected_panel = *data.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert selectable gui panel into area map
|
||||
pub fn insert_into_area_map(&mut self, panel: SelectablePanel, area: Rect) {
|
||||
self.area_map.entry(panel).or_insert(area);
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
pub fn next_loading(&mut self) {
|
||||
self.loading = self.loading.next()
|
||||
}
|
||||
|
||||
pub fn get_loading(&mut self) -> String {
|
||||
self.loading.to_string()
|
||||
}
|
||||
|
||||
pub fn reset_loading(&mut self) {
|
||||
self.loading = Loading::One;
|
||||
}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::{
|
||||
io,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
mod color_match;
|
||||
mod draw_blocks;
|
||||
mod gui_state;
|
||||
|
||||
pub use self::color_match::*;
|
||||
pub use self::gui_state::{GuiState, SelectablePanel};
|
||||
use crate::{app_data::AppData, app_error::AppError, input_handler::InputMessages};
|
||||
use draw_blocks::*;
|
||||
|
||||
/// Take control of the terminal in order to draw gui
|
||||
pub async fn create_ui(
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
sender: Sender<InputMessages>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
) -> Result<()> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let res = run_app(&mut terminal, app_data, sender, is_running, gui_state).await;
|
||||
|
||||
disable_raw_mode().unwrap();
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor().unwrap();
|
||||
|
||||
if let Err(err) = res {
|
||||
err.disp()
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a loop to draw the gui
|
||||
async fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
sender: Sender<InputMessages>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
) -> Result<(), AppError> {
|
||||
let input_poll_rate = std::time::Duration::from_millis(75);
|
||||
|
||||
// Check for docker connect errors before attempting to draw the gui
|
||||
let e = app_data.lock().get_error();
|
||||
if let Some(error) = e {
|
||||
if let AppError::DockerConnect = error {
|
||||
let mut seconds = 5;
|
||||
loop {
|
||||
if seconds < 1 {
|
||||
is_running.store(false, Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
terminal
|
||||
.draw(|f| draw_error(f, AppError::DockerConnect, Some(seconds)))
|
||||
.unwrap();
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
seconds -= 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app_data, &gui_state)).unwrap();
|
||||
if crossterm::event::poll(input_poll_rate).unwrap() {
|
||||
let event = event::read().unwrap();
|
||||
if let Event::Key(key) = event {
|
||||
sender
|
||||
.send(InputMessages::ButtonPress(key.code))
|
||||
.unwrap_or(0);
|
||||
} else if let Event::Mouse(m) = event {
|
||||
sender.send(InputMessages::MouseEvent(m)).unwrap_or(0);
|
||||
} else if let Event::Resize(_, _) = event {
|
||||
gui_state.lock().clear_area_map();
|
||||
terminal.autoresize().unwrap_or(());
|
||||
}
|
||||
}
|
||||
|
||||
if !is_running.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(
|
||||
f: &mut Frame<'_, B>,
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
// set max height for container section, needs +4 to deal with docker commands list and borders
|
||||
let mut height = app_data.lock().get_container_len();
|
||||
if height < 12 {
|
||||
height += 4;
|
||||
} else {
|
||||
height = 12
|
||||
}
|
||||
|
||||
let column_widths = app_data.lock().get_width();
|
||||
let has_containers = !app_data.lock().containers.items.is_empty();
|
||||
let has_error = app_data.lock().get_error();
|
||||
let log_index = app_data.lock().get_selected_log_index();
|
||||
let selected_panel = gui_state.lock().selected_panel;
|
||||
let show_help = gui_state.lock().show_help;
|
||||
|
||||
let whole_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Split into 3, containers+controls, logs, then graphs
|
||||
let upper_main = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Max(height as u16), Constraint::Percentage(50)].as_ref())
|
||||
.split(whole_layout[1]);
|
||||
|
||||
let top_split = if has_containers {
|
||||
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
// Containers + docker commands
|
||||
let top_panel = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(top_split.as_ref())
|
||||
.split(upper_main[0]);
|
||||
|
||||
let lower_split = if has_containers {
|
||||
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
|
||||
// Split into 3, containers+controls, logs, then graphs
|
||||
let lower_main = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(lower_split.as_ref())
|
||||
.split(upper_main[1]);
|
||||
|
||||
draw_containers(
|
||||
app_data,
|
||||
top_panel[0],
|
||||
f,
|
||||
gui_state,
|
||||
&selected_panel,
|
||||
&column_widths,
|
||||
);
|
||||
|
||||
if has_containers {
|
||||
draw_commands(
|
||||
app_data,
|
||||
top_panel[1],
|
||||
f,
|
||||
gui_state,
|
||||
log_index,
|
||||
&selected_panel,
|
||||
);
|
||||
}
|
||||
|
||||
draw_logs(
|
||||
app_data,
|
||||
lower_main[0],
|
||||
f,
|
||||
gui_state,
|
||||
log_index,
|
||||
&selected_panel,
|
||||
);
|
||||
|
||||
draw_info_bar(
|
||||
whole_layout[0],
|
||||
&column_widths,
|
||||
f,
|
||||
has_containers,
|
||||
show_help,
|
||||
);
|
||||
|
||||
// only draw charts if there are containers
|
||||
if has_containers {
|
||||
draw_chart(f, lower_main[1], app_data, log_index);
|
||||
}
|
||||
|
||||
// Check if error, and show popup if so
|
||||
if show_help {
|
||||
draw_help_box(f);
|
||||
}
|
||||
|
||||
if let Some(error) = has_error {
|
||||
app_data.lock().show_error = true;
|
||||
draw_error(f, error, None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user