277 lines
8.5 KiB
Rust
277 lines
8.5 KiB
Rust
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::{
|
|
io::{self, Stdout, Write},
|
|
process::Stdio,
|
|
sync::{atomic::Ordering, Arc},
|
|
};
|
|
use std::{sync::atomic::AtomicBool, time::Instant};
|
|
use tokio::sync::mpsc::Sender;
|
|
use tracing::error;
|
|
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, Status};
|
|
use crate::{
|
|
app_data::AppData, app_error::AppError, docker_data::DockerMessage,
|
|
input_handler::InputMessages,
|
|
};
|
|
|
|
pub struct Ui {
|
|
app_data: Arc<Mutex<AppData>>,
|
|
docker_sx: Sender<DockerMessage>,
|
|
gui_state: Arc<Mutex<GuiState>>,
|
|
is_running: Arc<AtomicBool>,
|
|
sender: Sender<InputMessages>,
|
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
|
}
|
|
|
|
impl Ui {
|
|
/// Create a new Ui struct, and execute the drawing loops
|
|
pub async fn create(
|
|
app_data: Arc<Mutex<AppData>>,
|
|
docker_sx: Sender<DockerMessage>,
|
|
gui_state: Arc<Mutex<GuiState>>,
|
|
is_running: Arc<AtomicBool>,
|
|
sender: Sender<InputMessages>,
|
|
) {
|
|
if let Ok(mut terminal) = Self::start_terminal() {
|
|
let mut ui = Self {
|
|
app_data,
|
|
docker_sx,
|
|
gui_state,
|
|
is_running,
|
|
sender,
|
|
terminal,
|
|
};
|
|
if let Err(e) = ui.draw_ui().await {
|
|
error!("{e}");
|
|
}
|
|
if let Err(e) = ui.end_terminal() {
|
|
error!("{e}");
|
|
};
|
|
}
|
|
}
|
|
|
|
// Setup the terminal for full-screen drawing mode, with mouse capture
|
|
fn start_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
|
|
enable_raw_mode()?;
|
|
let mut stdout = io::stdout();
|
|
execute!(stdout, EnableMouseCapture, EnterAlternateScreen)?;
|
|
let backend = CrosstermBackend::new(stdout);
|
|
Terminal::new(backend)
|
|
}
|
|
|
|
/// reset the terminal back to default settings
|
|
pub fn end_terminal(&mut self) -> Result<()> {
|
|
disable_raw_mode()?;
|
|
execute!(
|
|
self.terminal.backend_mut(),
|
|
LeaveAlternateScreen,
|
|
DisableMouseCapture
|
|
)?;
|
|
self.terminal.show_cursor()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Draw the the error message ui, for 5 seconds, with a countdown
|
|
fn err_loop(&mut self) -> Result<(), AppError> {
|
|
let mut seconds = 5;
|
|
let mut now = Instant::now();
|
|
loop {
|
|
// This is a fix for a weird bug on Linux + WSL which will output mouse movement to the stdout
|
|
std::thread::spawn(|| {
|
|
execute!(io::stdout(), EnableMouseCapture).unwrap_or(());
|
|
execute!(io::stdout(), DisableMouseCapture).unwrap_or(());
|
|
});
|
|
|
|
if now.elapsed() >= std::time::Duration::from_secs(1) {
|
|
seconds -= 1;
|
|
now = Instant::now();
|
|
}
|
|
|
|
if seconds < 1 {
|
|
break;
|
|
}
|
|
|
|
if self
|
|
.terminal
|
|
.draw(|f| draw_blocks::error(f, AppError::DockerConnect, Some(seconds)))
|
|
.is_err()
|
|
{
|
|
return Err(AppError::Terminal);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The loop for drawing the main UI to the terminal
|
|
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
|
let input_poll_rate = std::time::Duration::from_millis(100);
|
|
let update_duration =
|
|
std::time::Duration::from_millis(u64::from(self.app_data.lock().args.docker_interval));
|
|
let mut now = Instant::now();
|
|
while self.is_running.load(Ordering::SeqCst) {
|
|
if self
|
|
.terminal
|
|
.draw(|frame| ui(frame, &self.app_data, &self.gui_state))
|
|
.is_err()
|
|
{
|
|
return Err(AppError::Terminal);
|
|
}
|
|
if crossterm::event::poll(input_poll_rate).unwrap_or(false) {
|
|
if let Ok(event) = event::read() {
|
|
if let Event::Key(key) = event {
|
|
self.sender
|
|
.send(InputMessages::ButtonPress(key.code))
|
|
.await
|
|
.unwrap_or(());
|
|
} else if let Event::Mouse(m) = event {
|
|
self.sender
|
|
.send(InputMessages::MouseEvent(m))
|
|
.await
|
|
.unwrap_or(());
|
|
} else if let Event::Resize(_, _) = event {
|
|
self.gui_state.lock().clear_area_map();
|
|
self.terminal.autoresize().unwrap_or(());
|
|
}
|
|
}
|
|
}
|
|
|
|
if now.elapsed() >= update_duration {
|
|
self.docker_sx
|
|
.send(DockerMessage::Update)
|
|
.await
|
|
.unwrap_or(());
|
|
now = Instant::now();
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
|
|
/// Draw either the Error, or main oxker ui, to the terminal
|
|
async fn draw_ui(&mut self) -> Result<(), AppError> {
|
|
let status_dockerconnect = self
|
|
.gui_state
|
|
.lock()
|
|
.status_contains(&[Status::DockerConnect]);
|
|
if status_dockerconnect {
|
|
self.err_loop()?;
|
|
} else {
|
|
self.gui_loop().await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Draw the main ui to a frame of the terminal
|
|
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 height = app_data.lock().get_container_len();
|
|
let height = if height < 12 { height + 4 } else { 12 };
|
|
|
|
let column_widths = app_data.lock().get_width();
|
|
let has_containers = app_data.lock().get_container_len() > 0;
|
|
let has_error = app_data.lock().get_error();
|
|
let sorted_by = app_data.lock().get_sorted();
|
|
|
|
let show_help = gui_state.lock().status_contains(&[Status::Help]);
|
|
let info_text = gui_state.lock().info_box_text.clone();
|
|
let loading_icon = gui_state.lock().get_loading();
|
|
|
|
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.try_into().unwrap_or_default()),
|
|
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_blocks::containers(app_data, top_panel[0], f, gui_state, &column_widths);
|
|
|
|
if has_containers {
|
|
draw_blocks::commands(app_data, top_panel[1], f, gui_state);
|
|
}
|
|
|
|
draw_blocks::logs(app_data, lower_main[0], f, gui_state, &loading_icon);
|
|
|
|
draw_blocks::heading_bar(
|
|
whole_layout[0],
|
|
&column_widths,
|
|
f,
|
|
has_containers,
|
|
&loading_icon,
|
|
sorted_by,
|
|
gui_state,
|
|
);
|
|
|
|
// only draw charts if there are containers
|
|
if has_containers {
|
|
draw_blocks::chart(f, lower_main[1], app_data);
|
|
}
|
|
|
|
if let Some(info) = info_text {
|
|
draw_blocks::info(f, info);
|
|
}
|
|
|
|
// Check if error, and show popup if so
|
|
if show_help {
|
|
draw_blocks::help_box(f);
|
|
}
|
|
|
|
if let Some(error) = has_error {
|
|
draw_blocks::error(f, error, None);
|
|
}
|
|
}
|