Files
oxkerclone/src/ui/mod.rs
T
Jack Wills 0a1b531116 feat: Acutal fix the the mouse events output
The EnableMouseCapture from Crossterm was too broad, by only enabling a subject of the events, 1) performance is improvedand 2) and intermittent bug where mouse events were output to stdout has been removed
2023-03-02 01:09:17 +00:00

289 lines
8.7 KiB
Rust

use anyhow::Result;
use crossterm::{
event::{self, DisableMouseCapture, Event},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use parking_lot::Mutex;
use std::{
io::{self, Stdout, Write},
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>,
now: Instant,
sender: Sender<InputMessages>,
terminal: Terminal<CrosstermBackend<Stdout>>,
}
/// Enable moust capture, but don't enable all the mouse movements, which improves performance, and fixes the weird mouse event output bug
pub fn enable_mouse_capture() {
io::stdout()
.write_all(
concat!(
crossterm::csi!("?1000h"),
crossterm::csi!("?1015h"),
crossterm::csi!("?1006h"),
)
.as_bytes(),
)
.unwrap_or(());
}
impl Ui {
/// Create a new Ui struct, and execute the drawing loop
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(terminal) = Self::setup_terminal() {
let mut ui = Self {
app_data,
docker_sx,
gui_state,
is_running,
now: Instant::now(),
sender,
terminal,
};
if let Err(e) = ui.draw_ui().await {
error!("{e}");
}
if let Err(e) = ui.reset_terminal() {
error!("{e}");
};
} else {
error!("Terminal Error");
}
}
/// Setup the terminal for full-screen drawing mode, with mouse capture
fn setup_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
enable_mouse_capture();
let backend = CrosstermBackend::new(stdout);
Terminal::new(backend)
}
/// reset the terminal back to default settings
pub fn reset_terminal(&mut self) -> Result<()> {
self.terminal.clear()?;
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;
loop {
if self.now.elapsed() >= std::time::Duration::from_secs(1) {
seconds -= 1;
self.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));
while self.is_running.load(Ordering::SeqCst) {
if self
.terminal
.draw(|frame| draw_frame(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 self.now.elapsed() >= update_duration {
self.docker_sx
.send(DockerMessage::Update)
.await
.unwrap_or(());
self.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 draw_frame<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 2, logs, and optional charts
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);
}
}