refactor: Multiple UI improvements;
Use FrameData struct to store commonly accessed data, in order to reduce mutex locks. rename unpause to resume use get_selected_panel() function instead of directly gui_state.selected_panel debug mode now shows some usefull information
This commit is contained in:
@@ -28,6 +28,11 @@ impl ContainerId {
|
|||||||
pub fn get(&self) -> &str {
|
pub fn get(&self) -> &str {
|
||||||
self.0.as_str()
|
self.0.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Only return first 8 chars of id, is usually more than enough for uniqueness
|
||||||
|
pub fn get_short(&self) -> String {
|
||||||
|
self.0.chars().take(8).collect::<String>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ord for ContainerId {
|
impl Ord for ContainerId {
|
||||||
@@ -443,6 +448,20 @@ pub struct ContainerItem {
|
|||||||
pub is_oxker: bool,
|
pub is_oxker: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Basic display information, for when running in debug mode
|
||||||
|
impl fmt::Display for ContainerItem {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}, {}, {}, {}",
|
||||||
|
self.id.get_short(),
|
||||||
|
self.name,
|
||||||
|
self.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)),
|
||||||
|
self.mem_stats.back().unwrap_or(&ByteStats::new(0))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ContainerItem {
|
impl ContainerItem {
|
||||||
/// Create a new container item
|
/// Create a new container item
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|||||||
+36
-12
@@ -17,6 +17,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
pub use container_state::*;
|
pub use container_state::*;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
/// Global app_state, stored in an Arc<Mutex>
|
/// Global app_state, stored in an Arc<Mutex>
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
@@ -26,6 +27,17 @@ pub struct AppData {
|
|||||||
pub args: CliArgs,
|
pub args: CliArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
/// Global app_state, stored in an Arc<Mutex>
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AppData {
|
||||||
|
containers: StatefulList<ContainerItem>,
|
||||||
|
error: Option<AppError>,
|
||||||
|
sorted_by: Option<(Header, SortedOrder)>,
|
||||||
|
debug_string: String,
|
||||||
|
pub args: CliArgs,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum SortedOrder {
|
pub enum SortedOrder {
|
||||||
Asc,
|
Asc,
|
||||||
@@ -64,6 +76,17 @@ impl fmt::Display for Header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub fn get_debug_string(&self) -> &str {
|
||||||
|
&self.debug_string
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn set_debug_string(&mut self, x: &str) {
|
||||||
|
self.debug_string.push_str(x);
|
||||||
|
}
|
||||||
|
|
||||||
/// Change the sorted order, also set the selected container state to match new order
|
/// Change the sorted order, also set the selected container state to match new order
|
||||||
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
|
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
|
||||||
self.sorted_by = x;
|
self.sorted_by = x;
|
||||||
@@ -86,6 +109,7 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a default app_state
|
/// Generate a default app_state
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
pub fn default(args: CliArgs) -> Self {
|
pub fn default(args: CliArgs) -> Self {
|
||||||
Self {
|
Self {
|
||||||
args,
|
args,
|
||||||
@@ -95,6 +119,18 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a default app_state
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub fn default(args: CliArgs) -> Self {
|
||||||
|
Self {
|
||||||
|
args,
|
||||||
|
containers: StatefulList::new(vec![]),
|
||||||
|
error: None,
|
||||||
|
sorted_by: None,
|
||||||
|
debug_string: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Container sort related methods
|
/// Container sort related methods
|
||||||
|
|
||||||
/// Remove the sorted header & order, and sort by default - created datetime
|
/// Remove the sorted header & order, and sort by default - created datetime
|
||||||
@@ -410,18 +446,6 @@ impl AppData {
|
|||||||
self.get_selected_container().map_or(false, |i| i.is_oxker)
|
self.get_selected_container().map_or(false, |i| i.is_oxker)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the initial parsing has been completed, by making sure that all ids given (which are running) have a non empty cpu_stats vecdec
|
|
||||||
pub fn initialised(&mut self, all_ids: &[(bool, ContainerId)]) -> 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();
|
|
||||||
count_is_running == number_with_cpu_status
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the widths for the strings in the containers panel.
|
/// Find the widths for the strings in the containers panel.
|
||||||
/// So can display nicely and evenly
|
/// So can display nicely and evenly
|
||||||
pub fn get_width(&self) -> Columns {
|
pub fn get_width(&self) -> Columns {
|
||||||
|
|||||||
+1
-27
@@ -56,11 +56,6 @@ impl Binate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// struct Init {
|
|
||||||
// done: ,
|
|
||||||
// len:
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub struct DockerData {
|
pub struct DockerData {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
args: CliArgs,
|
args: CliArgs,
|
||||||
@@ -113,19 +108,7 @@ impl DockerData {
|
|||||||
spawn_id: SpawnId,
|
spawn_id: SpawnId,
|
||||||
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
||||||
) {
|
) {
|
||||||
// if dead and !init then inspect!
|
|
||||||
|
|
||||||
if state.is_alive() || init.is_some() {
|
if state.is_alive() || init.is_some() {
|
||||||
// // if state == State::Paused && init.is_some() {
|
|
||||||
// // app_data.lock().debug_string.push_str("is paused");
|
|
||||||
|
|
||||||
// // if let Ok(result) = docker.inspect_container(id.get(), Some(InspectContainerOptions{size:false})).await {
|
|
||||||
// // let mem_limit = format!("{}", result.host_config.map_or(0, |i|i.memory.unwrap_or_default()));
|
|
||||||
|
|
||||||
// // app_data.lock().debug_string.push_str(&mem_limit);
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // }else if state.is_alive() || init.is_some() {
|
|
||||||
let mut stream = docker
|
let mut stream = docker
|
||||||
.stats(
|
.stats(
|
||||||
id.get(),
|
id.get(),
|
||||||
@@ -136,14 +119,6 @@ impl DockerData {
|
|||||||
)
|
)
|
||||||
.take(1);
|
.take(1);
|
||||||
|
|
||||||
// let a = stream.next().await;
|
|
||||||
// app_data.lock().debug_string.push_str(&format!("{:?}", a.is_some()));
|
|
||||||
// let bb = a.unwrap().unwrap();
|
|
||||||
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// app_data.lock().debug_string.push_str("jkl");
|
|
||||||
|
|
||||||
while let Some(Ok(stats)) = stream.next().await {
|
while let Some(Ok(stats)) = stream.next().await {
|
||||||
let mem_stat = if state.is_alive() {
|
let mem_stat = if state.is_alive() {
|
||||||
Some(stats.memory_stats.usage.unwrap_or_default())
|
Some(stats.memory_stats.usage.unwrap_or_default())
|
||||||
@@ -186,7 +161,6 @@ impl DockerData {
|
|||||||
|
|
||||||
/// Update all stats, spawn each container into own tokio::spawn thread
|
/// Update all stats, spawn each container into own tokio::spawn thread
|
||||||
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
|
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
|
||||||
// let thing =all_ids.len();
|
|
||||||
for (state, id) in all_ids {
|
for (state, id) in all_ids {
|
||||||
// let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len())));
|
// let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len())));
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
@@ -308,7 +282,7 @@ impl DockerData {
|
|||||||
.lock()
|
.lock()
|
||||||
.entry(SpawnId::Log(container.id.clone()))
|
.entry(SpawnId::Log(container.id.clone()))
|
||||||
.or_insert_with(|| {
|
.or_insert_with(|| {
|
||||||
// TODO make a struct that can create this data
|
// MAYBE make a struct that can create this data?
|
||||||
let app_data = Arc::clone(&self.app_data);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let id = container.id.clone();
|
let id = container.id.clone();
|
||||||
|
|||||||
+12
-14
@@ -21,7 +21,6 @@ use crate::{
|
|||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
docker_data::DockerMessage,
|
docker_data::DockerMessage,
|
||||||
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui, DOCKER_COMMAND},
|
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui, DOCKER_COMMAND},
|
||||||
value_capture,
|
|
||||||
};
|
};
|
||||||
pub use message::InputMessages;
|
pub use message::InputMessages;
|
||||||
|
|
||||||
@@ -201,14 +200,13 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle any keyboard button events
|
/// Handle any keyboard button events
|
||||||
|
// TODO refactor this
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
|
async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
|
||||||
value_capture!(
|
let contains_delete = self
|
||||||
contains_delete,
|
.gui_state
|
||||||
self.gui_state
|
|
||||||
.lock()
|
.lock()
|
||||||
.status_contains(&[Status::DeleteConfirm])
|
.status_contains(&[Status::DeleteConfirm]);
|
||||||
);
|
|
||||||
|
|
||||||
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
|
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
|
||||||
|
|
||||||
@@ -258,8 +256,8 @@ impl InputHandler {
|
|||||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
// Skip control panel if no containers, could be refactored
|
// Skip control panel if no containers, could be refactored
|
||||||
let is_containers =
|
let is_containers = self.gui_state.lock().get_selected_panel()
|
||||||
self.gui_state.lock().selected_panel == SelectablePanel::Containers;
|
== SelectablePanel::Containers;
|
||||||
let count =
|
let count =
|
||||||
if self.app_data.lock().get_container_len() == 0 && is_containers {
|
if self.app_data.lock().get_container_len() == 0 && is_containers {
|
||||||
2
|
2
|
||||||
@@ -273,7 +271,7 @@ impl InputHandler {
|
|||||||
KeyCode::BackTab => {
|
KeyCode::BackTab => {
|
||||||
// Skip control panel if no containers, could be refactored
|
// Skip control panel if no containers, could be refactored
|
||||||
let is_containers =
|
let is_containers =
|
||||||
self.gui_state.lock().selected_panel == SelectablePanel::Logs;
|
self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
|
||||||
let count =
|
let count =
|
||||||
if self.app_data.lock().get_container_len() == 0 && is_containers {
|
if self.app_data.lock().get_container_len() == 0 && is_containers {
|
||||||
2
|
2
|
||||||
@@ -286,7 +284,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
KeyCode::Home => {
|
KeyCode::Home => {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
let selected_panel = self.gui_state.lock().selected_panel;
|
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||||
match selected_panel {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_start(),
|
SelectablePanel::Containers => locked_data.containers_start(),
|
||||||
SelectablePanel::Logs => locked_data.log_start(),
|
SelectablePanel::Logs => locked_data.log_start(),
|
||||||
@@ -295,7 +293,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
KeyCode::End => {
|
KeyCode::End => {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
let selected_panel = self.gui_state.lock().selected_panel;
|
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||||
match selected_panel {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_end(),
|
SelectablePanel::Containers => locked_data.containers_end(),
|
||||||
SelectablePanel::Logs => locked_data.log_end(),
|
SelectablePanel::Logs => locked_data.log_end(),
|
||||||
@@ -316,7 +314,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
// This isn't great, just means you can't send docker commands before full initialization of the program
|
// This isn't great, just means you can't send docker commands before full initialization of the program
|
||||||
let panel = self.gui_state.lock().selected_panel;
|
let panel = self.gui_state.lock().get_selected_panel();
|
||||||
if panel == SelectablePanel::Commands {
|
if panel == SelectablePanel::Commands {
|
||||||
let option_command = self.app_data.lock().selected_docker_command();
|
let option_command = self.app_data.lock().selected_docker_command();
|
||||||
|
|
||||||
@@ -417,7 +415,7 @@ impl InputHandler {
|
|||||||
/// Change state to next, depending which panel is currently in focus
|
/// Change state to next, depending which panel is currently in focus
|
||||||
fn next(&mut self) {
|
fn next(&mut self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
let selected_panel = self.gui_state.lock().selected_panel;
|
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||||
match selected_panel {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_next(),
|
SelectablePanel::Containers => locked_data.containers_next(),
|
||||||
SelectablePanel::Logs => locked_data.log_next(),
|
SelectablePanel::Logs => locked_data.log_next(),
|
||||||
@@ -428,7 +426,7 @@ impl InputHandler {
|
|||||||
/// Change state to previous, depending which panel is currently in focus
|
/// Change state to previous, depending which panel is currently in focus
|
||||||
fn previous(&mut self) {
|
fn previous(&mut self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
let selected_panel = self.gui_state.lock().selected_panel;
|
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||||
match selected_panel {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_previous(),
|
SelectablePanel::Containers => locked_data.containers_previous(),
|
||||||
SelectablePanel::Logs => locked_data.log_previous(),
|
SelectablePanel::Logs => locked_data.log_previous(),
|
||||||
|
|||||||
+37
-14
@@ -66,6 +66,7 @@ fn read_docker_host(args: &CliArgs) -> Option<String> {
|
|||||||
async fn docker_init(
|
async fn docker_init(
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
docker_rx: Receiver<DockerMessage>,
|
docker_rx: Receiver<DockerMessage>,
|
||||||
|
docker_tx: Sender<DockerMessage>,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
is_running: &Arc<AtomicBool>,
|
is_running: &Arc<AtomicBool>,
|
||||||
host: Option<String>,
|
host: Option<String>,
|
||||||
@@ -79,8 +80,9 @@ async fn docker_init(
|
|||||||
let app_data = Arc::clone(app_data);
|
let app_data = Arc::clone(app_data);
|
||||||
let gui_state = Arc::clone(gui_state);
|
let gui_state = Arc::clone(gui_state);
|
||||||
let is_running = Arc::clone(is_running);
|
let is_running = Arc::clone(is_running);
|
||||||
|
|
||||||
tokio::spawn(DockerData::init(
|
tokio::spawn(DockerData::init(
|
||||||
app_data, docker, docker_rx, gui_state, is_running,
|
app_data, docker, docker_rx, docker_tx, gui_state, is_running,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
app_data
|
app_data
|
||||||
@@ -102,15 +104,15 @@ fn handler_init(
|
|||||||
input_rx: Receiver<InputMessages>,
|
input_rx: Receiver<InputMessages>,
|
||||||
is_running: &Arc<AtomicBool>,
|
is_running: &Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let input_app_data = Arc::clone(app_data);
|
let app_data = Arc::clone(app_data);
|
||||||
let input_gui_state = Arc::clone(gui_state);
|
let gui_state = Arc::clone(gui_state);
|
||||||
let input_is_running = Arc::clone(is_running);
|
let is_running = Arc::clone(is_running);
|
||||||
tokio::spawn(input_handler::InputHandler::init(
|
tokio::spawn(input_handler::InputHandler::init(
|
||||||
input_app_data,
|
app_data,
|
||||||
input_rx,
|
input_rx,
|
||||||
docker_sx.clone(),
|
docker_sx.clone(),
|
||||||
input_gui_state,
|
gui_state,
|
||||||
input_is_running,
|
is_running,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,28 +126,49 @@ async fn main() {
|
|||||||
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
||||||
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
||||||
let is_running = Arc::new(AtomicBool::new(true));
|
let is_running = Arc::new(AtomicBool::new(true));
|
||||||
let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(32);
|
let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
|
||||||
|
|
||||||
docker_init(&app_data, docker_rx, &gui_state, &is_running, host).await;
|
docker_init(
|
||||||
|
&app_data,
|
||||||
|
docker_rx,
|
||||||
|
docker_tx.clone(),
|
||||||
|
&gui_state,
|
||||||
|
&is_running,
|
||||||
|
host,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
if args.gui {
|
if args.gui {
|
||||||
let (input_sx, input_rx) = tokio::sync::mpsc::channel(32);
|
let (input_sx, input_rx) = tokio::sync::mpsc::channel(32);
|
||||||
handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running);
|
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
|
||||||
Ui::create(app_data, docker_sx, gui_state, is_running, input_sx).await;
|
Ui::create(app_data, gui_state, is_running, input_sx).await;
|
||||||
} else {
|
} else {
|
||||||
info!("in debug mode");
|
info!("in debug mode\n");
|
||||||
// Debug mode for testing, mostly pointless, doesn't take terminal
|
// Debug mode for testing, less pointless now, will diplay some basic information
|
||||||
while is_running.load(Ordering::SeqCst) {
|
while is_running.load(Ordering::SeqCst) {
|
||||||
loop {
|
loop {
|
||||||
if let Some(err) = app_data.lock().get_error() {
|
if let Some(err) = app_data.lock().get_error() {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
docker_sx.send(DockerMessage::Update).await.ok();
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
||||||
args.docker_interval,
|
args.docker_interval,
|
||||||
)))
|
)))
|
||||||
.await;
|
.await;
|
||||||
|
let containers = app_data
|
||||||
|
.lock()
|
||||||
|
.get_container_items()
|
||||||
|
.clone()
|
||||||
|
.iter()
|
||||||
|
.map(|i| format!("{i}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if !containers.is_empty() {
|
||||||
|
for item in containers {
|
||||||
|
info!("{item}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-65
@@ -13,14 +13,16 @@ use ratatui::{
|
|||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::{fmt::Display, sync::Arc};
|
use std::{fmt::Display, sync::Arc};
|
||||||
|
|
||||||
use crate::app_data::{Header, SortedOrder};
|
use crate::app_data::{ContainerItem, Header, SortedOrder};
|
||||||
use crate::ui::Status;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::gui_state::{BoxLocation, DeleteButton, Region};
|
use super::{
|
||||||
|
gui_state::{BoxLocation, DeleteButton, Region},
|
||||||
|
FrameData,
|
||||||
|
};
|
||||||
use super::{GuiState, SelectablePanel};
|
use super::{GuiState, SelectablePanel};
|
||||||
|
|
||||||
const NAME_TEXT: &str = r#"
|
const NAME_TEXT: &str = r#"
|
||||||
@@ -39,7 +41,7 @@ const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
|||||||
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||||
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||||
const MARGIN: &str = " ";
|
const MARGIN: &str = " ";
|
||||||
const ARROW: &str = "▶ ";
|
const RIGHT_ARROW: &str = "▶ ";
|
||||||
const CIRCLE: &str = "⚪ ";
|
const CIRCLE: &str = "⚪ ";
|
||||||
|
|
||||||
/// From a given &str, return the maximum number of chars on a single line
|
/// From a given &str, return the maximum number of chars on a single line
|
||||||
@@ -55,6 +57,7 @@ fn max_line_width(text: &str) -> usize {
|
|||||||
fn generate_block<'a>(
|
fn generate_block<'a>(
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
panel: SelectablePanel,
|
panel: SelectablePanel,
|
||||||
) -> Block<'a> {
|
) -> Block<'a> {
|
||||||
@@ -77,7 +80,7 @@ fn generate_block<'a>(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.title(title);
|
.title(title);
|
||||||
if gui_state.lock().selected_panel == panel {
|
if fd.selected_panel == panel {
|
||||||
block = block.border_style(Style::default().fg(Color::LightCyan));
|
block = block.border_style(Style::default().fg(Color::LightCyan));
|
||||||
}
|
}
|
||||||
block
|
block
|
||||||
@@ -88,9 +91,10 @@ pub fn commands(
|
|||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
) {
|
) {
|
||||||
let block = || generate_block(app_data, area, gui_state, SelectablePanel::Commands);
|
let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands);
|
||||||
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
|
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
|
||||||
i.iter()
|
i.iter()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
@@ -106,7 +110,7 @@ pub fn commands(
|
|||||||
let items = List::new(items)
|
let items = List::new(items)
|
||||||
.block(block())
|
.block(block())
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||||
.highlight_symbol(ARROW);
|
.highlight_symbol(RIGHT_ARROW);
|
||||||
|
|
||||||
if let Some(i) = app_data.lock().get_control_state() {
|
if let Some(i) = app_data.lock().get_control_state() {
|
||||||
f.render_stateful_widget(items, area, i);
|
f.render_stateful_widget(items, area, i);
|
||||||
@@ -118,25 +122,12 @@ pub fn commands(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the containers panel
|
/// Format the container data to display nicely on the screen
|
||||||
pub fn containers(
|
fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
|
||||||
area: Rect,
|
|
||||||
f: &mut Frame,
|
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
|
||||||
widths: &Columns,
|
|
||||||
) {
|
|
||||||
let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers);
|
|
||||||
|
|
||||||
let items = app_data
|
|
||||||
.lock()
|
|
||||||
.get_container_items()
|
|
||||||
.iter()
|
|
||||||
.map(|i| {
|
|
||||||
let state_style = Style::default().fg(i.state.get_color());
|
let state_style = Style::default().fg(i.state.get_color());
|
||||||
let blue = Style::default().fg(Color::Blue);
|
let blue = Style::default().fg(Color::Blue);
|
||||||
|
|
||||||
let lines = Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!(
|
format!(
|
||||||
"{:<width$}",
|
"{:<width$}",
|
||||||
@@ -176,7 +167,7 @@ pub fn containers(
|
|||||||
format!(
|
format!(
|
||||||
"{}{:>width$}",
|
"{}{:>width$}",
|
||||||
MARGIN,
|
MARGIN,
|
||||||
i.id.get().chars().take(8).collect::<String>(),
|
i.id.get_short(),
|
||||||
width = &widths.id.1.into()
|
width = &widths.id.1.into()
|
||||||
),
|
),
|
||||||
blue,
|
blue,
|
||||||
@@ -197,9 +188,25 @@ pub fn containers(
|
|||||||
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
|
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
|
||||||
Style::default().fg(Color::Rgb(205, 140, 140)),
|
Style::default().fg(Color::Rgb(205, 140, 140)),
|
||||||
),
|
),
|
||||||
]);
|
])
|
||||||
ListItem::new(lines)
|
}
|
||||||
})
|
|
||||||
|
/// Draw the containers panel
|
||||||
|
pub fn containers(
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
area: Rect,
|
||||||
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
widths: &Columns,
|
||||||
|
) {
|
||||||
|
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
|
||||||
|
|
||||||
|
let items = app_data
|
||||||
|
.lock()
|
||||||
|
.get_container_items()
|
||||||
|
.iter()
|
||||||
|
.map(|i| ListItem::new(format_containers(i, widths)))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
@@ -212,7 +219,6 @@ pub fn containers(
|
|||||||
.block(block)
|
.block(block)
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||||
.highlight_symbol(CIRCLE);
|
.highlight_symbol(CIRCLE);
|
||||||
|
|
||||||
f.render_stateful_widget(items, area, app_data.lock().get_container_state());
|
f.render_stateful_widget(items, area, app_data.lock().get_container_state());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,12 +228,12 @@ pub fn logs(
|
|||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
loading_icon: &str,
|
|
||||||
) {
|
) {
|
||||||
let block = || generate_block(app_data, area, gui_state, SelectablePanel::Logs);
|
let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Logs);
|
||||||
if gui_state.lock().status_contains(&[Status::Init]) {
|
if fd.init {
|
||||||
let paragraph = Paragraph::new(format!("parsing logs {loading_icon}"))
|
let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
|
||||||
.style(Style::default())
|
.style(Style::default())
|
||||||
.block(block())
|
.block(block())
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
@@ -243,12 +249,11 @@ pub fn logs(
|
|||||||
} else {
|
} else {
|
||||||
let items = List::new(logs)
|
let items = List::new(logs)
|
||||||
.block(block())
|
.block(block())
|
||||||
.highlight_symbol(ARROW)
|
.highlight_symbol(RIGHT_ARROW)
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||||
|
|
||||||
// This should always return Some, as logs is not empty
|
// This should always return Some, as logs is not empty
|
||||||
if let Some(i) = app_data.lock().get_log_state() {
|
if let Some(log_state) = app_data.lock().get_log_state() {
|
||||||
f.render_stateful_widget(items, area, i);
|
f.render_stateful_widget(items, area, log_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,24 +343,20 @@ fn make_chart<'a, T: Stats + Display>(
|
|||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn heading_bar(
|
pub fn heading_bar(
|
||||||
area: Rect,
|
area: Rect,
|
||||||
columns: &Columns,
|
frame: &mut Frame,
|
||||||
f: &mut Frame,
|
data: &FrameData,
|
||||||
has_containers: bool,
|
|
||||||
loading_icon: &str,
|
|
||||||
sorted_by: Option<(Header, SortedOrder)>,
|
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
) {
|
) {
|
||||||
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
|
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
|
||||||
let help_visible = gui_state.lock().status_contains(&[Status::Help]);
|
|
||||||
|
|
||||||
f.render_widget(block(Color::Black), area);
|
frame.render_widget(block(Color::Black), area);
|
||||||
|
|
||||||
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
|
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
|
||||||
let header_block = |x: &Header| {
|
let header_block = |x: &Header| {
|
||||||
let mut color = Color::Black;
|
let mut color = Color::Black;
|
||||||
let mut suffix = "";
|
let mut suffix = "";
|
||||||
let mut suffix_margin = 0;
|
let mut suffix_margin = 0;
|
||||||
if let Some((a, b)) = sorted_by.as_ref() {
|
if let Some((a, b)) = data.sorted_by.as_ref() {
|
||||||
if x == a {
|
if x == a {
|
||||||
match b {
|
match b {
|
||||||
SortedOrder::Asc => suffix = " ⌃",
|
SortedOrder::Asc => suffix = " ⌃",
|
||||||
@@ -407,15 +408,15 @@ pub fn heading_bar(
|
|||||||
|
|
||||||
// Meta data to iterate over to create blocks with correct widths
|
// Meta data to iterate over to create blocks with correct widths
|
||||||
let header_meta = [
|
let header_meta = [
|
||||||
(Header::State, columns.state.1),
|
(Header::State, data.columns.state.1),
|
||||||
(Header::Status, columns.status.1),
|
(Header::Status, data.columns.status.1),
|
||||||
(Header::Cpu, columns.cpu.1),
|
(Header::Cpu, data.columns.cpu.1),
|
||||||
(Header::Memory, columns.mem.1 + columns.mem.2 + 3),
|
(Header::Memory, data.columns.mem.1 + data.columns.mem.2 + 3),
|
||||||
(Header::Id, columns.id.1),
|
(Header::Id, data.columns.id.1),
|
||||||
(Header::Name, columns.name.1),
|
(Header::Name, data.columns.name.1),
|
||||||
(Header::Image, columns.image.1),
|
(Header::Image, data.columns.image.1),
|
||||||
(Header::Rx, columns.net_rx.1),
|
(Header::Rx, data.columns.net_rx.1),
|
||||||
(Header::Tx, columns.net_tx.1),
|
(Header::Tx, data.columns.net_tx.1),
|
||||||
];
|
];
|
||||||
|
|
||||||
let header_data = header_meta
|
let header_data = header_meta
|
||||||
@@ -426,13 +427,13 @@ pub fn heading_bar(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let suffix = if help_visible { "exit" } else { "show" };
|
let suffix = if data.help_visible { "exit" } else { "show" };
|
||||||
let info_text = format!("( h ) {suffix} help {MARGIN}",);
|
let info_text = format!("( h ) {suffix} help {MARGIN}",);
|
||||||
let info_width = info_text.chars().count();
|
let info_width = info_text.chars().count();
|
||||||
|
|
||||||
let column_width = usize::from(area.width).saturating_sub(info_width);
|
let column_width = usize::from(area.width).saturating_sub(info_width);
|
||||||
let column_width = if column_width > 0 { column_width } else { 1 };
|
let column_width = if column_width > 0 { column_width } else { 1 };
|
||||||
let splits = if has_containers {
|
let splits = if data.has_containers {
|
||||||
vec![
|
vec![
|
||||||
Constraint::Min(2),
|
Constraint::Min(2),
|
||||||
Constraint::Min(column_width.try_into().unwrap_or_default()),
|
Constraint::Min(column_width.try_into().unwrap_or_default()),
|
||||||
@@ -446,13 +447,12 @@ pub fn heading_bar(
|
|||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(splits)
|
.constraints(splits)
|
||||||
.split(area);
|
.split(area);
|
||||||
if has_containers {
|
if data.has_containers {
|
||||||
// Draw loading icon, or not, and a prefix with a single space
|
// Draw loading icon, or not, and a prefix with a single space
|
||||||
let loading_icon = format!("{loading_icon:>2}");
|
let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
|
||||||
let loading_paragraph = Paragraph::new(loading_icon)
|
|
||||||
.block(block(Color::White))
|
.block(block(Color::White))
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
f.render_widget(loading_paragraph, split_bar[0]);
|
frame.render_widget(loading_paragraph, split_bar[0]);
|
||||||
|
|
||||||
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
||||||
let headers_section = Layout::default()
|
let headers_section = Layout::default()
|
||||||
@@ -466,12 +466,12 @@ pub fn heading_bar(
|
|||||||
gui_state
|
gui_state
|
||||||
.lock()
|
.lock()
|
||||||
.update_region_map(Region::Header(header), rect);
|
.update_region_map(Region::Header(header), rect);
|
||||||
f.render_widget(paragraph, rect);
|
frame.render_widget(paragraph, rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// show/hide help
|
// show/hide help
|
||||||
let color = if help_visible {
|
let color = if data.help_visible {
|
||||||
Color::Black
|
Color::Black
|
||||||
} else {
|
} else {
|
||||||
Color::White
|
Color::White
|
||||||
@@ -481,8 +481,8 @@ pub fn heading_bar(
|
|||||||
.alignment(Alignment::Right);
|
.alignment(Alignment::Right);
|
||||||
|
|
||||||
// If no containers, don't display the headers, could maybe do this first?
|
// If no containers, don't display the headers, could maybe do this first?
|
||||||
let help_index = if has_containers { 2 } else { 0 };
|
let help_index = if data.has_containers { 2 } else { 0 };
|
||||||
f.render_widget(help_paragraph, split_bar[help_index]);
|
frame.render_widget(help_paragraph, split_bar[help_index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Help popup box needs these three pieces of information
|
/// Help popup box needs these three pieces of information
|
||||||
@@ -876,13 +876,13 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option<u8>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw info box in one of the 9 BoxLocations
|
/// Draw info box in one of the 9 BoxLocations
|
||||||
pub fn info(f: &mut Frame, text: String) {
|
pub fn info(f: &mut Frame, text: &str) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title("")
|
.title("")
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.borders(Borders::NONE);
|
.borders(Borders::NONE);
|
||||||
|
|
||||||
let mut max_line_width = max_line_width(&text);
|
let mut max_line_width = max_line_width(text);
|
||||||
let mut lines = text.lines().count();
|
let mut lines = text.lines().count();
|
||||||
|
|
||||||
// Add some horizontal & vertical margins
|
// Add some horizontal & vertical margins
|
||||||
@@ -927,6 +927,16 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio
|
|||||||
.split(popup_layout[indexes.0])[indexes.1]
|
.split(popup_layout[indexes.0])[indexes.1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
// Single row at the top of the screen for debugging
|
||||||
|
pub fn debug_bar(area: Rect, f: &mut Frame, debug_string: &str) {
|
||||||
|
let block = Block::default().style(Style::default().bg(Color::Red));
|
||||||
|
let paragraph = Paragraph::new(debug_string)
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.block(block);
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw nothing, as in a blank screen
|
// Draw nothing, as in a blank screen
|
||||||
// pub fn nothing(f: &mut Frame) {
|
// pub fn nothing(f: &mut Frame) {
|
||||||
// let whole_layout = Layout::default()
|
// let whole_layout = Layout::default()
|
||||||
|
|||||||
+9
-4
@@ -166,15 +166,15 @@ pub enum Status {
|
|||||||
/// Global gui_state, stored in an Arc<Mutex>
|
/// Global gui_state, stored in an Arc<Mutex>
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct GuiState {
|
pub struct GuiState {
|
||||||
|
delete_container: Option<ContainerId>,
|
||||||
|
delete_map: HashMap<DeleteButton, Rect>,
|
||||||
heading_map: HashMap<Header, Rect>,
|
heading_map: HashMap<Header, Rect>,
|
||||||
is_loading: HashSet<Uuid>,
|
is_loading: HashSet<Uuid>,
|
||||||
loading_index: u8,
|
loading_index: u8,
|
||||||
panel_map: HashMap<SelectablePanel, Rect>,
|
panel_map: HashMap<SelectablePanel, Rect>,
|
||||||
delete_map: HashMap<DeleteButton, Rect>,
|
selected_panel: SelectablePanel,
|
||||||
status: HashSet<Status>,
|
status: HashSet<Status>,
|
||||||
delete_container: Option<ContainerId>,
|
|
||||||
pub info_box_text: Option<String>,
|
pub info_box_text: Option<String>,
|
||||||
pub selected_panel: SelectablePanel,
|
|
||||||
}
|
}
|
||||||
impl GuiState {
|
impl GuiState {
|
||||||
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||||
@@ -182,6 +182,11 @@ impl GuiState {
|
|||||||
self.panel_map.clear();
|
self.panel_map.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the currently selected panel
|
||||||
|
pub const fn get_selected_panel(&self) -> SelectablePanel {
|
||||||
|
self.selected_panel
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||||
pub fn panel_intersect(&mut self, rect: Rect) {
|
pub fn panel_intersect(&mut self, rect: Rect) {
|
||||||
if let Some(data) = self
|
if let Some(data) = self
|
||||||
@@ -293,7 +298,7 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
|
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
|
||||||
pub fn get_loading(&mut self) -> char {
|
pub fn get_loading(&self) -> char {
|
||||||
if self.is_loading.is_empty() {
|
if self.is_loading.is_empty() {
|
||||||
' '
|
' '
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+94
-73
@@ -4,7 +4,7 @@ use crossterm::{
|
|||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::{Mutex, MutexGuard};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
@@ -26,16 +26,16 @@ mod gui_state;
|
|||||||
pub use self::color_match::*;
|
pub use self::color_match::*;
|
||||||
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::AppData, app_error::AppError, docker_data::DockerMessage,
|
app_data::{AppData, Columns, ContainerId, Header, SortedOrder},
|
||||||
input_handler::InputMessages, parse_args::CliArgs,
|
app_error::AppError,
|
||||||
|
input_handler::InputMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const DOCKER_COMMAND: &str = "docker";
|
pub const DOCKER_COMMAND: &str = "docker";
|
||||||
|
|
||||||
pub struct Ui {
|
pub struct Ui {
|
||||||
args: CliArgs,
|
// args: CliArgs,
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker_sx: Sender<DockerMessage>,
|
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
input_poll_rate: Duration,
|
input_poll_rate: Duration,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
@@ -60,17 +60,14 @@ impl Ui {
|
|||||||
/// Create a new Ui struct, and execute the drawing loop
|
/// Create a new Ui struct, and execute the drawing loop
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker_sx: Sender<DockerMessage>,
|
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
sender: Sender<InputMessages>,
|
sender: Sender<InputMessages>,
|
||||||
) {
|
) {
|
||||||
if let Ok(terminal) = Self::setup_terminal() {
|
if let Ok(terminal) = Self::setup_terminal() {
|
||||||
let args = app_data.lock().args.clone();
|
// let args = app_data.lock().args.clone();
|
||||||
let mut ui = Self {
|
let mut ui = Self {
|
||||||
args,
|
|
||||||
app_data,
|
app_data,
|
||||||
docker_sx,
|
|
||||||
gui_state,
|
gui_state,
|
||||||
input_poll_rate: std::time::Duration::from_millis(100),
|
input_poll_rate: std::time::Duration::from_millis(100),
|
||||||
is_running,
|
is_running,
|
||||||
@@ -158,10 +155,8 @@ impl Ui {
|
|||||||
if child.kill().is_err() {
|
if child.kill().is_err() {
|
||||||
std::process::exit(1)
|
std::process::exit(1)
|
||||||
}
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.terminal.clear().ok();
|
self.terminal.clear().ok();
|
||||||
self.reset_terminal().ok();
|
self.reset_terminal().ok();
|
||||||
Self::init_terminal().ok();
|
Self::init_terminal().ok();
|
||||||
@@ -170,16 +165,10 @@ impl Ui {
|
|||||||
|
|
||||||
/// The loop for drawing the main UI to the terminal
|
/// The loop for drawing the main UI to the terminal
|
||||||
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
||||||
let update_duration =
|
|
||||||
std::time::Duration::from_millis(u64::from(self.args.docker_interval));
|
|
||||||
|
|
||||||
while self.is_running.load(Ordering::SeqCst) {
|
while self.is_running.load(Ordering::SeqCst) {
|
||||||
let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
|
let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
|
||||||
|
|
||||||
if exec {
|
if exec {
|
||||||
self.exec();
|
self.exec();
|
||||||
self.docker_sx.send(DockerMessage::Update).await.ok();
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self
|
if self
|
||||||
@@ -212,12 +201,6 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should this be done in the docker thread instead?
|
|
||||||
if self.now.elapsed() >= update_duration {
|
|
||||||
self.docker_sx.send(DockerMessage::Update).await.ok();
|
|
||||||
self.now = Instant::now();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -237,48 +220,94 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
// #[macro_export]
|
||||||
/// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression.
|
// /// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression.
|
||||||
macro_rules! value_capture {
|
// macro_rules! value_capture {
|
||||||
($name:ident, $lock_expr:expr) => {
|
// ($name:ident, $lock_expr:expr) => {
|
||||||
let $name = || $lock_expr;
|
// let $name = || $lock_expr;
|
||||||
let $name = $name();
|
// let $name = $name();
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
|
||||||
|
.split(f.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(1), Constraint::Min(1), Constraint::Min(100)].as_ref())
|
||||||
|
.split(f.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FrameData {
|
||||||
|
columns: Columns,
|
||||||
|
delete_confirm: Option<ContainerId>,
|
||||||
|
has_containers: bool,
|
||||||
|
has_error: Option<AppError>,
|
||||||
|
height: u16,
|
||||||
|
help_visible: bool,
|
||||||
|
init: bool,
|
||||||
|
info_text: Option<String>,
|
||||||
|
loading_icon: String,
|
||||||
|
selected_panel: SelectablePanel,
|
||||||
|
sorted_by: Option<(Header, SortedOrder)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
|
||||||
|
fn from(data: (MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)) -> Self {
|
||||||
|
// set max height for container section, needs +5 to deal with docker commands list and borders
|
||||||
|
let height = data.0.get_container_len();
|
||||||
|
let height = if height < 12 {
|
||||||
|
u16::try_from(height + 5).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
12
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
columns: data.0.get_width(),
|
||||||
|
delete_confirm: data.1.get_delete_container(),
|
||||||
|
has_containers: data.0.get_container_len() > 1,
|
||||||
|
has_error: data.0.get_error(),
|
||||||
|
height,
|
||||||
|
help_visible: data.1.status_contains(&[Status::Help]),
|
||||||
|
init: data.1.status_contains(&[Status::Init]),
|
||||||
|
info_text: data.1.info_box_text.clone(),
|
||||||
|
loading_icon: data.1.get_loading().to_string(),
|
||||||
|
selected_panel: data.1.get_selected_panel(),
|
||||||
|
sorted_by: data.0.get_sorted(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the main ui to a frame of the terminal
|
/// Draw the main ui to a frame of the terminal
|
||||||
/// TODO add a single line area for debug message - if not in release mode?
|
|
||||||
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
|
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
|
||||||
value_capture!(height, app_data.lock().get_container_len());
|
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
|
||||||
value_capture!(column_widths, app_data.lock().get_width());
|
|
||||||
value_capture!(has_containers, app_data.lock().get_container_len() > 0);
|
|
||||||
value_capture!(sorted_by, app_data.lock().get_sorted());
|
|
||||||
value_capture!(delete_confirm, gui_state.lock().get_delete_container());
|
|
||||||
value_capture!(has_error, app_data.lock().get_error());
|
|
||||||
value_capture!(info_text, gui_state.lock().info_box_text.clone());
|
|
||||||
value_capture!(loading_icon, gui_state.lock().get_loading().to_string());
|
|
||||||
|
|
||||||
// set max height for container section, needs +5 to deal with docker commands list and borders
|
let whole_layout = get_wholelayout(f);
|
||||||
let height = if height < 12 { height + 5 } else { 12 };
|
#[cfg(debug_assertions)]
|
||||||
|
draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string());
|
||||||
|
|
||||||
let whole_layout = Layout::default()
|
#[cfg(debug_assertions)]
|
||||||
.direction(Direction::Vertical)
|
let whole_layout_split = (1, 2);
|
||||||
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
|
|
||||||
.split(f.size());
|
#[cfg(not(debug_assertions))]
|
||||||
|
let whole_layout_split = (0, 1);
|
||||||
|
|
||||||
// Split into 3, containers+controls, logs, then graphs
|
// Split into 3, containers+controls, logs, then graphs
|
||||||
let upper_main = Layout::default()
|
let upper_main = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(
|
.constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref())
|
||||||
[
|
.split(whole_layout[whole_layout_split.1]);
|
||||||
Constraint::Max(height.try_into().unwrap_or_default()),
|
|
||||||
Constraint::Percentage(50),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(whole_layout[1]);
|
|
||||||
|
|
||||||
let top_split = if has_containers {
|
let top_split = if fd.has_containers {
|
||||||
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
|
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
|
||||||
} else {
|
} else {
|
||||||
vec![Constraint::Percentage(100)]
|
vec![Constraint::Percentage(100)]
|
||||||
@@ -289,7 +318,7 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
.constraints(top_split)
|
.constraints(top_split)
|
||||||
.split(upper_main[0]);
|
.split(upper_main[0]);
|
||||||
|
|
||||||
let lower_split = if has_containers {
|
let lower_split = if fd.has_containers {
|
||||||
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
|
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
|
||||||
} else {
|
} else {
|
||||||
vec![Constraint::Percentage(100)]
|
vec![Constraint::Percentage(100)]
|
||||||
@@ -301,22 +330,14 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
.constraints(lower_split)
|
.constraints(lower_split)
|
||||||
.split(upper_main[1]);
|
.split(upper_main[1]);
|
||||||
|
|
||||||
draw_blocks::containers(app_data, top_panel[0], f, gui_state, &column_widths);
|
draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state, &fd.columns);
|
||||||
|
|
||||||
draw_blocks::logs(app_data, lower_main[0], f, gui_state, &loading_icon);
|
draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state);
|
||||||
|
|
||||||
draw_blocks::heading_bar(
|
draw_blocks::heading_bar(whole_layout[whole_layout_split.0], f, &fd, gui_state);
|
||||||
whole_layout[0],
|
|
||||||
&column_widths,
|
|
||||||
f,
|
|
||||||
has_containers,
|
|
||||||
&loading_icon,
|
|
||||||
sorted_by,
|
|
||||||
gui_state,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(id) = delete_confirm {
|
if let Some(id) = fd.delete_confirm.as_ref() {
|
||||||
app_data.lock().get_container_name_by_id(&id).map_or_else(
|
app_data.lock().get_container_name_by_id(id).map_or_else(
|
||||||
|| {
|
|| {
|
||||||
// If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation
|
// If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation
|
||||||
// so if in that unique situation, just clear the delete_container id
|
// so if in that unique situation, just clear the delete_container id
|
||||||
@@ -329,21 +350,21 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
}
|
}
|
||||||
|
|
||||||
// only draw commands + charts if there are containers
|
// only draw commands + charts if there are containers
|
||||||
if has_containers {
|
if fd.has_containers {
|
||||||
draw_blocks::commands(app_data, top_panel[1], f, gui_state);
|
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
|
||||||
draw_blocks::chart(f, lower_main[1], app_data);
|
draw_blocks::chart(f, lower_main[1], app_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(info) = info_text {
|
if let Some(info) = fd.info_text {
|
||||||
draw_blocks::info(f, info);
|
draw_blocks::info(f, &info);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if error, and show popup if so
|
// Check if error, and show popup if so
|
||||||
if gui_state.lock().status_contains(&[Status::Help]) {
|
if fd.help_visible {
|
||||||
draw_blocks::help_box(f);
|
draw_blocks::help_box(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(error) = has_error {
|
if let Some(error) = fd.has_error {
|
||||||
draw_blocks::error(f, error, None);
|
draw_blocks::error(f, error, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user